feat: 全站 PageShell 设计系统与专业 UI 统一
- 新增 PageShell/PageTabs/ContentPanel/PageLoading/PageEmpty 页面骨架 - index.css 设计令牌文档化,布局令牌与纯色主按钮(去除渐变粉彩) - 全站路由页接入 PageShell,云账单三 Tab 为标杆 - 主题体系:跟随系统 + 主流/开发者分组,新增 Dracula/Tokyo Night,One Dark 暖灰差异化 - 登录页/侧栏/顶栏去渐变光球,企业克制风 - 修复 AIAlertAnalyzer JSX 与构建依赖导入问题
This commit is contained in:
1 parent
1acb0c49af
commit
e363ded445
213 files changed
+7148
-6818
No files matched your search
@@ -44,6 +44,8 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/u
|
||||
import { toast } from 'sonner';
|
||||
import { pipelineApi, deployApi } from '@/api';
|
||||
import { useConfirm } from '@/components/ui/confirm-dialog';
|
||||
import { PageShell } from '@/components/layout/PageShell';
|
||||
import { PageEmpty } from '@/components/layout/PageEmpty';
|
||||
import type { JsonData } from '@/types/common';
|
||||
|
||||
// ---- types ----
|
||||
@@ -76,10 +78,10 @@ function TaskCard({ task, selected, onSelect, onDelete }: { task: Task; selected
|
||||
<div
|
||||
onClick={onSelect}
|
||||
className={`flex items-center gap-3 p-3 rounded-md border cursor-pointer transition-all group ${
|
||||
selected ? 'border-blue-500 bg-blue-500/10' : 'border-[var(--dg-border)] hover:border-slate-400 bg-[var(--dg-bg-secondary)]'
|
||||
selected ? 'border-[var(--dg-blue)] bg-[color-mix(in_srgb,var(--dg-blue)_12%,transparent)]' : 'border-[var(--dg-border)] hover:border-[var(--dg-border-hover)] bg-[var(--dg-bg-secondary)]'
|
||||
}`}
|
||||
>
|
||||
<div className={`p-1.5 rounded ${selected ? 'bg-blue-100' : 'bg-slate-100 dark:bg-slate-800'}`}>
|
||||
<div className={`p-1.5 rounded ${selected ? 'bg-[color-mix(in_srgb,var(--dg-blue)_15%,transparent)]' : 'bg-[var(--dg-bg-tertiary)]'}`}>
|
||||
<Icon className={`w-4 h-4 ${selected ? 'text-blue-600' : 'text-[var(--dg-text-secondary)]'}`} />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
@@ -113,7 +115,7 @@ function TaskEditor({ task, onUpdate }: { task: Task | null; onUpdate: (t: Task)
|
||||
<div className="flex gap-2">{['shell', 'python'].map(t => (<Badge key={t} variant={task.script_type === t ? 'default' : 'outline'} className="cursor-pointer text-xs" onClick={() => onUpdate({ ...task, script_type: t })}>{t}</Badge>))}</div>
|
||||
</div>
|
||||
<div className="space-y-1"><Label className="text-xs">脚本内容</Label>
|
||||
<Textarea value={task.script} onChange={e => onUpdate({ ...task, script: e.target.value })} className="font-mono text-xs min-h-[200px] bg-slate-950 text-green-400" placeholder="#!/bin/bash\nset -e\necho 'hello'" />
|
||||
<Textarea value={task.script} onChange={e => onUpdate({ ...task, script: e.target.value })} className="font-mono text-xs min-h-[200px] bg-[var(--dg-bg-primary)] dg-text-green" placeholder="#!/bin/bash\nset -e\necho 'hello'" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -132,18 +134,18 @@ function LogDialog({ open, onClose, run, logs }: { open: boolean; onClose: () =>
|
||||
<span>耗时: {fmtDuration(run?.duration)}</span>
|
||||
<span>{run?.created_at ? new Date(run.created_at).toLocaleString('zh-CN') : '-'}</span>
|
||||
</div>
|
||||
<div className="bg-slate-950 rounded-md p-3 max-h-[50vh] overflow-y-auto font-mono text-xs">
|
||||
<div className="bg-[var(--dg-bg-primary)] rounded-md p-3 max-h-[50vh] overflow-y-auto font-mono text-xs">
|
||||
{logs.length === 0 ? (
|
||||
<div className="text-slate-500 py-4 text-center">暂无日志</div>
|
||||
<div className="dg-text-muted py-4 text-center">暂无日志</div>
|
||||
) : (
|
||||
logs.map((l, i) => (
|
||||
<div key={i} className="flex gap-2 py-0.5">
|
||||
<span className="text-slate-500 shrink-0">[{l.time}]</span>
|
||||
<span className="dg-text-muted shrink-0">[{l.time}]</span>
|
||||
<span className={
|
||||
l.level === 'error' ? 'text-red-400' :
|
||||
l.level === 'warn' ? 'text-yellow-400' :
|
||||
l.level === 'success' ? 'text-green-400' :
|
||||
'text-slate-300'
|
||||
l.level === 'error' ? 'dg-text-red' :
|
||||
l.level === 'warn' ? 'dg-text-yellow' :
|
||||
l.level === 'success' ? 'dg-text-green' :
|
||||
'text-[var(--dg-text-secondary)]'
|
||||
}>
|
||||
{l.level === 'success' ? '✓' : l.level === 'error' ? '✗' : l.level === 'warn' ? '!' : '>'} {l.message}
|
||||
</span>
|
||||
@@ -323,27 +325,48 @@ export default function PipelineEditor() {
|
||||
|
||||
const envBadge = environments.find(e => String(e.id) === String(environmentId));
|
||||
|
||||
if (loading) return <div className="flex items-center justify-center py-20"><Loader2 className="w-6 h-6 animate-spin" /></div>;
|
||||
if (!pipeline) return <div className="text-center py-20 text-[var(--dg-text-secondary)]">流水线不存在</div>;
|
||||
const editorTabs = [
|
||||
{ id: 'flow', label: '流程配置' },
|
||||
{ id: 'triggers', label: '触发设置' },
|
||||
{ id: 'variables', label: '变量' },
|
||||
{ id: 'runs', label: '发布记录', badge: runs.length || undefined },
|
||||
{ id: 'members', label: '成员管理', badge: members.length || undefined },
|
||||
];
|
||||
|
||||
if (!loading && !pipeline) {
|
||||
return (
|
||||
<PageShell variant="workspace" title="流水线编辑" icon={<GitBranch className="h-5 w-5" />}>
|
||||
<PageEmpty title="流水线不存在" description="请检查链接或返回列表重试" />
|
||||
</PageShell>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold flex items-center gap-2 text-[var(--dg-text-primary)]">
|
||||
<GitBranch className="w-5 h-5 text-blue-500" />
|
||||
{pipeline.name}
|
||||
{envBadge && <Badge style={{ backgroundColor: envBadge.color + '20', color: envBadge.color, borderColor: envBadge.color + '40' }} className="text-xs ml-2">{envBadge.display_name}</Badge>}
|
||||
</h1>
|
||||
<p className="text-sm text-[var(--dg-text-secondary)] mt-1">code: {pipeline.code} · {pipeline.description}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" onClick={handleSave} disabled={saving}><Save className="w-4 h-4 mr-1" />{saving ? '保存中...' : '保存'}</Button>
|
||||
<Button variant="default" onClick={handleTrigger}><Play className="w-4 h-4 mr-1" />运行</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<PageShell
|
||||
variant="workspace"
|
||||
title={pipeline?.name || '流水线编辑'}
|
||||
subtitle={pipeline ? `code: ${pipeline.code} · ${pipeline.description || ''}` : undefined}
|
||||
icon={<GitBranch className="h-5 w-5" />}
|
||||
loading={loading}
|
||||
tabs={editorTabs}
|
||||
activeTab={activeTab}
|
||||
onTabChange={setActiveTab}
|
||||
actions={(
|
||||
<>
|
||||
{envBadge && (
|
||||
<Badge style={{ backgroundColor: envBadge.color + '20', color: envBadge.color, borderColor: envBadge.color + '40' }} className="text-xs">
|
||||
{envBadge.display_name}
|
||||
</Badge>
|
||||
)}
|
||||
<Button variant="outline" className="dg-btn-secondary" onClick={handleSave} disabled={saving}>
|
||||
<Save className="w-4 h-4 mr-1" />{saving ? '保存中...' : '保存'}
|
||||
</Button>
|
||||
<Button className="dg-btn-primary" onClick={handleTrigger}>
|
||||
<Play className="w-4 h-4 mr-1" />运行
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
>
|
||||
{/* Environment Selector - quick bar */}
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<CardContent className="p-3 flex items-center gap-3">
|
||||
@@ -368,14 +391,6 @@ export default function PipelineEditor() {
|
||||
</Card>
|
||||
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList>
|
||||
<TabsTrigger value="flow">流程配置</TabsTrigger>
|
||||
<TabsTrigger value="triggers">触发设置</TabsTrigger>
|
||||
<TabsTrigger value="variables">变量</TabsTrigger>
|
||||
<TabsTrigger value="runs">发布记录 ({runs.length})</TabsTrigger>
|
||||
<TabsTrigger value="members">成员管理 ({members.length})</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* ====== Flow Tab ====== */}
|
||||
<TabsContent value="flow" className="mt-3">
|
||||
<div className="flex gap-4">
|
||||
@@ -433,18 +448,18 @@ export default function PipelineEditor() {
|
||||
<TabsContent value="triggers" className="mt-3">
|
||||
<div className="space-y-4 max-w-lg">
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]"><CardContent className="pt-6 space-y-4">
|
||||
<div className="flex items-center justify-between"><div className="flex items-center gap-2 text-[var(--dg-text-primary)]"><GitBranch className="w-5 h-5 text-blue-500" /><span className="font-medium">Git Push 触发</span></div><Switch checked={triggerMode === 'git_push'} onCheckedChange={v => setTriggerMode(v ? 'git_push' : 'manual')} /></div>
|
||||
<div className="flex items-center justify-between"><div className="flex items-center gap-2 text-[var(--dg-text-primary)]"><GitBranch className="w-5 h-5 dg-text-blue" /><span className="font-medium">Git Push 触发</span></div><Switch checked={triggerMode === 'git_push'} onCheckedChange={v => setTriggerMode(v ? 'git_push' : 'manual')} /></div>
|
||||
{triggerMode === 'git_push' && <div className="space-y-1"><Label>监听分支</Label><Input value={watchBranches} onChange={e => setWatchBranches(e.target.value)} placeholder="master, release/*" className="h-9" /></div>}
|
||||
</CardContent></Card>
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]"><CardContent className="pt-6 space-y-4">
|
||||
<div className="flex items-center justify-between"><div className="flex items-center gap-2 text-[var(--dg-text-primary)]"><Clock className="w-5 h-5 text-blue-500" /><span className="font-medium">定时触发</span></div><Switch checked={triggerMode === 'cron'} onCheckedChange={v => setTriggerMode(v ? 'cron' : 'manual')} /></div>
|
||||
<div className="flex items-center justify-between"><div className="flex items-center gap-2 text-[var(--dg-text-primary)]"><Clock className="w-5 h-5 dg-text-blue" /><span className="font-medium">定时触发</span></div><Switch checked={triggerMode === 'cron'} onCheckedChange={v => setTriggerMode(v ? 'cron' : 'manual')} /></div>
|
||||
{triggerMode === 'cron' && <div className="space-y-1"><Label>Cron 表达式</Label><Input value={cronExpr} onChange={e => setCronExpr(e.target.value)} placeholder="0 2 * * *" className="h-9 font-mono" /></div>}
|
||||
</CardContent></Card>
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]"><CardContent className="pt-6 space-y-4">
|
||||
<div className="flex items-center justify-between"><div className="flex items-center gap-2 text-[var(--dg-text-primary)]"><Webhook className="w-5 h-5 text-blue-500" /><span className="font-medium">Webhook 触发</span></div><Switch checked={triggerMode === 'webhook'} onCheckedChange={v => setTriggerMode(v ? 'webhook' : 'manual')} /></div>
|
||||
<div className="flex items-center justify-between"><div className="flex items-center gap-2 text-[var(--dg-text-primary)]"><Webhook className="w-5 h-5 dg-text-blue" /><span className="font-medium">Webhook 触发</span></div><Switch checked={triggerMode === 'webhook'} onCheckedChange={v => setTriggerMode(v ? 'webhook' : 'manual')} /></div>
|
||||
{triggerMode === 'webhook' && <div className="space-y-1"><Label>Secret Token</Label><Input value={webhookToken} onChange={e => setWebhookToken(e.target.value)} type="password" placeholder="输入签名密钥" className="h-9" /></div>}
|
||||
</CardContent></Card>
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]"><CardContent className="pt-6"><div className="flex items-center gap-2 text-[var(--dg-text-primary)]"><Hand className="w-5 h-5 text-blue-500" /><span className="font-medium">手动触发</span><Badge variant="secondary" className="text-xs">默认启用</Badge></div></CardContent></Card>
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]"><CardContent className="pt-6"><div className="flex items-center gap-2 text-[var(--dg-text-primary)]"><Hand className="w-5 h-5 dg-text-blue" /><span className="font-medium">手动触发</span><Badge variant="secondary" className="text-xs">默认启用</Badge></div></CardContent></Card>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
@@ -487,10 +502,10 @@ export default function PipelineEditor() {
|
||||
<tr key={r.id} className="border-b border-[var(--dg-border)] hover:bg-[var(--dg-bg-tertiary)]/50">
|
||||
<td className="px-3 py-2 font-medium text-[var(--dg-text-primary)]">#{r.build_number || r.id}</td>
|
||||
<td className="px-3 py-2">
|
||||
{r.status === 'success' ? <Badge className="bg-emerald-500/20 text-emerald-400 border-emerald-500/30"><Check className="w-3 h-3 mr-0.5" />成功</Badge> :
|
||||
r.status === 'failed' ? <Badge className="bg-red-500/20 text-red-400 border-red-500/30"><X className="w-3 h-3 mr-0.5" />失败</Badge> :
|
||||
r.status === 'running' ? <Badge className="bg-blue-500/20 text-blue-400 border-blue-500/30"><Loader2 className="w-3 h-3 animate-spin mr-0.5" />执行中</Badge> :
|
||||
r.status === 'cancelled' ? <Badge className="bg-yellow-500/20 text-yellow-400 border-yellow-500/30"><StopCircle className="w-3 h-3 mr-0.5" />已停止</Badge> :
|
||||
{r.status === 'success' ? <Badge className="bg-emerald-500/20 dg-text-green border-emerald-500/30"><Check className="w-3 h-3 mr-0.5" />成功</Badge> :
|
||||
r.status === 'failed' ? <Badge className="bg-red-500/20 dg-text-red border-red-500/30"><X className="w-3 h-3 mr-0.5" />失败</Badge> :
|
||||
r.status === 'running' ? <Badge className="bg-blue-500/20 dg-text-blue border-blue-500/30"><Loader2 className="w-3 h-3 animate-spin mr-0.5" />执行中</Badge> :
|
||||
r.status === 'cancelled' ? <Badge className="bg-yellow-500/20 dg-text-yellow border-yellow-500/30"><StopCircle className="w-3 h-3 mr-0.5" />已停止</Badge> :
|
||||
<Badge variant="secondary" className="text-[var(--dg-text-secondary)]">{r.status}</Badge>}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-[var(--dg-text-secondary)] text-xs">
|
||||
@@ -506,9 +521,9 @@ export default function PipelineEditor() {
|
||||
<td className="px-3 py-2 text-[var(--dg-text-secondary)] text-xs">{r.created_at ? new Date(r.created_at).toLocaleString('zh-CN') : '-'}</td>
|
||||
<td className="px-3 py-2 text-right">
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
<Button variant="ghost" size="sm" className="h-7 text-xs text-blue-400 hover:text-blue-300" onClick={() => handleViewLog(r)}><FileText className="w-3.5 h-3.5 mr-1" />日志</Button>
|
||||
<Button variant="ghost" size="sm" className="h-7 text-xs dg-text-blue hover:text-blue-300" onClick={() => handleViewLog(r)}><FileText className="w-3.5 h-3.5 mr-1" />日志</Button>
|
||||
{(r.status === 'running' || r.status === 'pending') && (
|
||||
<Button variant="ghost" size="sm" className="h-7 text-xs text-red-400 hover:text-red-300" onClick={() => handleStop(r.id)}><StopCircle className="w-3.5 h-3.5 mr-1" />停止</Button>
|
||||
<Button variant="ghost" size="sm" className="h-7 text-xs dg-text-red hover:dg-text-red" onClick={() => handleStop(r.id)}><StopCircle className="w-3.5 h-3.5 mr-1" />停止</Button>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
@@ -527,7 +542,7 @@ export default function PipelineEditor() {
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<UserPlus className="w-4 h-4 text-blue-500" />
|
||||
<UserPlus className="w-4 h-4 dg-text-blue" />
|
||||
<span className="text-sm font-medium text-[var(--dg-text-primary)]">添加用户</span>
|
||||
</div>
|
||||
<div className="flex items-end gap-3 mt-3">
|
||||
@@ -572,13 +587,13 @@ export default function PipelineEditor() {
|
||||
{directUsers.map((m: JsonData) => (
|
||||
<div key={m.id} className="flex items-center justify-between px-4 py-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-full bg-blue-500/20 flex items-center justify-center"><Users className="w-4 h-4 text-blue-400" /></div>
|
||||
<div className="w-8 h-8 rounded-full bg-blue-500/20 flex items-center justify-center"><Users className="w-4 h-4 dg-text-blue" /></div>
|
||||
<div>
|
||||
<div className="text-sm font-medium text-[var(--dg-text-primary)]">{m.username}</div>
|
||||
<div className="text-xs text-[var(--dg-text-secondary)]">{m.role === 'owner' ? <span className="text-amber-400">拥有者</span> : m.role === 'editor' ? <span className="text-blue-400">编辑者</span> : <span className="text-slate-400">查看者</span>}</div>
|
||||
<div className="text-xs text-[var(--dg-text-secondary)]">{m.role === 'owner' ? <span className="dg-text-yellow">拥有者</span> : m.role === 'editor' ? <span className="dg-text-blue">编辑者</span> : <span className="dg-text-secondary">查看者</span>}</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" className="h-7 text-xs text-red-400 hover:text-red-300" onClick={() => handleRemoveMember(m.id)}><Trash2 className="w-3.5 h-3.5 mr-1" />移除</Button>
|
||||
<Button variant="ghost" size="sm" className="h-7 text-xs dg-text-red hover:dg-text-red" onClick={() => handleRemoveMember(m.id)}><Trash2 className="w-3.5 h-3.5 mr-1" />移除</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -594,20 +609,20 @@ export default function PipelineEditor() {
|
||||
</div>
|
||||
{groupAssignments.length === 0 ? (
|
||||
<div className="py-4 text-center text-[var(--dg-text-secondary)] text-xs">
|
||||
暂无组授权 — 在「<a href="/deploy/groups" className="text-blue-400 underline">用户组管理</a>」页面创建组后,可在此授权
|
||||
暂无组授权 — 在「<a href="/deploy/groups" className="dg-text-blue underline">用户组管理</a>」页面创建组后,可在此授权
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y divide-[var(--dg-border)]">
|
||||
{groupAssignments.map((m: JsonData) => (
|
||||
<div key={m.id} className="flex items-center justify-between px-4 py-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-full bg-purple-500/20 flex items-center justify-center"><Shield className="w-4 h-4 text-purple-400" /></div>
|
||||
<div className="w-8 h-8 rounded-full bg-purple-500/20 flex items-center justify-center"><Shield className="w-4 h-4 dg-text-purple" /></div>
|
||||
<div>
|
||||
<div className="text-sm font-medium text-[var(--dg-text-primary)]">{m.group?.name || `组 #${m.group_id}`}</div>
|
||||
<div className="text-xs text-[var(--dg-text-secondary)]">{m.role === 'owner' ? <span className="text-amber-400">拥有者</span> : m.role === 'editor' ? <span className="text-blue-400">编辑者</span> : <span className="text-slate-400">查看者</span>} · 组内全体成员</div>
|
||||
<div className="text-xs text-[var(--dg-text-secondary)]">{m.role === 'owner' ? <span className="dg-text-yellow">拥有者</span> : m.role === 'editor' ? <span className="dg-text-blue">编辑者</span> : <span className="dg-text-secondary">查看者</span>} · 组内全体成员</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" className="h-7 text-xs text-red-400 hover:text-red-300" onClick={() => handleRemoveMember(m.id)}><Trash2 className="w-3.5 h-3.5 mr-1" />撤销授权</Button>
|
||||
<Button variant="ghost" size="sm" className="h-7 text-xs dg-text-red hover:dg-text-red" onClick={() => handleRemoveMember(m.id)}><Trash2 className="w-3.5 h-3.5 mr-1" />撤销授权</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -623,6 +638,6 @@ export default function PipelineEditor() {
|
||||
|
||||
{/* Log Dialog */}
|
||||
<LogDialog open={logOpen} onClose={() => setLogOpen(false)} run={logRun} logs={logs} />
|
||||
</div>
|
||||
</PageShell>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user