fix(i18n): 回退到稳定的 32% 覆盖率状态 (tsc + Vite 构建通过)
批量转换脚本引入的语法错误已回退,保留稳定的 i18n 转换: - 翻译词典: 22 namespaces, 3600+ keys - 已接入: 145+ 文件 (Dashboard/Login/K8s 主页面/Monitor/System/Deploy) - 语言切换器在 Header 中可用 (🇨🇳/🇺🇸) - tsc 零错误, Vite 构建成功
This commit is contained in:
1 parent
ecec73989b
commit
72bfb0e12d
191 files changed
+5057
-7592
No files matched your search
@@ -1,5 +1,6 @@
|
||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import {
|
||||
Play, Square, RefreshCw, Clock, User, AlertCircle, CheckCircle2,
|
||||
@@ -15,17 +16,15 @@ import { FileBrowserDialog } from './FileBrowserDialog';
|
||||
import { PageShell } from '@/components/layout/PageShell';
|
||||
import { StatStrip } from '@/components/ui/stat-strip';
|
||||
import type { JsonData } from '@/types/common';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import i18n from '@/i18n';
|
||||
|
||||
// ═══════════ 阶段定义 (与 PipelineManager 共用) ═══════════
|
||||
const STAGE_DEFS = [
|
||||
{ key: 'checkout', label: i18n.t('pipeline.checkout'), icon: GitBranch, color: '#6366F1', order: 1 },
|
||||
{ key: 'build', label: i18n.t('pipeline.build'), icon: Box, color: '#F59E0B', order: 2 },
|
||||
{ key: 'test', label: i18n.t('common.test'), icon: CheckCircle2,color: '#10B981', order: 3 },
|
||||
{ key: 'image', label: i18n.t('deploy.stage.image'), icon: Container, color: '#06B6D4', order: 4 },
|
||||
{ key: 'deploy', label: i18n.t('pipeline.deploy'), icon: Rocket, color: '#EF4444', order: 5 },
|
||||
{ key: 'verify', label: i18n.t('common.validate'), icon: ShieldCheck, color: '#3B82F6', order: 6 },
|
||||
{ key: 'checkout', label: '代码检出', icon: GitBranch, color: '#6366F1', order: 1 },
|
||||
{ key: 'build', label: '构建', icon: Box, color: '#F59E0B', order: 2 },
|
||||
{ key: 'test', label: '测试', icon: CheckCircle2,color: '#10B981', order: 3 },
|
||||
{ key: 'image', label: '镜像打包', icon: Container, color: '#06B6D4', order: 4 },
|
||||
{ key: 'deploy', label: '部署', icon: Rocket, color: '#EF4444', order: 5 },
|
||||
{ key: 'verify', label: '验证', icon: ShieldCheck, color: '#3B82F6', order: 6 },
|
||||
].sort((a, b) => a.order - b.order);
|
||||
|
||||
interface PipelineRun {
|
||||
@@ -76,10 +75,10 @@ const STATUS_ICONS: JsonData = {
|
||||
|
||||
function formatDuration(seconds: number): string {
|
||||
if (!seconds) return '-';
|
||||
if (seconds < 60) return i18n.t('deploy.pipelinerundetail.sbe383c', { seconds: seconds });
|
||||
if (seconds < 60) return `${seconds}秒`;
|
||||
const m = Math.floor(seconds / 60);
|
||||
const s = seconds % 60;
|
||||
return i18n.t('deploy.pipelinerundetail.s084409', { m: m, s: s });
|
||||
return `${m}分${s}秒`;
|
||||
}
|
||||
|
||||
function formatTime(ts: string): string {
|
||||
@@ -134,6 +133,19 @@ function getStageLogSummary(logs: LogEntry[], stageKey: string): string {
|
||||
export default function PipelineRunDetail() {
|
||||
const { t } = useTranslation();
|
||||
const { code: pipelineCode } = useParams<{ code: string }>();
|
||||
|
||||
// 阶段名称国际化映射
|
||||
const stageLabel = (key: string) => {
|
||||
const map: Record<string, string> = {
|
||||
checkout: t('deploy.stage.checkout'),
|
||||
build: t('deploy.stage.build'),
|
||||
test: t('deploy.stage.test'),
|
||||
image: t('deploy.stage.image'),
|
||||
deploy: t('deploy.stage.deploy'),
|
||||
verify: t('common.verify'),
|
||||
};
|
||||
return map[key] || key;
|
||||
};
|
||||
const [pipeline, setPipeline] = useState<JsonData>(null);
|
||||
const [runs, setRuns] = useState<PipelineRun[]>([]);
|
||||
const [selectedRun, setSelectedRun] = useState<PipelineRun | null>(null);
|
||||
@@ -168,7 +180,7 @@ export default function PipelineRunDetail() {
|
||||
if (!selectedRun && list.length > 0) {
|
||||
setSelectedRun(list[0]);
|
||||
}
|
||||
} catch { toast.error(t('deploy.pipelinerundetail.sa4e14e')); }
|
||||
} catch { toast.error(t('deploy.runDetail.fetchRunsFailed')); }
|
||||
finally { setLoading(false); }
|
||||
}, [pipelineCode, pageSize]);
|
||||
|
||||
@@ -239,18 +251,18 @@ export default function PipelineRunDetail() {
|
||||
const triggerRun = async () => {
|
||||
try {
|
||||
await pipelineApi.triggerByCode(pipelineCode);
|
||||
toast.success(t('automation.triggered'));
|
||||
toast.success(t('deploy.toast.triggered'));
|
||||
fetchRuns();
|
||||
} catch { toast.error(t('automation.triggerFailed')); }
|
||||
} catch { toast.error(t('deploy.toast.triggerFailed')); }
|
||||
};
|
||||
|
||||
// 停止
|
||||
const stopRun = async (runId: number) => {
|
||||
try {
|
||||
await pipelineApi.stopRunByCode(pipelineCode, runId);
|
||||
toast.success(t('common.stopped'));
|
||||
toast.success(t('deploy.toast.stopped'));
|
||||
fetchRuns();
|
||||
} catch { toast.error(t('pipeline.stopFailed')); }
|
||||
} catch { toast.error(t('deploy.toast.stopFailed')); }
|
||||
};
|
||||
|
||||
// 批量删除
|
||||
@@ -259,10 +271,10 @@ export default function PipelineRunDetail() {
|
||||
setDeleting(true);
|
||||
try {
|
||||
await pipelineApi.batchDeleteRuns(pipelineCode, Array.from(selectedIds));
|
||||
toast.success(t('deploy.pipelinerundetail.s4b340c', { selectedIds.size: selectedIds.size }));
|
||||
toast.success(t('deploy.runDetail.deletedCount', { count: selectedIds.size }));
|
||||
setSelectedIds(new Set());
|
||||
fetchRuns();
|
||||
} catch { toast.error(t('cloud.batchDeleteFailed')); }
|
||||
} catch { toast.error(t('deploy.toast.batchDeleteFailed')); }
|
||||
finally { setDeleting(false); }
|
||||
};
|
||||
|
||||
@@ -302,15 +314,15 @@ export default function PipelineRunDetail() {
|
||||
<PageShell
|
||||
variant="workspace"
|
||||
title={pipeline?.name || t('deploy.pipeline.detail')}
|
||||
subtitle={selectedRun ? t('deploy.pipelinerundetail.se9940a', { selectedRun.build_number: selectedRun.build_number }) : (pipeline?.code ? `code: ${pipeline.code}` : t('deploy.pipelinerundetail.sefc52b'))}
|
||||
subtitle={selectedRun ? t('deploy.pipeline.release', { number: selectedRun.build_number }) : (pipeline?.code ? `code: ${pipeline.code}` : t('deploy.runDetail.recordsAndLogs'))}
|
||||
icon={<History className="h-5 w-5" />}
|
||||
contentClassName="space-y-4 h-full flex flex-col"
|
||||
metrics={(
|
||||
<StatStrip
|
||||
items={[
|
||||
{ label: t('deploy.pipelinerundetail.s9b409b'), value: runs.length, color: 'blue' },
|
||||
{ label: t('common.success'), value: successCount, color: 'emerald' },
|
||||
{ label: t('common.executing'), value: runningCount, color: 'amber' },
|
||||
{ label: t('deploy.runDetail.execRecords'), value: runs.length, color: 'blue' },
|
||||
{ label: t('deploy.status.success'), value: successCount, color: 'emerald' },
|
||||
{ label: t('deploy.runDetail.executing'), value: runningCount, color: 'amber' },
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
@@ -326,7 +338,7 @@ export default function PipelineRunDetail() {
|
||||
)}
|
||||
{selectedIds.size > 0 && (
|
||||
<Button variant="destructive" size="sm" onClick={batchDelete} disabled={deleting}>
|
||||
<Trash2 className="w-4 h-4 mr-1" />删除 ({selectedIds.size})
|
||||
<Trash2 className="w-4 h-4 mr-1" />{t('common.delete')} ({selectedIds.size})
|
||||
</Button>
|
||||
)}
|
||||
<select
|
||||
@@ -334,15 +346,15 @@ export default function PipelineRunDetail() {
|
||||
onChange={e => setPageSize(Number(e.target.value))}
|
||||
className="h-8 px-2 text-xs rounded border border-[var(--dg-border)] dg-input"
|
||||
>
|
||||
<option value={10}>{t('deploy.pipelinerundetail.s8776a1')}</option>
|
||||
<option value={20}>{t('deploy.pipelinerundetail.s96dbf3')}</option>
|
||||
<option value={30}>{t('deploy.pipelinerundetail.s074a81')}</option>
|
||||
<option value={10}>10{t('deploy.runDetail.perPage')}</option>
|
||||
<option value={20}>20{t('deploy.runDetail.perPage')}</option>
|
||||
<option value={30}>30{t('deploy.runDetail.perPage')}</option>
|
||||
</select>
|
||||
<Button variant="outline" size="sm" className="dg-btn-secondary" onClick={fetchRuns}>
|
||||
<RefreshCw className="w-4 h-4 mr-1" />刷新
|
||||
<RefreshCw className="w-4 h-4 mr-1" />{t('common.refresh')}
|
||||
</Button>
|
||||
<Button size="sm" className="dg-btn-primary" onClick={triggerRun}>
|
||||
<Play className="w-4 h-4 mr-1" />立即执行
|
||||
<Play className="w-4 h-4 mr-1" />{t('deploy.pipeline.runNow')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
@@ -383,7 +395,7 @@ export default function PipelineRunDetail() {
|
||||
<div key={def.key} className="flex items-center">
|
||||
<button
|
||||
onClick={() => setSelectedStage(selectedStage === def.key ? null : def.key)}
|
||||
title={`${def.label}${status ? ` (${status})t('deploy.pipelinerundetail.sdaf2bb')}
|
||||
title={`${stageLabel(def.key)}${status ? ` (${status})` : ''} — ${t('deploy.runDetail.clickToViewLog')}`}
|
||||
className={`flex items-center gap-1.5 px-3 py-2 text-[12px] font-semibold border shadow-sm transition-all cursor-pointer
|
||||
${isActive ? 'ring-1 ring-offset-0 scale-[1.02]' : 'hover:brightness-110'}`}
|
||||
style={{
|
||||
@@ -394,7 +406,7 @@ export default function PipelineRunDetail() {
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
<span>{def.label}</span>
|
||||
<span>{stageLabel(def.key)}</span>
|
||||
</button>
|
||||
{!isLast && (
|
||||
<div className="flex items-center justify-center w-5 h-5 -mx-px z-10">
|
||||
@@ -406,7 +418,7 @@ export default function PipelineRunDetail() {
|
||||
})}
|
||||
{selectedRun && (
|
||||
<span className="ml-auto text-[10px] text-[var(--dg-text-secondary)] opacity-40 shrink-0 pl-3 whitespace-nowrap">
|
||||
{selectedStage ? STAGE_DEFS.find(d => d.key === selectedStage)?.label : t('pipeline.allStages')}
|
||||
{selectedStage ? stageLabel(selectedStage) : t('deploy.pipeline.allStages')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -418,7 +430,7 @@ export default function PipelineRunDetail() {
|
||||
{loading && runs.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<Loader2 className="w-6 h-6 animate-spin mx-auto mb-2 text-[var(--dg-text-muted)]" />
|
||||
加载中...
|
||||
{t('common.loading')}
|
||||
</div>
|
||||
) : runs.length === 0 ? (
|
||||
<div className="text-center py-12 border border-dashed border-[var(--dg-border)] rounded-lg m-4">
|
||||
@@ -433,12 +445,12 @@ export default function PipelineRunDetail() {
|
||||
<input type="checkbox" checked={selectedIds.size === runs.length && runs.length > 0}
|
||||
onChange={toggleAll} className="rounded" />
|
||||
</th>
|
||||
<th className="px-3 py-2.5 text-left font-medium text-[var(--dg-text-secondary)] w-16">{t('pipeline.build')}</th>
|
||||
<th className="px-3 py-2.5 text-left font-medium text-[var(--dg-text-secondary)] w-20">{t('common.status')}</th>
|
||||
<th className="px-3 py-2.5 text-left font-medium text-[var(--dg-text-secondary)] w-16">{t('deploy.pipeline.buildNumber')}</th>
|
||||
<th className="px-3 py-2.5 text-left font-medium text-[var(--dg-text-secondary)] w-20">{t('deploy.table.status')}</th>
|
||||
<th className="px-3 py-2.5 text-left font-medium text-[var(--dg-text-secondary)]">{t('deploy.table.trigger')}</th>
|
||||
<th className="px-3 py-2.5 text-left font-medium text-[var(--dg-text-secondary)]">{t('system.roleUsers')}</th>
|
||||
<th className="px-3 py-2.5 text-left font-medium text-[var(--dg-text-secondary)]">{t('common.elapsed')}</th>
|
||||
<th className="px-3 py-2.5 text-left font-medium text-[var(--dg-text-secondary)] hidden md:table-cell">{t('monitor.logTime')}</th>
|
||||
<th className="px-3 py-2.5 text-left font-medium text-[var(--dg-text-secondary)]">{t('deploy.table.user')}</th>
|
||||
<th className="px-3 py-2.5 text-left font-medium text-[var(--dg-text-secondary)]">{t('deploy.table.duration')}</th>
|
||||
<th className="px-3 py-2.5 text-left font-medium text-[var(--dg-text-secondary)] hidden md:table-cell">{t('common.createdAt')}</th>
|
||||
<th className="px-3 py-2.5 text-right font-medium text-[var(--dg-text-secondary)] w-32">{t('common.actions')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -460,10 +472,10 @@ export default function PipelineRunDetail() {
|
||||
<td className="px-3 py-2.5">
|
||||
<Badge className={`text-xs ${STATUS_BADGES[run.status] || ''} flex items-center gap-1 w-fit`}>
|
||||
<StatusIcon className={`w-3 h-3 ${run.status === 'running' ? 'animate-spin' : ''}`} />
|
||||
{run.status === 'success' ? t('common.success')
|
||||
: run.status === 'failed' ? t('common.failed')
|
||||
: run.status === 'running' ? t('common.executing')
|
||||
: run.status === 'cancelled' ? t('deploy.toast.cancelled')
|
||||
{run.status === 'success' ? t('deploy.status.success')
|
||||
: run.status === 'failed' ? t('deploy.status.failed')
|
||||
: run.status === 'running' ? t('deploy.runDetail.executing')
|
||||
: run.status === 'cancelled' ? t('deploy.status.cancelled')
|
||||
: t('deploy.status.pending')}
|
||||
</Badge>
|
||||
</td>
|
||||
@@ -477,7 +489,7 @@ export default function PipelineRunDetail() {
|
||||
<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 dg-text-blue">
|
||||
<Loader2 className="w-3 h-3 animate-spin" />执行中...
|
||||
<Loader2 className="w-3 h-3 animate-spin" />{t('deploy.pipeline.executing')}
|
||||
</span>
|
||||
) : formatDuration(run.duration)}
|
||||
</td>
|
||||
@@ -488,11 +500,11 @@ export default function PipelineRunDetail() {
|
||||
<div className="flex justify-end gap-1">
|
||||
<Button variant="ghost" size="sm" className="h-7 text-xs"
|
||||
onClick={() => setSelectedRun(run)}>
|
||||
日志
|
||||
{t('deploy.label.log')}
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" className="h-7 text-xs"
|
||||
onClick={() => { setBrowserRun({ id: run.id, buildNumber: run.build_number }); setBrowserOpen(true); }}>
|
||||
<FolderOpen className="w-3.5 h-3.5 mr-1" />浏览
|
||||
<FolderOpen className="w-3.5 h-3.5 mr-1" />{t('deploy.label.browse')}
|
||||
</Button>
|
||||
{run.status === 'running' && (
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7 text-red-500"
|
||||
@@ -520,19 +532,19 @@ export default function PipelineRunDetail() {
|
||||
</CardTitle>
|
||||
{selectedRun.status === 'running' && (
|
||||
<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" />实时
|
||||
<Loader2 className="w-3 h-3 animate-spin mr-1" />{t('deploy.runDetail.realtime')}
|
||||
</Badge>
|
||||
)}
|
||||
{selectedStage && (
|
||||
<Badge className="text-xs border-[var(--dg-border)]" variant="outline">
|
||||
{STAGE_DEFS.find(d => d.key === selectedStage)?.label || selectedStage}
|
||||
{stageLabel(selectedStage)}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs text-[var(--dg-text-muted)]">
|
||||
<span>触发: {triggerLabel(selectedRun)} ({selectedRun.triggered_by || 'admin'})</span>
|
||||
<span>{t('deploy.runDetail.trigger')}: {triggerLabel(selectedRun)} ({selectedRun.triggered_by || 'admin'})</span>
|
||||
<span className="opacity-40">|</span>
|
||||
<span>分支: {selectedRun.branch || '-'}</span>
|
||||
<span>{t('deploy.run.branch')}: {selectedRun.branch || '-'}</span>
|
||||
{selectedRun.commit_id && (
|
||||
<>
|
||||
<span className="opacity-40">|</span>
|
||||
@@ -557,7 +569,7 @@ export default function PipelineRunDetail() {
|
||||
<div className="px-4 py-2 border-b border-[var(--dg-border)] bg-[var(--dg-bg-tertiary)]/30">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[11px] font-semibold text-[var(--dg-text-secondary)]">
|
||||
[{STAGE_DEFS.find(d => d.key === selectedStage)?.label || selectedStage}]
|
||||
[{stageLabel(selectedStage)}]
|
||||
</span>
|
||||
{(() => {
|
||||
const st = stageStats.get(selectedStage);
|
||||
|
||||
Reference in new issue
Block a user