refactor(i18n): U15 ApiAccessKeySection i18n 迁移 — 56 个 hash 乱码键清理为语义化 user.apiKey.* 键 + 组件全量接入 useTranslation

- 清理 56 个 apiaccesskey.s{hash} 乱码键为语义化 user.apiKey.* 键 (title/issue/rotate/revoke/descLabel 等)
- ApiAccessKeySection.tsx 全量接入 useTranslation() + t() 调用, UI 中文 57→0
- 复用 common.loading/common.cancel/common.save 通用键
- zh-CN/en-US 完全对齐 (7556 = 7556), 零 hash 键残留
- tsc -b=0, npm build✓ (18.23s), ESLint errors=0/any=0, go build=0, 17/17 测试通过
This commit is contained in:
attacker committed 2026-07-10 18:29:03 +08:00
1 parent a2edf6aa9f
commit 178c98dd37
3 files changed
+177 -171

No files matched your search

@@ -1,5 +1,6 @@
// ApiAccessKeySection — 单条 CI/CD 访问凭证(凭证卡片布局)// Logan.Li
// ApiAccessKeySection — 单条 CI/CD 访问凭证(凭证卡片布局)
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
@@ -52,7 +53,7 @@ function toDateInputValue(value?: string) {
return Number.isNaN(d.getTime()) ? defaultExpiresDate() : d.toISOString().slice(0, 10);
}
/** 根据创建/到期时间计算有效期进度(0-100)// Logan.Li */
/** Compute expiry progress (0-100) based on created/expires timestamps */
function expiryProgress(createdAt?: string, expiresAt?: string) {
if (!createdAt || !expiresAt) return { pct: 0, daysLeft: null as number | null };
const start = new Date(createdAt).getTime();
@@ -71,6 +72,7 @@ function shortKeyId(id?: string) {
}
export function ApiAccessKeySection() {
const { t } = useTranslation();
const { toast } = useToast();
const confirm = useConfirm();
const apiBase = getPublicApiBase();
@@ -137,9 +139,9 @@ export function ApiAccessKeySection() {
});
setKeyMeta(res);
setFormOpen(false);
toast({ title: '凭证信息已更新' });
toast({ title: t('user.apiKey.updated') });
} catch (e: unknown) {
toast({ title: '更新失败', description: getErrorMessage(e), variant: 'destructive' });
toast({ title: t('user.apiKey.updateFailed'), description: getErrorMessage(e), variant: 'destructive' });
} finally {
setActionLoading(false);
}
@@ -148,9 +150,9 @@ export function ApiAccessKeySection() {
const hasKey = !!keyMeta?.has_token;
const ok = await confirm({
title: hasKey ? '轮换凭证' : '签发凭证',
description: hasKey ? '旧凭证将立即作废,是否继续?' : '凭证明文仅显示一次,请妥善保存。',
confirmText: hasKey ? '轮换' : '签发',
title: hasKey ? t('user.apiKey.rotateTitle') : t('user.apiKey.issue'),
description: hasKey ? t('user.apiKey.rotateDesc') : t('user.apiKey.issueDesc'),
confirmText: hasKey ? t('user.apiKey.rotateBtn') : t('user.apiKey.issue'),
variant: hasKey ? 'destructive' : 'default',
});
if (!ok) return;
@@ -178,9 +180,9 @@ export function ApiAccessKeySection() {
expired: false,
days_left: Math.ceil((new Date(res.expires_at).getTime() - Date.now()) / 86400000),
});
toast({ title: '凭证已签发', description: '已保存至本浏览器,请立即复制' });
toast({ title: t('user.apiKey.issued'), description: t('user.apiKey.issuedDesc') });
} catch (e: unknown) {
toast({ title: '签发失败', description: getErrorMessage(e), variant: 'destructive' });
toast({ title: t('user.apiKey.issueFailed'), description: getErrorMessage(e), variant: 'destructive' });
} finally {
setActionLoading(false);
}
@@ -188,9 +190,9 @@ export function ApiAccessKeySection() {
const handleRevoke = async () => {
const ok = await confirm({
title: '吊销凭证',
description: '吊销后 Pipeline / curl 调用将全部失败,是否继续?',
confirmText: '吊销',
title: t('user.apiKey.revokeTitle'),
description: t('user.apiKey.revokeDesc'),
confirmText: t('user.apiKey.revokeBtn'),
variant: 'destructive',
});
if (!ok) return;
@@ -200,15 +202,15 @@ export function ApiAccessKeySection() {
setPlainToken('');
setSessionApiAccessKey('');
setKeyMeta({ has_token: false });
toast({ title: '凭证已吊销' });
toast({ title: t('user.apiKey.revoked') });
} catch (e: unknown) {
toast({ title: '吊销失败', description: getErrorMessage(e), variant: 'destructive' });
toast({ title: t('user.apiKey.revokeFailed'), description: getErrorMessage(e), variant: 'destructive' });
} finally {
setActionLoading(false);
}
};
const authHeader = copyableToken ? `X-AIOps-Access-Key: ${copyableToken}` : 'X-AIOps-Access-Key: <你的访问凭证>';
const authHeader = copyableToken ? `X-AIOps-Access-Key: ${copyableToken}` : `X-AIOps-Access-Key: <${t('user.apiKey.placeholderToken')}>`;
const sampleCluster = 'test-k8s';
const sampleNs = 'default';
const sampleDep = 'demo-jar';
@@ -232,27 +234,27 @@ export function ApiAccessKeySection() {
<div>
<CardTitle className="text-base flex items-center gap-2 text-[var(--dg-text-primary)]">
<KeyRound className="h-4 w-4 text-[var(--dg-blue)]" />
CI/CD 访问凭证
<Badge className="text-[10px] bg-[var(--dg-blue)]/15 text-[var(--dg-blue)] border-[var(--dg-blue)]/30">长期有效</Badge>
{t('user.apiKey.title')}
<Badge className="text-[10px] bg-[var(--dg-blue)]/15 text-[var(--dg-blue)] border-[var(--dg-blue)]/30">{t('user.apiKey.longTerm')}</Badge>
</CardTitle>
<CardDescription className="text-xs mt-1">
每人唯一凭证,Deployment 发布脚本自动引用,不随登录会话过期。
{t('user.apiKey.desc')}
</CardDescription>
</div>
{!loading && !keyMeta?.has_token && (
<Button size="sm" onClick={openCreate} disabled={actionLoading}>
<Plus className="h-4 w-4 mr-1" />签发凭证
<Plus className="h-4 w-4 mr-1" />{t('user.apiKey.issue')}
</Button>
)}
</CardHeader>
<CardContent className="space-y-4">
{loading ? (
<div className="flex items-center gap-2 py-8 text-sm text-[var(--dg-text-muted)] justify-center">
<Loader2 className="h-4 w-4 animate-spin" />加载中…
<Loader2 className="h-4 w-4 animate-spin" />{t('common.loading')}
</div>
) : keyMeta?.has_token ? (
<div className="rounded-xl border border-[var(--dg-border)] bg-[var(--dg-bg-tertiary)]/40 overflow-hidden">
{/* 凭证主体:左侧色条 + 内容区 // Logan.Li */}
{/* credential body: left color bar + content area */}
<div className="flex">
<div className={cn(
'w-1 shrink-0',
@@ -261,7 +263,7 @@ export function ApiAccessKeySection() {
<div className="flex-1 p-4 space-y-4">
<div className="flex flex-wrap items-start justify-between gap-3">
<div className="space-y-1 min-w-0 flex-1">
<p className="text-[10px] uppercase tracking-wider text-[var(--dg-text-muted)]">凭证字符串</p>
<p className="text-[10px] uppercase tracking-wider text-[var(--dg-text-muted)]">{t('user.apiKey.tokenLabel')}</p>
<div className="flex items-center gap-2">
<code className="flex-1 min-w-0 text-xs font-mono text-[var(--dg-text-primary)] bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded-md px-3 py-2 truncate block">
{showToken && copyableToken ? copyableToken : mask}
@@ -272,7 +274,7 @@ export function ApiAccessKeySection() {
size="icon"
className="h-9 w-9 shrink-0 border-[var(--dg-border)]"
onClick={() => setShowToken(!showToken)}
title={showToken ? '隐藏' : '显示'}
title={showToken ? t('user.apiKey.hide') : t('user.apiKey.show')}
>
{showToken ? <EyeOff className="h-3.5 w-3.5" /> : <Eye className="h-3.5 w-3.5" />}
</Button>
@@ -281,41 +283,41 @@ export function ApiAccessKeySection() {
</div>
<div className="flex flex-wrap gap-1.5 shrink-0">
<Button variant="outline" size="sm" className="h-8 text-xs border-[var(--dg-border)]" onClick={openEdit} disabled={actionLoading}>
<Settings2 className="h-3 w-3 mr-1" />调整
<Settings2 className="h-3 w-3 mr-1" />{t('user.apiKey.adjust')}
</Button>
<Button variant="outline" size="sm" className="h-8 text-xs border-[var(--dg-border)]" onClick={openCreate} disabled={actionLoading}>
<RotateCw className="h-3 w-3 mr-1" />轮换
<RotateCw className="h-3 w-3 mr-1" />{t('user.apiKey.rotate')}
</Button>
<Button variant="outline" size="sm" className="h-8 text-xs border-red-500/30 dg-text-red" onClick={handleRevoke} disabled={actionLoading}>
<Ban className="h-3 w-3 mr-1" />吊销
<Ban className="h-3 w-3 mr-1" />{t('user.apiKey.revoke')}
</Button>
</div>
</div>
<div className="grid grid-cols-2 sm:grid-cols-3 gap-x-6 gap-y-3 text-xs">
<div>
<p className="text-[var(--dg-text-muted)] mb-0.5">用途说明</p>
<p className="text-[var(--dg-text-secondary)]">{keyMeta.description || '未填写'}</p>
<p className="text-[var(--dg-text-muted)] mb-0.5">{t('user.apiKey.descLabel')}</p>
<p className="text-[var(--dg-text-secondary)]">{keyMeta.description || t('user.apiKey.descEmpty')}</p>
</div>
<div>
<p className="text-[var(--dg-text-muted)] mb-0.5">凭证标识</p>
<p className="text-[var(--dg-text-muted)] mb-0.5">{t('user.apiKey.keyIdLabel')}</p>
<p className="font-mono text-[var(--dg-text-secondary)]" title={keyMeta.key_id}>{shortKeyId(keyMeta.key_id)}</p>
</div>
<div>
<p className="text-[var(--dg-text-muted)] mb-0.5">签发时间</p>
<p className="text-[var(--dg-text-muted)] mb-0.5">{t('user.apiKey.createdLabel')}</p>
<p className="text-[var(--dg-text-secondary)]">{formatDateTime(keyMeta.created_at)}</p>
</div>
</div>
{/* 有效期进度条 // Logan.Li */}
{/* expiry progress bar */}
<div className="space-y-1.5">
<div className="flex items-center justify-between text-[11px]">
<span className="flex items-center gap-1 text-[var(--dg-text-muted)]">
<Clock className="h-3 w-3" />
{isExpired ? '已过期' : daysLeft != null ? `剩余 ${daysLeft} 天` : '有效期'}
{isExpired ? t('user.apiKey.expired') : daysLeft != null ? t('user.apiKey.daysLeft', { days: daysLeft }) : t('user.apiKey.validity')}
</span>
<span className="text-[var(--dg-text-muted)]">
有效至 {formatDateTime(keyMeta.expires_at)}
{t('user.apiKey.validUntil', { date: formatDateTime(keyMeta.expires_at) })}
</span>
</div>
<div className="h-1.5 rounded-full bg-[var(--dg-bg-secondary)] overflow-hidden">
@@ -329,13 +331,13 @@ export function ApiAccessKeySection() {
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-[11px] text-[var(--dg-text-muted)] pt-1 border-t border-[var(--dg-border)]/60">
<span className="inline-flex items-center gap-1">
<Activity className="h-3 w-3" />
最近调用 {formatDateTime(keyMeta.last_used_at)}
{t('user.apiKey.lastUsed', { date: formatDateTime(keyMeta.last_used_at) })}
</span>
{keyMeta.prefix && (
<span>前缀 <code className="text-[var(--dg-text-secondary)]">{keyMeta.prefix}</code></span>
<span>{t('user.apiKey.prefix')} <code className="text-[var(--dg-text-secondary)]">{keyMeta.prefix}</code></span>
)}
{isExpired && (
<Badge variant="outline" className="text-[10px] border-red-500/40 dg-text-red">需续期或轮换</Badge>
<Badge variant="outline" className="text-[10px] border-red-500/40 dg-text-red">{t('user.apiKey.needRenew')}</Badge>
)}
</div>
</div>
@@ -346,35 +348,35 @@ export function ApiAccessKeySection() {
<div className="mx-auto mb-3 h-12 w-12 rounded-full bg-[var(--dg-blue)]/10 flex items-center justify-center">
<KeyRound className="h-6 w-6 text-[var(--dg-blue)]" />
</div>
<p className="text-sm text-[var(--dg-text-secondary)]">尚未签发 CI/CD 访问凭证</p>
<p className="text-xs text-[var(--dg-text-muted)] mt-1 mb-4">签发后可用于 Pipeline 触发镜像更新、滚动重启等操作</p>
<p className="text-sm text-[var(--dg-text-secondary)]">{t('user.apiKey.emptyTitle')}</p>
<p className="text-xs text-[var(--dg-text-muted)] mt-1 mb-4">{t('user.apiKey.emptyDesc')}</p>
<Button size="sm" onClick={openCreate} disabled={actionLoading}>
<Plus className="h-4 w-4 mr-1" />立即签发
<Plus className="h-4 w-4 mr-1" />{t('user.apiKey.issueNow')}
</Button>
</div>
)}
{keyMeta?.has_token && !copyableToken && (
<p className="text-[11px] dg-text-yellow/90">本机未缓存凭证明文,请点击「轮换」重新签发一次;签发后将自动填入 Deployment 发布 curl。</p>
<p className="text-[11px] dg-text-yellow/90">{t('user.apiKey.cacheWarning')}</p>
)}
<div className="rounded-lg bg-[var(--dg-bg-tertiary)]/60 p-4 space-y-3">
<p className="text-xs font-medium text-[var(--dg-text-secondary)] flex items-center gap-1.5">
<Rocket className="h-3.5 w-3.5 dg-text-purple" />调用示例
<Rocket className="h-3.5 w-3.5 dg-text-purple" />{t('user.apiKey.examples')}
</p>
<CodeSnippetBlock
label="更新镜像"
label={t('user.apiKey.updateImage')}
code={updateCurl}
copyLabel="curl 已复制"
copyLabel={t('user.apiKey.curlCopied')}
copyDisabled={!copyableToken}
copyDisabledTitle="请先签发访问凭证"
copyDisabledTitle={t('user.apiKey.copyDisabledTitle')}
/>
<CodeSnippetBlock
label="滚动重启"
label={t('user.apiKey.restart')}
code={restartCurl}
copyLabel="curl 已复制"
copyLabel={t('user.apiKey.curlCopied')}
copyDisabled={!copyableToken}
copyDisabledTitle="请先签发访问凭证"
copyDisabledTitle={t('user.apiKey.copyDisabledTitle')}
/>
</div>
</CardContent>
@@ -384,24 +386,24 @@ export function ApiAccessKeySection() {
<DialogContent className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] sm:max-w-md">
<DialogHeader>
<DialogTitle className="text-[var(--dg-text-primary)]">
{formMode === 'edit' ? '调整凭证' : keyMeta?.has_token ? '轮换凭证' : '签发凭证'}
{formMode === 'edit' ? t('user.apiKey.adjustTitle') : keyMeta?.has_token ? t('user.apiKey.rotateTitle') : t('user.apiKey.issue')}
</DialogTitle>
<DialogDescription className="text-xs text-[var(--dg-text-muted)]">
{formMode === 'edit' ? '修改用途说明与有效期限,凭证字符串不变。' : '填写用途并设定有效期,签发后请立即保存明文。'}
{formMode === 'edit' ? t('user.apiKey.editDesc') : t('user.apiKey.createDesc')}
</DialogDescription>
</DialogHeader>
<div className="space-y-3 py-2">
<div>
<Label className="text-xs text-[var(--dg-text-muted)]">用途说明</Label>
<Label className="text-xs text-[var(--dg-text-muted)]">{t('user.apiKey.descLabel')}</Label>
<Input
value={formDescription}
onChange={e => setFormDescription(e.target.value)}
placeholder="如 Jenkins 流水线、GitLab CI"
placeholder={t('user.apiKey.placeholderDesc')}
className="mt-1 h-9 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]"
/>
</div>
<div>
<Label className="text-xs text-[var(--dg-text-muted)]">有效至</Label>
<Label className="text-xs text-[var(--dg-text-muted)]">{t('user.apiKey.validUntilLabel')}</Label>
<Input
type="date"
value={formExpiresAt}
@@ -411,10 +413,10 @@ export function ApiAccessKeySection() {
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setFormOpen(false)} className="border-[var(--dg-border)]">取消</Button>
<Button variant="outline" onClick={() => setFormOpen(false)} className="border-[var(--dg-border)]">{t('common.cancel')}</Button>
<Button onClick={handleFormSubmit} disabled={actionLoading || !formExpiresAt}>
{actionLoading ? <Loader2 className="h-4 w-4 animate-spin mr-1" /> : null}
{formMode === 'edit' ? '保存' : keyMeta?.has_token ? '轮换' : '签发'}
{formMode === 'edit' ? t('common.save') : keyMeta?.has_token ? t('user.apiKey.rotateBtn') : t('user.apiKey.issue')}
</Button>
</DialogFooter>
</DialogContent>
@@ -423,19 +425,19 @@ export function ApiAccessKeySection() {
<Dialog open={tokenDialogOpen} onOpenChange={setTokenDialogOpen}>
<DialogContent className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] sm:max-w-lg">
<DialogHeader>
<DialogTitle className="text-[var(--dg-text-primary)]">请保存凭证明文</DialogTitle>
<DialogTitle className="text-[var(--dg-text-primary)]">{t('user.apiKey.saveTokenTitle')}</DialogTitle>
<DialogDescription className="text-xs text-[var(--dg-text-muted)]">
仅此一次机会查看完整字符串,关闭后无法再次获取。
{t('user.apiKey.saveTokenDesc')}
</DialogDescription>
</DialogHeader>
<CodeSnippetBlock
label="访问凭证"
label={t('user.apiKey.accessTokenLabel')}
code={revealedToken}
copyLabel="凭证已复制"
copyLabel={t('user.apiKey.credentialCopied')}
className="mt-2"
/>
<DialogFooter>
<Button onClick={() => setTokenDialogOpen(false)}>我已保存</Button>
<Button onClick={() => setTokenDialogOpen(false)}>{t('user.apiKey.saved')}</Button>
</DialogFooter>
</DialogContent>
</Dialog>