feat: 设置语言精简中英+补全时区;构建节点磁盘显示;浏览器 favicon

- 系统设置:界面语言仅保留 简体中文/English;时区按 亚/欧/美/大洋洲
  分组补全 22 个常用时区
- 构建节点:BuildNode 新增 diskInfo 字段(模型/CRUD/seed),卡片显示
  磁盘用量(未上报显示 —),添加/编辑弹窗可维护
- 新增 favicon.svg(主题渐变火箭图标),index.html 注入 icon 链接
This commit is contained in:
Logan.Li committed 2026-10-27 09:16:42 +08:00
1 parent 581d0a8351
commit a3b4637e19
7 files changed
+71 -10

No files matched your search

+3 -3
View File
@@ -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)
+1 -1
View File
@@ -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)
} }
+1
View File
@@ -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"`
+1
View File
@@ -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>
+15
View File
@@ -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

+18 -4
View File
@@ -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
+32 -2
View File
@@ -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>