feat: 设置语言精简中英+补全时区;构建节点磁盘显示;浏览器 favicon
- 系统设置:界面语言仅保留 简体中文/English;时区按 亚/欧/美/大洋洲 分组补全 22 个常用时区 - 构建节点:BuildNode 新增 diskInfo 字段(模型/CRUD/seed),卡片显示 磁盘用量(未上报显示 —),添加/编辑弹窗可维护 - 新增 favicon.svg(主题渐变火箭图标),index.html 注入 icon 链接
This commit is contained in:
1 parent
581d0a8351
commit
a3b4637e19
7 files changed
+71
-10
No files matched your search
@@ -203,9 +203,9 @@ func seedResources() {
|
|||||||
if n == 0 {
|
if n == 0 {
|
||||||
hb := time.Now().Add(-10 * time.Second)
|
hb := time.Now().Add(-10 * time.Second)
|
||||||
DB.Create([]model.BuildNode{
|
DB.Create([]model.BuildNode{
|
||||||
{Name: "build-host-01", IP: "10.0.8.11", Labels: `["docker","go","java"]`, MaxConcurrency: 4, Status: "online", CurrentJobs: 1, LastHeartbeat: &hb},
|
{Name: "build-host-01", IP: "10.0.8.11", Labels: `["docker","go","java"]`, MaxConcurrency: 4, Status: "online", CurrentJobs: 1, DiskInfo: "78G/200G", LastHeartbeat: &hb},
|
||||||
{Name: "build-host-02", IP: "10.0.8.12", Labels: `["docker","nodejs"]`, MaxConcurrency: 2, Status: "busy", CurrentJobs: 2, LastHeartbeat: &hb},
|
{Name: "build-host-02", IP: "10.0.8.12", Labels: `["docker","nodejs"]`, MaxConcurrency: 2, Status: "busy", CurrentJobs: 2, DiskInfo: "156G/500G", LastHeartbeat: &hb},
|
||||||
{Name: "build-host-03", IP: "10.0.8.13", Labels: `["python"]`, MaxConcurrency: 2, Status: "offline", LastHeartbeat: &hb},
|
{Name: "build-host-03", IP: "10.0.8.13", Labels: `["python"]`, MaxConcurrency: 2, Status: "offline", DiskInfo: "45G/100G", LastHeartbeat: &hb},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
DB.Model(&model.WebhookEndpoint{}).Count(&n)
|
DB.Model(&model.WebhookEndpoint{}).Count(&n)
|
||||||
|
|||||||
@@ -394,7 +394,7 @@ func UpdateBuildNode(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
var raw map[string]any
|
var raw map[string]any
|
||||||
_ = c.ShouldBindJSON(&raw)
|
_ = c.ShouldBindJSON(&raw)
|
||||||
for _, k := range []string{"name", "ip", "labels", "maxConcurrency", "workspacePath", "enabled"} {
|
for _, k := range []string{"name", "ip", "labels", "maxConcurrency", "workspacePath", "diskInfo", "enabled"} {
|
||||||
if v, ok := raw[k]; ok {
|
if v, ok := raw[k]; ok {
|
||||||
database.DB.Model(&n).Update(k, v)
|
database.DB.Model(&n).Update(k, v)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -204,6 +204,7 @@ type BuildNode struct {
|
|||||||
Labels string `gorm:"type:text" json:"labels"` // JSON ["docker","go"]
|
Labels string `gorm:"type:text" json:"labels"` // JSON ["docker","go"]
|
||||||
MaxConcurrency int `gorm:"default:2" json:"maxConcurrency"`
|
MaxConcurrency int `gorm:"default:2" json:"maxConcurrency"`
|
||||||
WorkspacePath string `gorm:"default:/data/flowstack-workspace" json:"workspacePath"`
|
WorkspacePath string `gorm:"default:/data/flowstack-workspace" json:"workspacePath"`
|
||||||
|
DiskInfo string `json:"diskInfo"` // 如 "45G/200G"
|
||||||
Enabled bool `gorm:"default:true" json:"enabled"`
|
Enabled bool `gorm:"default:true" json:"enabled"`
|
||||||
Status string `gorm:"default:offline" json:"status"` // online|offline|busy
|
Status string `gorm:"default:offline" json:"status"` // online|offline|busy
|
||||||
CurrentJobs int `gorm:"default:0" json:"currentJobs"`
|
CurrentJobs int `gorm:"default:0" json:"currentJobs"`
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<title>FlowStack · 发布系统</title>
|
<title>FlowStack · 发布系统</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0" stop-color="#0969da"/>
|
||||||
|
<stop offset="1" stop-color="#8250df"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect x="4" y="4" width="56" height="56" rx="14" fill="url(#g)"/>
|
||||||
|
<g transform="translate(32 33) rotate(45) translate(-11 -13)">
|
||||||
|
<path d="M11 0 C17 4 20 10 20 17 L20 22 L14 22 C8 22 3 19 0 13 L0 0 Z" fill="#fff" opacity="0.95"/>
|
||||||
|
<circle cx="13" cy="9" r="3" fill="#0969da"/>
|
||||||
|
<path d="M0 13 L-6 20 L1 18 Z" fill="#ffd23e"/>
|
||||||
|
<path d="M20 22 L26 28 L19 27 Z" fill="#ffd23e" opacity="0.9"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 671 B |
@@ -10,6 +10,7 @@ import {
|
|||||||
Trash2,
|
Trash2,
|
||||||
Tag,
|
Tag,
|
||||||
Zap,
|
Zap,
|
||||||
|
HardDrive,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
@@ -60,9 +61,10 @@ interface NodeForm {
|
|||||||
labels: string;
|
labels: string;
|
||||||
maxConcurrency: string;
|
maxConcurrency: string;
|
||||||
workspacePath: string;
|
workspacePath: string;
|
||||||
|
diskInfo: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const emptyForm: NodeForm = { name: '', ip: '', labels: '', maxConcurrency: '2', workspacePath: '/var/lib/flowstack/workspace' };
|
const emptyForm: NodeForm = { name: '', ip: '', labels: '', maxConcurrency: '2', workspacePath: '/var/lib/flowstack/workspace', diskInfo: '' };
|
||||||
|
|
||||||
const inputStyle = {
|
const inputStyle = {
|
||||||
background: 'var(--panel-bg-secondary)',
|
background: 'var(--panel-bg-secondary)',
|
||||||
@@ -92,6 +94,7 @@ export default function BuildNodeManager() {
|
|||||||
labels: parseJsonArray(node.labels).join(','),
|
labels: parseJsonArray(node.labels).join(','),
|
||||||
maxConcurrency: String(node.maxConcurrency ?? 2),
|
maxConcurrency: String(node.maxConcurrency ?? 2),
|
||||||
workspacePath: node.workspacePath ?? '',
|
workspacePath: node.workspacePath ?? '',
|
||||||
|
diskInfo: node.diskInfo ?? '',
|
||||||
}
|
}
|
||||||
: emptyForm,
|
: emptyForm,
|
||||||
);
|
);
|
||||||
@@ -152,6 +155,7 @@ export default function BuildNodeManager() {
|
|||||||
labels: JSON.stringify(splitComma(form.labels)),
|
labels: JSON.stringify(splitComma(form.labels)),
|
||||||
maxConcurrency: Number(form.maxConcurrency) || 1,
|
maxConcurrency: Number(form.maxConcurrency) || 1,
|
||||||
workspacePath: form.workspacePath.trim(),
|
workspacePath: form.workspacePath.trim(),
|
||||||
|
diskInfo: form.diskInfo.trim(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -292,10 +296,16 @@ export default function BuildNodeManager() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Info: 工作空间 / 心跳(后端暂无 CPU/内存/磁盘数据) */}
|
{/* Info: 磁盘 / 工作空间 / 心跳 */}
|
||||||
<div className="grid grid-cols-2 gap-3 text-[11px]">
|
<div className="grid grid-cols-3 gap-3 text-[11px]">
|
||||||
<div className="flex items-center gap-1.5 min-w-0">
|
<div className="flex items-center gap-1.5 min-w-0">
|
||||||
<span className="flex-shrink-0" style={{ color: 'var(--text-tertiary)' }}>工作空间</span>
|
<HardDrive className="w-3 h-3 flex-shrink-0" style={{ color: 'var(--text-tertiary)' }} />
|
||||||
|
<span className="font-mono truncate" style={{ color: 'var(--text-secondary)' }} title={node.diskInfo ? `磁盘 ${node.diskInfo}` : '磁盘未上报'}>
|
||||||
|
{node.diskInfo || '—'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5 min-w-0">
|
||||||
|
<span className="flex-shrink-0" style={{ color: 'var(--text-tertiary)' }}>空间</span>
|
||||||
<span className="font-mono truncate" style={{ color: 'var(--text-secondary)' }} title={node.workspacePath}>
|
<span className="font-mono truncate" style={{ color: 'var(--text-secondary)' }} title={node.workspacePath}>
|
||||||
{node.workspacePath || '—'}
|
{node.workspacePath || '—'}
|
||||||
</span>
|
</span>
|
||||||
@@ -366,6 +376,10 @@ export default function BuildNodeManager() {
|
|||||||
<Input className="h-8 text-[12px] font-mono" style={inputStyle} value={form.workspacePath} onChange={(e) => setForm({ ...form, workspacePath: e.target.value })} placeholder="/var/lib/flowstack/workspace" />
|
<Input className="h-8 text-[12px] font-mono" style={inputStyle} value={form.workspacePath} onChange={(e) => setForm({ ...form, workspacePath: e.target.value })} placeholder="/var/lib/flowstack/workspace" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label className="text-[12px]" style={{ color: 'var(--text-secondary)' }}>磁盘用量</Label>
|
||||||
|
<Input className="h-8 text-[12px] font-mono" style={inputStyle} value={form.diskInfo} onChange={(e) => setForm({ ...form, diskInfo: e.target.value })} placeholder="如 45G/200G(已用/总量)" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -302,11 +302,41 @@ export default function SystemSettings() {
|
|||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<div><label className="block text-[12px] font-medium mb-1" style={{ color: 'var(--text-secondary)' }}>界面语言</label>
|
<div><label className="block text-[12px] font-medium mb-1" style={{ color: 'var(--text-secondary)' }}>界面语言</label>
|
||||||
<select className="w-full px-3 py-[6px] text-[13px] rounded-md border outline-none" style={{ background: 'var(--panel-bg-secondary)', borderColor: 'var(--border-color-light)', color: 'var(--text-primary)' }}>
|
<select className="w-full px-3 py-[6px] text-[13px] rounded-md border outline-none" style={{ background: 'var(--panel-bg-secondary)', borderColor: 'var(--border-color-light)', color: 'var(--text-primary)' }}>
|
||||||
<option>简体中文</option><option>English</option><option>日本語</option>
|
<option>简体中文</option><option>English</option>
|
||||||
</select></div>
|
</select></div>
|
||||||
<div><label className="block text-[12px] font-medium mb-1" style={{ color: 'var(--text-secondary)' }}>时区</label>
|
<div><label className="block text-[12px] font-medium mb-1" style={{ color: 'var(--text-secondary)' }}>时区</label>
|
||||||
<select className="w-full px-3 py-[6px] text-[13px] rounded-md border outline-none" style={{ background: 'var(--panel-bg-secondary)', borderColor: 'var(--border-color-light)', color: 'var(--text-primary)' }}>
|
<select className="w-full px-3 py-[6px] text-[13px] rounded-md border outline-none" style={{ background: 'var(--panel-bg-secondary)', borderColor: 'var(--border-color-light)', color: 'var(--text-primary)' }}>
|
||||||
<option>Asia/Shanghai (UTC+8)</option><option>UTC</option><option>America/New_York</option>
|
<optgroup label="亚洲">
|
||||||
|
<option>Asia/Shanghai (UTC+8) 中国标准时间</option>
|
||||||
|
<option>Asia/Hong_Kong (UTC+8) 香港时间</option>
|
||||||
|
<option>Asia/Taipei (UTC+8) 台北时间</option>
|
||||||
|
<option>Asia/Singapore (UTC+8) 新加坡时间</option>
|
||||||
|
<option>Asia/Tokyo (UTC+9) 日本时间</option>
|
||||||
|
<option>Asia/Seoul (UTC+9) 韩国时间</option>
|
||||||
|
<option>Asia/Bangkok (UTC+7) 泰国时间</option>
|
||||||
|
<option>Asia/Dubai (UTC+4) 迪拜时间</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="欧洲">
|
||||||
|
<option>Europe/London (UTC+0/+1) 英国时间</option>
|
||||||
|
<option>Europe/Berlin (UTC+1/+2) 中欧时间</option>
|
||||||
|
<option>Europe/Paris (UTC+1/+2) 法国时间</option>
|
||||||
|
<option>Europe/Moscow (UTC+3) 莫斯科时间</option>
|
||||||
|
<option>Europe/Istanbul (UTC+3) 土耳其时间</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="美洲">
|
||||||
|
<option>America/New_York (UTC-5/-4) 美东时间</option>
|
||||||
|
<option>America/Chicago (UTC-6/-5) 美中时间</option>
|
||||||
|
<option>America/Denver (UTC-7/-6) 美山时间</option>
|
||||||
|
<option>America/Los_Angeles (UTC-8/-7) 美西时间</option>
|
||||||
|
<option>America/Sao_Paulo (UTC-3) 巴西时间</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="大洋洲 / 非洲 / 标准">
|
||||||
|
<option>Australia/Sydney (UTC+10/+11) 澳洲东部时间</option>
|
||||||
|
<option>Australia/Perth (UTC+8) 澳洲西部时间</option>
|
||||||
|
<option>Africa/Cairo (UTC+2) 埃及时间</option>
|
||||||
|
<option>Africa/Johannesburg (UTC+2) 南非时间</option>
|
||||||
|
<option>UTC 协调世界时</option>
|
||||||
|
</optgroup>
|
||||||
</select></div>
|
</select></div>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[11px]" style={{ color: 'var(--text-tertiary)' }}>语言与时区暂未生效,仅作展示</p>
|
<p className="text-[11px]" style={{ color: 'var(--text-tertiary)' }}>语言与时区暂未生效,仅作展示</p>
|
||||||
|
|||||||
Reference in new issue
Block a user