feat(i18n): deploy 模块(Pipeline/Credential)国际化增量迁移 — 翻译键 deploy 命名空间(455键) + CredentialManager 补全 3 处 UI 文案

- PipelineManager.tsx: 流水线全量翻译(标题/搜索/CRUD/阶段/任务/运行/横幅)
- CredentialManager.tsx: 操作按钮 title 属性补全(编辑/禁用启用/删除)
- zh-CN.json + en-US.json: deploy 命名空间 455 键完全对齐
- 总翻译键: 1019(zh) = 1019(en)
- tsc -b 零错误, npm build 成功(17.42s)
This commit is contained in:
attacker committed 2026-06-30 22:10:26 +08:00
1 parent ed7a5c07a7
commit 01967bc52b
4 files changed
+1053 -67

No files matched your search

@@ -1,4 +1,5 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
Plus, Pencil, Trash2, RefreshCw, Key, Shield, Search,
Clock, CheckCircle2, XCircle, Zap,
@@ -17,6 +18,7 @@ import { CredFormDialog } from './credential-manager/CredFormDialog';
// 凭据管理页编排器 // Logan.Li
export default function CredentialManager() {
const { t } = useTranslation();
const confirm = useConfirm();
const [dialogOpen, setDialogOpen] = useState(false);
const [editing, setEditing] = useState<Credential | null>(null);
@@ -38,28 +40,28 @@ export default function CredentialManager() {
const handleDelete = async (cred: Credential) => {
const ok = await confirm({
title: '删除凭据',
description: `确定删除凭据「${cred.name}」?此操作不可撤销。`,
confirmText: '删除',
title: t('deploy.cred.deleteCred'),
description: t('deploy.pipeline.deleteConfirm', { name: cred.name }),
confirmText: t('common.delete'),
variant: 'destructive',
});
if (!ok) return;
try {
await deleteMutation.mutateAsync(cred.id);
toast.success('已删除');
} catch { toast.error('删除失败'); }
toast.success(t('deploy.toast.deleted'));
} catch { toast.error(t('deploy.toast.deleteFailed')); }
};
const handleToggleActive = async (cred: Credential) => {
try {
await toggleActiveMutation.mutateAsync({ id: cred.id, isActive: !cred.is_active });
toast.success(cred.is_active ? '已禁用' : '已启用');
} catch { toast.error('操作失败'); }
toast.success(cred.is_active ? t('deploy.toast.disabled') : t('deploy.toast.enabled'));
} catch { toast.error(t('deploy.toast.operationFailed')); }
};
const handleCopyPrefix = (prefix: string) => {
navigator.clipboard.writeText(prefix);
toast.success('引用 ID 已复制');
toast.success(t('deploy.cred.refIdCopied'));
};
const typeOptions = Object.entries(TYPE_CONFIG) as [SecretType, typeof TYPE_CONFIG[SecretType]][];
@@ -68,17 +70,17 @@ export default function CredentialManager() {
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<h2 className="text-lg font-semibold text-[var(--dg-text-primary)]">凭据管理</h2>
<h2 className="text-lg font-semibold text-[var(--dg-text-primary)]">{t('deploy.cred.title')}</h2>
<p className="text-sm text-[var(--dg-text-muted)] mt-0.5">
统一管理 SSH 密钥、Docker 密码、AKSK、数据库等敏感凭据(AES 加密存储)
{t('deploy.cred.subtitle')}
</p>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" onClick={() => refetch()}>
<RefreshCw className="w-4 h-4 mr-1" />刷新
<RefreshCw className="w-4 h-4 mr-1" />{t('common.refresh')}
</Button>
<Button size="sm" onClick={() => { setEditing(null); setDialogOpen(true); }}>
<Plus className="w-4 h-4 mr-1" />新增凭据
<Plus className="w-4 h-4 mr-1" />{t('deploy.cred.addCredential')}
</Button>
</div>
</div>
@@ -87,10 +89,10 @@ export default function CredentialManager() {
<div className="flex items-center gap-3 flex-wrap">
<Select value={filterType} onValueChange={setFilterType}>
<SelectTrigger className="w-[140px] bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] h-9 text-sm">
<SelectValue placeholder="全部类型" />
<SelectValue placeholder={t('deploy.label.allType')} />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">全部类型</SelectItem>
<SelectItem value="all">{t('deploy.label.allType')}</SelectItem>
{typeOptions.map(([k, v]) => (
<SelectItem key={k} value={k}>
<span className="flex items-center gap-2"><v.icon className="h-3.5 w-3.5" />{v.label}</span>
@@ -101,23 +103,23 @@ export default function CredentialManager() {
<div className="relative flex-1 max-w-xs">
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-4 w-4 text-[var(--dg-text-muted)]" />
<Input value={filterSearch} onChange={e => setFilterSearch(e.target.value)}
placeholder="搜索名称、描述..."
placeholder={t('deploy.label.searchPlaceholder')}
className="pl-8 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] h-9 text-sm" />
</div>
<span className="text-xs text-[var(--dg-text-muted)]">{credentials.length} 个凭据</span>
<span className="text-xs text-[var(--dg-text-muted)]">{t('deploy.cred.credCount', { count: credentials.length })}</span>
</div>
{loading ? (
<div className="text-center py-16 text-[var(--dg-text-secondary)]">
<Zap className="w-6 h-6 animate-spin mx-auto mb-2 text-[var(--dg-blue)]" />加载中...
<Zap className="w-6 h-6 animate-spin mx-auto mb-2 text-[var(--dg-blue)]" />{t('deploy.label.loading')}
</div>
) : credentials.length === 0 ? (
<div className="text-center py-16 border border-dashed border-[var(--dg-border)] rounded-lg">
<Shield className="w-12 h-12 mx-auto mb-3 text-[var(--dg-text-muted)]" />
<p className="text-[var(--dg-text-secondary)] mb-2">暂无凭据</p>
<p className="text-xs text-[var(--dg-text-muted)] mb-4">创建 SSH、Docker、AKSK 等类型的凭据,供流水线和服务器使用</p>
<p className="text-[var(--dg-text-secondary)] mb-2">{t('deploy.cred.noCredentials')}</p>
<p className="text-xs text-[var(--dg-text-muted)] mb-4">{t('deploy.cred.subtitle')}</p>
<Button onClick={() => { setEditing(null); setDialogOpen(true); }}>
<Plus className="w-4 h-4 mr-1" />创建第一个凭据
<Plus className="w-4 h-4 mr-1" />{t('deploy.cred.createFirst')}
</Button>
</div>
) : (
@@ -125,13 +127,13 @@ export default function CredentialManager() {
<table className="w-full text-sm">
<thead className="bg-[var(--dg-bg-tertiary)] border-b border-[var(--dg-border)]">
<tr>
<th className="px-3 py-2.5 text-left font-medium text-[var(--dg-text-secondary)]">名称</th>
<th className="px-3 py-2.5 text-left font-medium text-[var(--dg-text-secondary)]">类型</th>
<th className="px-3 py-2.5 text-left font-medium text-[var(--dg-text-secondary)] hidden md:table-cell">摘要</th>
<th className="px-3 py-2.5 text-left font-medium text-[var(--dg-text-secondary)] hidden lg:table-cell">标签</th>
<th className="px-3 py-2.5 text-left font-medium text-[var(--dg-text-secondary)] hidden xl:table-cell">引用ID</th>
<th className="px-3 py-2.5 text-left font-medium text-[var(--dg-text-secondary)]">状态</th>
<th className="px-3 py-2.5 text-right font-medium text-[var(--dg-text-secondary)]">操作</th>
<th className="px-3 py-2.5 text-left font-medium text-[var(--dg-text-secondary)]">{t('deploy.table.name')}</th>
<th className="px-3 py-2.5 text-left font-medium text-[var(--dg-text-secondary)]">{t('deploy.table.type')}</th>
<th className="px-3 py-2.5 text-left font-medium text-[var(--dg-text-secondary)] hidden md:table-cell">{t('deploy.table.repoUrl')}</th>
<th className="px-3 py-2.5 text-left font-medium text-[var(--dg-text-secondary)] hidden lg:table-cell">{t('deploy.table.tagLabel')}</th>
<th className="px-3 py-2.5 text-left font-medium text-[var(--dg-text-secondary)] hidden xl:table-cell">{t('deploy.cred.refId')}</th>
<th className="px-3 py-2.5 text-left font-medium text-[var(--dg-text-secondary)]">{t('deploy.table.status')}</th>
<th className="px-3 py-2.5 text-right font-medium text-[var(--dg-text-secondary)]">{t('deploy.table.actions')}</th>
</tr>
</thead>
<tbody>
@@ -181,28 +183,28 @@ export default function CredentialManager() {
<td className="px-3 py-2.5">
<div className="flex items-center gap-1">
{cred.is_active ? (
<Badge className="bg-green-500/20 text-green-400 text-xs border-0">启用</Badge>
<Badge className="bg-green-500/20 text-green-400 text-xs border-0">{t('common.enable')}</Badge>
) : (
<Badge className="bg-red-500/20 text-red-400 text-xs border-0">禁用</Badge>
<Badge className="bg-red-500/20 text-red-400 text-xs border-0">{t('common.disable')}</Badge>
)}
{isExpired && (
<Badge className="bg-yellow-500/20 text-yellow-400 text-xs border-0"><Clock className="h-2.5 w-2.5 mr-0.5" />过期</Badge>
<Badge className="bg-yellow-500/20 text-yellow-400 text-xs border-0"><Clock className="h-2.5 w-2.5 mr-0.5" />{t('deploy.status.expired')}</Badge>
)}
</div>
</td>
<td className="px-3 py-2.5 text-right" onClick={(e) => e.stopPropagation()}>
<div className="flex justify-end gap-1">
<Button variant="ghost" size="icon" className="h-7 w-7"
onClick={() => { setEditing(cred as Credential); setDialogOpen(true); }} title="编辑">
onClick={() => { setEditing(cred as Credential); setDialogOpen(true); }} title={t('common.edit')}>
<Pencil className="w-3.5 h-3.5" />
</Button>
<Button variant="ghost" size="icon" className="h-7 w-7"
onClick={() => handleToggleActive(cred as Credential)}
title={cred.is_active ? '禁用' : '启用'}>
title={cred.is_active ? t('common.disable') : t('common.enable')}>
{cred.is_active ? <XCircle className="w-3.5 h-3.5 text-yellow-500" /> : <CheckCircle2 className="w-3.5 h-3.5 text-green-500" />}
</Button>
<Button variant="ghost" size="icon" className="h-7 w-7 text-red-500"
onClick={() => handleDelete(cred as Credential)} title="删除">
onClick={() => handleDelete(cred as Credential)} title={t('common.delete')}>
<Trash2 className="w-3.5 h-3.5" />
</Button>
</div>