feat(i18n): ServerManager 服务器管理页国际化 — 27 处硬编码中文改翻译键 + server 命名空间(48 键)

- ServerManager.tsx: 页面标题/按钮/搜索/状态筛选/表头/分页/toast/CSV导出/确认弹框 全量 t() 化
- 复用 common.* 通用键(refresh/export/delete/status/actions/loading/running/stopped/maintenance/offline)
- 新增 server.* 命名空间: title/subtitle/batchImport/cloudImport/csvImport/addServer/searchPlaceholder/tableHeaders/pagination/toast/csv 子组
- zh-CN/en-US 键总数 242→290 (server 48 键, 中英完全对齐)
- tsc -b=0, npm build=✓(19.13s), 17/17 集成测试通过
This commit is contained in:
attacker committed 2026-06-29 13:25:18 +08:00
1 parent eb0fef7fce
commit 3e9352f690
3 files changed
+153 -51

No files matched your search

@@ -1,4 +1,5 @@
import { useState, useEffect, useRef, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useQueryClient } from '@tanstack/react-query';
import { cn } from '@/lib/utils';
import { Card, CardContent } from '@/components/ui/card';
@@ -56,6 +57,7 @@ import { ServerTableRow } from './ServerTableRow';
import { usePermission } from '@/hooks/usePermission';
export function ServerManager() {
const { t } = useTranslation();
const confirm = useConfirm();
const queryClient = useQueryClient();
const { hasPermission } = usePermission();
@@ -215,18 +217,18 @@ export function ServerManager() {
const handleDelete = async (id: string) => {
const server = servers.find((s) => String(s.id) === String(id));
const ok = await confirm({
title: '删除服务器',
description: `确定删除「${server?.hostname || id}」吗?此操作不可撤销。`,
confirmText: '删除',
title: t('server.deleteTitle'),
description: `${server?.hostname || id}`,
confirmText: t('common.delete'),
variant: 'destructive',
});
if (!ok) return;
try {
await deleteMutation.mutateAsync(id);
toast.success('服务器已删除');
toast.success(t('server.deleted'));
} catch (e) {
console.error('删除服务器失败:', e);
toast.error('删除失败');
console.error('[ServerManager] delete failed:', e);
toast.error(t('server.deleteFailed'));
}
};
@@ -234,19 +236,19 @@ export function ServerManager() {
const handleBatchDelete = async () => {
if (selectedServers.length === 0) return;
const ok = await confirm({
title: '批量删除服务器',
description: `确定删除选中的 ${selectedServers.length} 台服务器吗?此操作不可撤销。`,
confirmText: '批量删除',
title: t('server.batchDeleteTitle'),
description: `${selectedServers.length}`,
confirmText: t('server.batchDelete'),
variant: 'destructive',
});
if (!ok) return;
try {
await batchDeleteMutation.mutateAsync(selectedServers.map((id) => Number(id)));
toast.success(`已删除 ${selectedServers.length} 台服务器`);
toast.success(t('server.deletedCount', { count: selectedServers.length }));
setSelectedServers([]);
} catch (e) {
console.error('批量删除失败:', e);
toast.error('批量删除失败');
console.error('[ServerManager] batch delete failed:', e);
toast.error(t('server.batchDeleteFailed'));
}
};
@@ -320,11 +322,11 @@ export function ServerManager() {
} else {
await createMutation.mutateAsync(payload);
}
toast.success(editingId ? '服务器已更新' : '服务器已创建');
toast.success(editingId ? t('server.updated') : t('server.created'));
setFormOpen(false);
} catch (e) {
console.error('保存服务器失败:', e);
toast.error('保存失败,请重试');
console.error('[ServerManager] save failed:', e);
toast.error(t('server.saveFailed'));
} finally {
setFormSaving(false);
}
@@ -333,10 +335,14 @@ export function ServerManager() {
// 导出 CSV — 将当前服务器列表导出为 CSV 文件 // Logan.Li
const handleExport = () => {
if (servers.length === 0) {
toast.error('没有可导出的数据');
toast.error(t('server.noExportData'));
return;
}
const headers = ['主机名', 'IP地址', '公网IP', '端口', '操作系统', 'CPU(核)', '内存(GB)', '磁盘(GB)', '状态', '标签'];
const headers = [
t('server.csv.hostname'), t('server.csv.ip'), t('server.csv.publicIp'), t('server.csv.port'),
t('server.csv.os'), t('server.csv.cpu'), t('server.csv.memory'), t('server.csv.disk'),
t('server.csv.status'), t('server.csv.tags'),
];
const rows = servers.map((s) => {
const memGB = s.memory ? Math.round(Number(s.memory) / 1024) : 0;
const tags = getTags(s).join(';');
@@ -353,7 +359,7 @@ export function ServerManager() {
a.download = `servers_${new Date().toISOString().slice(0, 10)}.csv`;
a.click();
URL.revokeObjectURL(url);
toast.success(`已导出 ${servers.length} 台服务器`);
toast.success(t('server.exported', { count: servers.length }));
};
const colSpanCount = 8;
@@ -363,8 +369,8 @@ export function ServerManager() {
{/* Page Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-[var(--dg-text-primary)]">服务器管理</h1>
<p className="text-sm text-[var(--dg-text-muted)] mt-1">管理云平台服务器资产</p>
<h1 className="text-2xl font-bold text-[var(--dg-text-primary)]">{t('server.title')}</h1>
<p className="text-sm text-[var(--dg-text-muted)] mt-1">{t('server.subtitle')}</p>
</div>
<div className="flex items-center gap-2">
<Button
@@ -373,7 +379,7 @@ export function ServerManager() {
onClick={() => refetch()}
>
<RefreshCw className={cn('h-4 w-4 mr-2', loading && 'animate-spin')} />
刷新
{t('common.refresh')}
</Button>
<Button
variant="outline"
@@ -381,34 +387,34 @@ export function ServerManager() {
onClick={handleExport}
>
<Download className="h-4 w-4 mr-2" />
导出
{t('common.export')}
</Button>
{hasPermission('server:create') && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)] hover:bg-[var(--dg-bg-hover)]">
<Upload className="h-4 w-4 mr-2" />
批量导入
{t('server.batchImport')}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]">
<DropdownMenuLabel>选择导入方式</DropdownMenuLabel>
<DropdownMenuLabel>{t('server.importMethod')}</DropdownMenuLabel>
<DropdownMenuSeparator className="bg-[var(--dg-border)]" />
<DropdownMenuItem
className="hover:bg-[var(--dg-bg-hover)] cursor-pointer focus:bg-[var(--dg-bg-hover)] focus:text-[var(--dg-text-primary)]"
onClick={() => setCloudImportOpen(true)}
>
<Cloud className="h-4 w-4 mr-2 text-[var(--dg-blue)]" />
云主机导入
<span className="ml-2 text-xs text-[var(--dg-text-muted)]">从云平台同步</span>
{t('server.cloudImport')}
<span className="ml-2 text-xs text-[var(--dg-text-muted)]">{t('server.cloudImportHint')}</span>
</DropdownMenuItem>
<DropdownMenuItem
className="hover:bg-[var(--dg-bg-hover)] cursor-pointer focus:bg-[var(--dg-bg-hover)] focus:text-[var(--dg-text-primary)]"
onClick={() => setExcelImportOpen(true)}
>
<FileSpreadsheet className="h-4 w-4 mr-2 text-emerald-400" />
CSV导入
<span className="ml-2 text-xs text-[var(--dg-text-muted)]">hostname + IP 即可</span>
{t('server.csvImport')}
<span className="ml-2 text-xs text-[var(--dg-text-muted)]">{t('server.csvImportHint')}</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@@ -419,7 +425,7 @@ export function ServerManager() {
onClick={openCreateForm}
>
<Plus className="h-4 w-4 mr-2" />
添加服务器
{t('server.addServer')}
</Button>
)}
</div>
@@ -437,7 +443,7 @@ export function ServerManager() {
<div className="relative w-64">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-[var(--dg-text-muted)]" />
<Input
placeholder="搜索主机名或IP..."
placeholder={t('server.searchPlaceholder')}
value={searchQuery}
onChange={(e) => handleSearch(e.target.value)}
className="pl-9 h-9 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)]"
@@ -445,20 +451,20 @@ export function ServerManager() {
</div>
<Select value={statusFilter} onValueChange={handleStatusFilter}>
<SelectTrigger className="w-32 h-9 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]">
<SelectValue placeholder="状态" />
<SelectValue placeholder={t('common.status')} />
</SelectTrigger>
<SelectContent className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]">
<SelectItem value="all">全部状态</SelectItem>
<SelectItem value="running">运行中</SelectItem>
<SelectItem value="stopped">已停止</SelectItem>
<SelectItem value="maintenance">维护中</SelectItem>
<SelectItem value="offline">离线</SelectItem>
<SelectItem value="all">{t('server.allStatus')}</SelectItem>
<SelectItem value="running">{t('common.running')}</SelectItem>
<SelectItem value="stopped">{t('common.stopped')}</SelectItem>
<SelectItem value="maintenance">{t('common.maintenance')}</SelectItem>
<SelectItem value="offline">{t('common.offline')}</SelectItem>
</SelectContent>
</Select>
</div>
{selectedServers.length > 0 && (
<div className="flex items-center gap-2">
<span className="text-sm text-[var(--dg-text-secondary)]">已选择 {selectedServers.length} 项</span>
<span className="text-sm text-[var(--dg-text-secondary)]">{t('server.selected', { count: selectedServers.length })}</span>
{hasPermission('server:delete') && (
<Button
variant="outline"
@@ -467,7 +473,7 @@ export function ServerManager() {
onClick={handleBatchDelete}
>
<Trash2 className="h-4 w-4 mr-2" />
批量删除
{t('server.batchDelete')}
</Button>
)}
</div>
@@ -487,14 +493,14 @@ export function ServerManager() {
className="border-[var(--dg-border)]"
/>
</TableHead>
<TableHead className="text-[var(--dg-text-secondary)]">主机名</TableHead>
<TableHead className="text-[var(--dg-text-secondary)]">IP地址</TableHead>
<TableHead className="text-[var(--dg-text-secondary)]">公网IP</TableHead>
<TableHead className="text-[var(--dg-text-secondary)]">状态</TableHead>
<TableHead className="text-[var(--dg-text-secondary)]">配置</TableHead>
<TableHead className="text-[var(--dg-text-secondary)]">使用率</TableHead>
<TableHead className="text-[var(--dg-text-secondary)]">标签</TableHead>
<TableHead className="text-[var(--dg-text-secondary)] text-right">操作</TableHead>
<TableHead className="text-[var(--dg-text-secondary)]">{t('server.hostname')}</TableHead>
<TableHead className="text-[var(--dg-text-secondary)]">{t('server.ipAddress')}</TableHead>
<TableHead className="text-[var(--dg-text-secondary)]">{t('server.publicIp')}</TableHead>
<TableHead className="text-[var(--dg-text-secondary)]">{t('common.status')}</TableHead>
<TableHead className="text-[var(--dg-text-secondary)]">{t('server.config')}</TableHead>
<TableHead className="text-[var(--dg-text-secondary)]">{t('server.usage')}</TableHead>
<TableHead className="text-[var(--dg-text-secondary)]">{t('server.tags')}</TableHead>
<TableHead className="text-[var(--dg-text-secondary)] text-right">{t('common.actions')}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -502,13 +508,13 @@ export function ServerManager() {
<TableRow className="border-[var(--dg-border)]">
<TableCell colSpan={colSpanCount} className="text-center text-[var(--dg-text-muted)] py-12">
<RefreshCw className="h-5 w-5 animate-spin mx-auto mb-2" />
加载中...
{t('common.loading')}
</TableCell>
</TableRow>
) : servers.length === 0 ? (
<TableRow className="border-[var(--dg-border)]">
<TableCell colSpan={colSpanCount} className="text-center text-[var(--dg-text-muted)] py-12">
暂无服务器数据
{t('server.noData')}
</TableCell>
</TableRow>
) : (
@@ -532,7 +538,7 @@ export function ServerManager() {
{/* 分页 */}
<div className="flex items-center justify-between px-4 py-3 border-t border-[var(--dg-border)]">
<div className="flex items-center gap-2 text-sm text-[var(--dg-text-muted)]">
<span>每页</span>
<span>{t('server.perPage')}</span>
<Select value={String(pageSize)} onValueChange={(v) => { setPageSize(Number(v)); setPage(1); }}>
<SelectTrigger className="w-16 h-8 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] text-xs">
<SelectValue />
@@ -543,8 +549,8 @@ export function ServerManager() {
<SelectItem value="30">30</SelectItem>
</SelectContent>
</Select>
<span>条</span>
<span className="ml-4">共 {total} 条</span>
<span>{t('server.items')}</span>
<span className="ml-4">{t('server.totalItems', { count: total })}</span>
</div>
<div className="flex items-center gap-1">
<Button