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
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user