feat: 全站 PageShell 设计系统与专业 UI 统一

- 新增 PageShell/PageTabs/ContentPanel/PageLoading/PageEmpty 页面骨架
- index.css 设计令牌文档化,布局令牌与纯色主按钮(去除渐变粉彩)
- 全站路由页接入 PageShell,云账单三 Tab 为标杆
- 主题体系:跟随系统 + 主流/开发者分组,新增 Dracula/Tokyo Night,One Dark 暖灰差异化
- 登录页/侧栏/顶栏去渐变光球,企业克制风
- 修复 AIAlertAnalyzer JSX 与构建依赖导入问题
This commit is contained in:
Logan committed 2026-07-01 12:45:38 +08:00
1 parent 1acb0c49af
commit e363ded445
213 files changed
+7148 -6818

No files matched your search

@@ -12,6 +12,8 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { toast } from 'sonner';
import { pipelineApi } from '@/api';
import { FileBrowserDialog } from './FileBrowserDialog';
import { PageShell } from '@/components/layout/PageShell';
import { StatStrip } from '@/components/ui/stat-strip';
import type { JsonData } from '@/types/common';
// ═══════════ 阶段定义 (与 PipelineManager 共用) ═══════════
@@ -55,11 +57,11 @@ interface StageStatus {
}
const STATUS_BADGES: Record<string, string> = {
success: 'bg-emerald-500/20 text-emerald-400 border-emerald-500/30',
failed: 'bg-red-500/20 text-red-400 border-red-500/30',
running: 'bg-blue-500/20 text-blue-400 border-blue-500/30',
pending: 'bg-slate-500/20 text-slate-400 border-slate-500/30',
cancelled: 'bg-amber-500/20 text-amber-400 border-amber-500/30',
success: 'bg-emerald-500/20 dg-text-green border-emerald-500/30',
failed: 'bg-red-500/20 dg-text-red border-red-500/30',
running: 'bg-blue-500/20 dg-text-blue border-blue-500/30',
pending: 'bg-[var(--dg-bg-active)]/20 dg-text-secondary border-[var(--dg-border)]',
cancelled: 'bg-amber-500/20 dg-text-yellow border-amber-500/30',
};
const STATUS_ICONS: JsonData = {
@@ -290,37 +292,35 @@ export default function PipelineRunDetail() {
// 获取当前选中阶段的摘要日志
const stageSummary = selectedStage ? getStageLogSummary(logs, selectedStage) : '';
const successCount = runs.filter(r => r.status === 'success').length;
const runningCount = runs.filter(r => r.status === 'running').length;
return (
<div className="space-y-4 h-full flex flex-col">
{/* ═══════════ 页面头部 ═══════════ */}
<div className="flex items-center justify-between">
<div>
<h2 className="text-lg font-semibold text-[var(--dg-text-primary)]">
<History className="w-5 h-5 inline mr-2" />
流水线详情
</h2>
<div className="flex items-center gap-2 mt-0.5 flex-wrap">
{pipeline?.name && (
<span className="text-sm font-medium text-[var(--dg-text-primary)]">
{pipeline.name}
</span>
)}
{pipeline?.environment && (
<Badge className="text-xs" style={{
backgroundColor: pipeline.environment.color + '20',
color: pipeline.environment.color,
}}>
{pipeline.environment.display_name}
</Badge>
)}
{selectedRun && (
<span className="text-sm text-[var(--dg-text-secondary)]">
发布 #{selectedRun.build_number}
</span>
)}
</div>
</div>
<div className="flex gap-2 items-center">
<PageShell
variant="workspace"
title={pipeline?.name || '流水线详情'}
subtitle={selectedRun ? `发布 #${selectedRun.build_number}` : (pipeline?.code ? `code: ${pipeline.code}` : '执行记录与日志')}
icon={<History className="h-5 w-5" />}
contentClassName="space-y-4 h-full flex flex-col"
metrics={(
<StatStrip
items={[
{ label: '执行记录', value: runs.length, color: 'blue' },
{ label: '成功', value: successCount, color: 'emerald' },
{ label: '执行中', value: runningCount, color: 'amber' },
]}
/>
)}
actions={(
<>
{pipeline?.environment && (
<Badge className="text-xs" style={{
backgroundColor: pipeline.environment.color + '20',
color: pipeline.environment.color,
}}>
{pipeline.environment.display_name}
</Badge>
)}
{selectedIds.size > 0 && (
<Button variant="destructive" size="sm" onClick={batchDelete} disabled={deleting}>
<Trash2 className="w-4 h-4 mr-1" />删除 ({selectedIds.size})
@@ -329,21 +329,21 @@ export default function PipelineRunDetail() {
<select
value={pageSize}
onChange={e => setPageSize(Number(e.target.value))}
className="h-8 px-2 text-xs rounded border border-[var(--dg-border)] bg-[var(--dg-bg-secondary)] text-[var(--dg-text-primary)]"
className="h-8 px-2 text-xs rounded border border-[var(--dg-border)] dg-input"
>
<option value={10}>10条/页</option>
<option value={20}>20条/页</option>
<option value={30}>30条/页</option>
</select>
<Button variant="outline" size="sm" onClick={fetchRuns}>
<Button variant="outline" size="sm" className="dg-btn-secondary" onClick={fetchRuns}>
<RefreshCw className="w-4 h-4 mr-1" />刷新
</Button>
<Button size="sm" onClick={triggerRun}>
<Button size="sm" className="dg-btn-primary" onClick={triggerRun}>
<Play className="w-4 h-4 mr-1" />立即执行
</Button>
</div>
</div>
</>
)}
>
{/* ═══════════ 阶段进度条 (交互式) ═══════════ */}
<div className="flex items-center gap-0 px-4 py-3 rounded-lg bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] overflow-x-auto">
{STAGE_DEFS.map((def, i) => {
@@ -473,7 +473,7 @@ export default function PipelineRunDetail() {
</td>
<td className="px-3 py-2.5 text-[var(--dg-text-secondary)] font-mono text-xs">
{run.status === 'running' ? (
<span className="flex items-center gap-1 text-blue-400">
<span className="flex items-center gap-1 dg-text-blue">
<Loader2 className="w-3 h-3 animate-spin" />执行中...
</span>
) : formatDuration(run.duration)}
@@ -516,7 +516,7 @@ export default function PipelineRunDetail() {
{pipeline?.name || `#${pipelineCode}`} #{selectedRun.build_number}
</CardTitle>
{selectedRun.status === 'running' && (
<Badge className="bg-blue-500/20 text-blue-400 border-blue-500/30 text-xs">
<Badge className="bg-blue-500/20 dg-text-blue border-blue-500/30 text-xs">
<Loader2 className="w-3 h-3 animate-spin mr-1" />实时
</Badge>
)}
@@ -533,7 +533,7 @@ export default function PipelineRunDetail() {
{selectedRun.commit_id && (
<>
<span className="opacity-40">|</span>
<code className="bg-amber-400/10 text-amber-400 px-1 py-0.5 rounded text-[10px]">
<code className="bg-amber-400/10 dg-text-yellow px-1 py-0.5 rounded text-[10px]">
{selectedRun.commit_id.slice(0, 8)}
</code>
</>
@@ -541,7 +541,7 @@ export default function PipelineRunDetail() {
{selectedRun.image_tag && (
<>
<span className="opacity-40">|</span>
<code className="bg-cyan-400/10 text-cyan-400 px-1 py-0.5 rounded text-[10px]">
<code className="bg-cyan-400/10 dg-text-cyan px-1 py-0.5 rounded text-[10px]">
📦 {selectedRun.image_tag}
</code>
</>
@@ -558,9 +558,9 @@ export default function PipelineRunDetail() {
</span>
{(() => {
const st = stageStats.get(selectedStage);
if (st === 'success' || st === 'completed') return <CheckCircle2 className="h-3.5 w-3.5 text-emerald-400" />;
if (st === 'failed' || st === 'error') return <XCircle className="h-3.5 w-3.5 text-red-400" />;
if (st === 'running' || st === 'in_progress') return <Loader2 className="h-3.5 w-3.5 text-blue-400 animate-spin" />;
if (st === 'success' || st === 'completed') return <CheckCircle2 className="h-3.5 w-3.5 dg-text-green" />;
if (st === 'failed' || st === 'error') return <XCircle className="h-3.5 w-3.5 dg-text-red" />;
if (st === 'running' || st === 'in_progress') return <Loader2 className="h-3.5 w-3.5 dg-text-blue animate-spin" />;
return null;
})()}
<span className="text-xs text-[var(--dg-text-muted)] truncate font-mono">
@@ -571,7 +571,7 @@ export default function PipelineRunDetail() {
)}
<div className="h-full overflow-y-auto bg-[#0d1117] font-mono text-xs leading-5 p-4" style={{ minHeight: 280 }}>
{filteredLogs.length === 0 && selectedRun.status !== 'running' ? (
<div className="text-center py-12 text-slate-500">
<div className="text-center py-12 dg-text-muted">
{selectedStage ? '该阶段暂无日志输出' : '暂无日志输出'}
</div>
) : (
@@ -601,25 +601,25 @@ export default function PipelineRunDetail() {
: isSuccess ? 'border-l-2 border-emerald-500 bg-emerald-500/5' : '';
const msgColor = isError ? 'text-red-200'
: isWarn ? 'text-amber-200'
: isSuccess ? 'text-emerald-200' : 'text-slate-300';
: isSuccess ? 'text-emerald-200' : 'text-[var(--dg-text-secondary)]';
const levelBadge = isError
? <span className="shrink-0 px-1.5 py-px rounded text-[10px] font-bold bg-red-600/40 text-red-200">ERR</span>
: isWarn
? <span className="shrink-0 px-1.5 py-px rounded text-[10px] font-bold bg-amber-600/40 text-amber-200">WARN</span>
: isSuccess
? <span className="shrink-0 px-1.5 py-px rounded text-[10px] font-bold bg-emerald-600/30 text-emerald-200"> OK </span>
: <span className="shrink-0 px-1.5 py-px rounded text-[10px] bg-slate-600/30 text-slate-400">INFO</span>;
: <span className="shrink-0 px-1.5 py-px rounded text-[10px] bg-[var(--dg-bg-active)]/30 dg-text-secondary">INFO</span>;
return (
<React.Fragment key={i}>
{showStage && (
<div className={`flex items-center gap-2 my-2 py-1 px-2 border-l-4 rounded-r ${stageStyles[entry.stage || ''] || 'border-slate-500 bg-slate-500/10'}`}>
<div className={`flex items-center gap-2 my-2 py-1 px-2 border-l-4 rounded-r ${stageStyles[entry.stage || ''] || 'border-[var(--dg-border)] bg-[var(--dg-bg-active)]/10'}`}>
<span className="text-xs">{stageIcons[entry.stage || ''] || '▸'}</span>
<span className="text-[11px] font-semibold tracking-wider text-slate-200">{entry.stage}</span>
<span className="flex-1 border-t border-slate-700/50"></span>
<span className="text-[11px] font-semibold tracking-wider text-[var(--dg-text-primary)]">{entry.stage}</span>
<span className="flex-1 border-t border-[var(--dg-border)]/50"></span>
</div>
)}
<div className={`flex items-start gap-2 px-2 py-0.5 -mx-1 rounded-sm ${accent} ${!isError && !isWarn && !isSuccess ? 'hover:bg-white/[0.03]' : ''}`}>
<span className="text-slate-600 shrink-0 w-14 text-right text-[10px] pt-px">{entry.time}</span>
<span className="dg-text-muted shrink-0 w-14 text-right text-[10px] pt-px">{entry.time}</span>
{levelBadge}
<span className={`break-all ${msgColor}`}>{entry.message}</span>
</div>
@@ -629,7 +629,7 @@ export default function PipelineRunDetail() {
})()}
{selectedRun.status === 'running' && (
<div className="flex items-center gap-2 mt-2 py-1.5 px-3 bg-blue-500/10 border-l-4 border-blue-500 rounded-r">
<Loader2 className="w-3 h-3 animate-spin text-blue-400 shrink-0" />
<Loader2 className="w-3 h-3 animate-spin dg-text-blue shrink-0" />
<span className="text-[11px] text-blue-300 font-medium">流水线执行中,实时输出...</span>
<span className="flex gap-1 ml-1">
<span className="w-1 h-1 rounded-full bg-blue-400 animate-bounce" style={{ animationDelay: '0ms' }}></span>
@@ -654,6 +654,6 @@ export default function PipelineRunDetail() {
runId={browserRun?.id ?? null}
buildNumber={browserRun?.buildNumber ?? 0}
/>
</div>
</PageShell>
);
}