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