From 9f15f42b477c1ddf152bb053ecdb7485b638277d Mon Sep 17 00:00:00 2001 From: ryan Date: Fri, 24 Jul 2026 16:37:24 +0800 Subject: [PATCH] fix(frontend): keep browser API on same-origin rewrites in dev Document that NEXT_PUBLIC_WAVELET_BACKEND_URL must stay unset for local next dev so axios hits /api/* and Next rewrites proxy to the backend. --- frontend/.env.example | 9 +- frontend/app/(docs)/docs/api/page.tsx | 18 +- frontend/app/(docs)/docs/how-to-use/page.tsx | 14 +- .../app/(docs)/docs/privacy-policy/page.tsx | 15 +- .../app/(docs)/docs/terms-of-service/page.tsx | 13 +- .../database/components/cache-manager.tsx | 102 +- .../admin/database/components/sql-console.tsx | 46 +- .../database/components/table-browser.tsx | 28 +- .../app/(main)/admin/database/page-client.tsx | 79 +- frontend/app/(main)/admin/demo/page.tsx | 60 +- .../(main)/admin/demo/tabs/controls-tab.tsx | 46 +- .../(main)/admin/demo/tabs/dashboard-tab.tsx | 42 +- .../app/(main)/admin/demo/tabs/table-tab.tsx | 60 +- .../admin/files/components/file-list.tsx | 106 +- .../admin/files/components/file-stats.tsx | 92 +- .../files/components/storage-config-tab.tsx | 82 +- frontend/app/(main)/admin/files/page.tsx | 12 +- .../logs/components/access-analytics.tsx | 50 +- .../admin/logs/components/access-logs.tsx | 83 +- .../(main)/admin/logs/components/app-logs.tsx | 12 +- .../app/(main)/admin/logs/page-client.tsx | 12 +- .../admin/push/components/events-tab.tsx | 163 +- .../admin/push/components/histories-tab.tsx | 82 +- .../admin/push/components/settings-tab.tsx | 127 +- frontend/app/(main)/admin/push/page.tsx | 10 +- .../settings/components/system-status.tsx | 150 +- .../app/(main)/admin/settings/page-client.tsx | 18 +- frontend/app/(main)/admin/system/page.tsx | 45 +- .../tasks/components/task-executions.tsx | 125 +- .../admin/tasks/components/task-manager.tsx | 57 +- .../admin/tasks/components/task-schedules.tsx | 107 +- frontend/app/(main)/admin/tasks/page.tsx | 12 +- .../users/components/create-user-modal.tsx | 50 +- .../users/components/edit-user-modal.tsx | 53 +- .../users/components/user-detail-sheet.tsx | 46 +- .../users/components/user-filter-bar.tsx | 30 +- frontend/app/(main)/admin/users/page.tsx | 34 +- frontend/app/(main)/files/page.tsx | 4 +- frontend/app/not-found.tsx | 10 +- frontend/components/auth/cap-widget.tsx | 20 +- frontend/components/auth/register-form.tsx | 2 +- frontend/components/auth/require-auth.tsx | 4 +- frontend/components/common/docs/api.tsx | 829 ++++----- .../components/common/docs/how-to-use.tsx | 463 +++-- .../common/docs/legal-page-layout.tsx | 18 +- frontend/components/common/docs/privacy.tsx | 291 ++- frontend/components/common/docs/terms.tsx | 294 ++- .../components/common/file-image-preview.tsx | 6 +- .../common/general/manage-pannel.tsx | 58 +- .../common/general/password-dialog.tsx | 18 +- frontend/components/common/home/home-main.tsx | 56 +- .../common/settings/auth-source-modal.tsx | 34 +- .../components/common/settings/info-tab.tsx | 62 +- .../common/settings/operation-tab.tsx | 20 +- .../components/common/settings/other-tab.tsx | 18 +- .../components/common/settings/profile.tsx | 183 +- .../common/settings/security-tab.tsx | 140 +- .../components/common/settings/system-tab.tsx | 113 +- .../components/common/settings/templates.tsx | 65 +- .../components/common/user/file-manager.tsx | 52 +- .../components/home/developer-section.tsx | 30 +- frontend/components/home/footer-section.tsx | 65 +- frontend/components/home/hero-section.tsx | 20 +- frontend/components/layout/empty.tsx | 20 +- frontend/components/layout/error.tsx | 21 +- frontend/components/layout/loading.tsx | 18 +- frontend/components/layout/search-dialog.tsx | 75 +- .../components/providers/title-updater.tsx | 12 +- frontend/contexts/admin-context.tsx | 10 +- frontend/contexts/admin-users-context.tsx | 21 +- frontend/contexts/user-context.tsx | 11 +- frontend/lib/services/core/config.ts | 7 +- frontend/messages/en.json | 1630 ++++++++++++++++- frontend/messages/zh-CN.json | 1626 +++++++++++++++- frontend/scripts/check-i18n-keys.mjs | 5 +- 75 files changed, 5971 insertions(+), 2450 deletions(-) diff --git a/frontend/.env.example b/frontend/.env.example index 1027682d..61c067ac 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -1,11 +1,16 @@ # 前端 URL WAVELET_FRONTEND_URL=http://localhost:3000 -# 后端 URL +# 后端 URL(服务端与 next.config rewrites 使用,不会暴露到浏览器) WAVELET_BACKEND_URL=http://localhost:8000 +# 浏览器侧 API base URL +# - 本地 `pnpm dev`:务必留空或不设置,请求走同源 `/api/*`,由 Next rewrites 反代到 WAVELET_BACKEND_URL +# - 静态导出 / 浏览器需直连后端(如部分 WebSocket 场景)时再设置,例如: +# NEXT_PUBLIC_WAVELET_BACKEND_URL=http://localhost:8000 + # Session Cookie WAVELET_SESSION_COOKIE_NAME=wavelet_session_id # 环境 -NODE_ENV=production \ No newline at end of file +NODE_ENV=development diff --git a/frontend/app/(docs)/docs/api/page.tsx b/frontend/app/(docs)/docs/api/page.tsx index 26797fef..ac4c3fac 100644 --- a/frontend/app/(docs)/docs/api/page.tsx +++ b/frontend/app/(docs)/docs/api/page.tsx @@ -1,17 +1,21 @@ +import { getTranslations } from 'next-intl/server'; import { LegalPageLayout } from '@/components/common/docs/legal-page-layout'; -import { apiSections, DOCS_LAST_UPDATED } from '@/components/common/docs/api'; +import { + getApiSections, + DOCS_LAST_UPDATED, +} from '@/components/common/docs/api'; + +export default async function ApiDocPage() { + const t = await getTranslations('docs'); -export default function ApiDocPage() { return ( - Wavelet 提供简单、强大的 API - 接口,支持多种编程语言和开发环境。通过标准化的 RESTful - 接口,您可以轻松地与后端服务进行集成与二次开发。 + {t('api.description')}

} /> diff --git a/frontend/app/(docs)/docs/how-to-use/page.tsx b/frontend/app/(docs)/docs/how-to-use/page.tsx index 94481331..3be6a4fa 100644 --- a/frontend/app/(docs)/docs/how-to-use/page.tsx +++ b/frontend/app/(docs)/docs/how-to-use/page.tsx @@ -1,17 +1,19 @@ +import { getTranslations } from 'next-intl/server'; import { LegalPageLayout } from '@/components/common/docs/legal-page-layout'; -import { howToUseSections } from '@/components/common/docs/how-to-use'; +import { getHowToUseSections } from '@/components/common/docs/how-to-use'; import { DOCS_LAST_UPDATED } from '@/components/common/docs/api'; -export default function HowToUsePage() { +export default async function HowToUsePage() { + const t = await getTranslations('docs'); + return ( - 欢迎使用 Wavelet - 脚手架系统。本文档将引导您快速了解平台的核心组件、角色权限设计及二次开发流程,帮助您快速上手开发。 + {t('howToUse.description')}

} /> diff --git a/frontend/app/(docs)/docs/privacy-policy/page.tsx b/frontend/app/(docs)/docs/privacy-policy/page.tsx index 195ed2b8..4b5c69df 100644 --- a/frontend/app/(docs)/docs/privacy-policy/page.tsx +++ b/frontend/app/(docs)/docs/privacy-policy/page.tsx @@ -1,18 +1,21 @@ +import { getTranslations } from 'next-intl/server'; import { LegalPageLayout } from '@/components/common/docs/legal-page-layout'; -import { privacySections } from '@/components/common/docs/privacy'; +import { getPrivacySections } from '@/components/common/docs/privacy'; import { TERMS_LAST_UPDATED } from '@/components/common/docs/terms'; -export default function PrivacyPolicyPage() { +export default async function PrivacyPolicyPage() { + const t = await getTranslations('docs'); + return ( - 我们非常重视您的隐私保护。本政策将向您透明地展示我们如何收集、使用、存储您的个人信息, + {t('privacy.description')}
- 以及您享有的相关权利。 + {t('privacy.descriptionRights')} } /> diff --git a/frontend/app/(docs)/docs/terms-of-service/page.tsx b/frontend/app/(docs)/docs/terms-of-service/page.tsx index ecb24e91..cc2e1f63 100644 --- a/frontend/app/(docs)/docs/terms-of-service/page.tsx +++ b/frontend/app/(docs)/docs/terms-of-service/page.tsx @@ -1,16 +1,19 @@ +import { getTranslations } from 'next-intl/server'; import { LegalPageLayout } from '@/components/common/docs/legal-page-layout'; import { TERMS_LAST_UPDATED, - termsSections, + getTermsSections, } from '@/components/common/docs/terms'; -export default function TermsOfServicePage() { +export default async function TermsOfServicePage() { + const t = await getTranslations('docs'); + return ( 为了保障您的合法权益,请您仔细阅读以下条款。} + sections={getTermsSections(t)} + description={{t('terms.description')}} /> ); } diff --git a/frontend/app/(main)/admin/database/components/cache-manager.tsx b/frontend/app/(main)/admin/database/components/cache-manager.tsx index ec09a178..5f2d5650 100644 --- a/frontend/app/(main)/admin/database/components/cache-manager.tsx +++ b/frontend/app/(main)/admin/database/components/cache-manager.tsx @@ -4,6 +4,7 @@ import * as React from 'react'; import { useCallback, useEffect, useState } from 'react'; import { toast } from 'sonner'; import { HardDrive, RefreshCw, Trash2 } from 'lucide-react'; +import { useTranslations } from 'next-intl'; import { Card, @@ -52,6 +53,7 @@ interface CacheManagerProps { } export function CacheManager({ refreshTrigger }: CacheManagerProps) { + const t = useTranslations('admin.database.cache'); const [cacheStatus, setCacheStatus] = useState(null); const [loadingCache, setLoadingCache] = useState(true); const [savingConfig, setSavingConfig] = useState(false); @@ -64,22 +66,25 @@ export function CacheManager({ refreshTrigger }: CacheManagerProps) { const [lruEnabled, setLruEnabled] = useState(true); // 获取磁盘缓存状态 - const fetchCacheStatus = useCallback(async (isSilent = false) => { - if (!isSilent) setLoadingCache(true); - try { - const data = await services.adminCache.getCacheStatus(); - setCacheStatus(data); - setMaxSizeMB(data.max_size_mb.toString()); - setTtlMinutes(data.ttl_minutes.toString()); - setLruEnabled(data.lru_enabled); - } catch (err) { - toast.error('获取磁盘缓存状态失败', { - description: err instanceof Error ? err.message : '未知错误', - }); - } finally { - setLoadingCache(false); - } - }, []); + const fetchCacheStatus = useCallback( + async (isSilent = false) => { + if (!isSilent) setLoadingCache(true); + try { + const data = await services.adminCache.getCacheStatus(); + setCacheStatus(data); + setMaxSizeMB(data.max_size_mb.toString()); + setTtlMinutes(data.ttl_minutes.toString()); + setLruEnabled(data.lru_enabled); + } catch (err) { + toast.error(t('fetchCacheStatusFailed'), { + description: err instanceof Error ? err.message : '未知错误', + }); + } finally { + setLoadingCache(false); + } + }, + [t], + ); // 保存配置 const handleSaveConfig = async (e: React.FormEvent) => { @@ -87,14 +92,14 @@ export function CacheManager({ refreshTrigger }: CacheManagerProps) { const size = parseInt(maxSizeMB, 10); const ttl = parseInt(ttlMinutes, 10); if (isNaN(size) || size < 1) { - toast.error('保存失败', { - description: '最大容量限制必须是大于等于 1 的整数', + toast.error(t('saveFailed'), { + description: t('maxSizeMustBePositive'), }); return; } if (isNaN(ttl) || ttl < 0) { - toast.error('保存失败', { - description: '默认过期时间必须是大于等于 0 的整数', + toast.error(t('saveFailed'), { + description: t('ttlMustBeNonNegative'), }); return; } @@ -105,10 +110,12 @@ export function CacheManager({ refreshTrigger }: CacheManagerProps) { ttl_minutes: ttl, lru_enabled: lruEnabled, }); - toast.success('保存成功', { description: '磁盘缓存策略已热更新' }); + toast.success(t('saveSuccess'), { + description: t('cachePolicyHotUpdated'), + }); await fetchCacheStatus(true); } catch (err) { - toast.error('保存配置失败', { + toast.error(t('saveConfigFailed'), { description: err instanceof Error ? err.message : '未知错误', }); } finally { @@ -121,11 +128,11 @@ export function CacheManager({ refreshTrigger }: CacheManagerProps) { setClearingCache(true); try { await services.adminCache.clearCache(); - toast.success('清空成功', { description: '缓存数据已全部清除' }); + toast.success(t('clearSuccess'), { description: t('cacheDataCleared') }); setShowClearConfirm(false); await fetchCacheStatus(true); } catch (err) { - toast.error('清空缓存失败', { + toast.error(t('clearCacheFailed'), { description: err instanceof Error ? err.message : '未知错误', }); } finally { @@ -149,10 +156,12 @@ export function CacheManager({ refreshTrigger }: CacheManagerProps) {
- 缓存管理 + + {t('cacheManagement')} +
- 管理和监控系统级磁盘缓存的资源占用、生命周期及淘汰策略 + {t('cacheManagementDesc')}
@@ -166,7 +175,7 @@ export function CacheManager({ refreshTrigger }: CacheManagerProps) { - 刷新状态 + {t('refreshStatus')} @@ -181,13 +190,13 @@ export function CacheManager({ refreshTrigger }: CacheManagerProps) { {/* 左边:状态区 (2/5 cols) */}

- 运行状态 + {t('runtimeStatus')}

{/* 已占空间 */}

- 已用空间 + {t('usedSpace')}

{formatBytes(cacheStatus.total_size)} @@ -196,12 +205,12 @@ export function CacheManager({ refreshTrigger }: CacheManagerProps) { {/* Key数量 */}

- 缓存键数量 + {t('cacheKeysCount')}

{formatNumber(cacheStatus.keys_count)}{' '} - 个文件 + {t('files')}

@@ -210,7 +219,7 @@ export function CacheManager({ refreshTrigger }: CacheManagerProps) { {/* 存储路径 */}

- 缓存基准目录 + {t('cacheBaseDirectory')}

setShowClearConfirm(true)} > - 立即清空缓存 + {t('clearCacheNow')}
{/* 右边:配置区 (3/5 cols) */}

- 策略配置 + {t('policyConfig')}

- 当总大小超出该值时,自动触发淘汰算法 + {t('maxSizeHint')}

- 缓存项的最长存活时间,超期后将失效被清理 + {t('ttlHint')}

@@ -281,10 +290,10 @@ export function CacheManager({ refreshTrigger }: CacheManagerProps) { htmlFor='lruEnabled' className='text-xs font-semibold block cursor-pointer' > - 启用 LRU 淘汰机制 + {t('enableLru')} - 在到达最大容量限制时,自动移除最久未被访问的缓存项。关闭该功能仅清理过期项。 + {t('enableLruDesc')}
- {savingConfig ? '正在保存...' : '保存设置'} + {savingConfig ? t('saving') : t('saveSettings')}
@@ -309,7 +318,7 @@ export function CacheManager({ refreshTrigger }: CacheManagerProps) { ) : (
- 未加载到缓存状态信息 + {t('noCacheStatus')}
)} @@ -320,11 +329,10 @@ export function CacheManager({ refreshTrigger }: CacheManagerProps) { - 确认清空所有缓存? + {t('confirmClearAllCache')} - 该操作将彻底清空磁盘目录下的全部缓存文件(包含临时解压、处理后的图片及各种块文件),重置键数量统计为 - 0。此操作不可撤销,且可能导致用户拉取资源时出现一过性的响应变慢。 + {t('confirmClearDesc')} @@ -335,7 +343,7 @@ export function CacheManager({ refreshTrigger }: CacheManagerProps) { onClick={() => setShowClearConfirm(false)} disabled={clearingCache} > - 取消 + {t('cancel')} diff --git a/frontend/app/(main)/admin/database/components/sql-console.tsx b/frontend/app/(main)/admin/database/components/sql-console.tsx index 97e6be68..7aa2f728 100644 --- a/frontend/app/(main)/admin/database/components/sql-console.tsx +++ b/frontend/app/(main)/admin/database/components/sql-console.tsx @@ -7,6 +7,7 @@ import CodeMirror from '@uiw/react-codemirror'; import { sql } from '@codemirror/lang-sql'; import { toast } from 'sonner'; import { ArrowLeft, Play, RefreshCw, Terminal, Trash2 } from 'lucide-react'; +import { useTranslations } from 'next-intl'; import { Button } from '@/components/ui/button'; import { @@ -49,6 +50,7 @@ interface SQLConsoleProps { } export function SQLConsole({ dbType, onClose }: SQLConsoleProps) { + const t = useTranslations('admin.database.sql'); // SQL 控制台状态 const [sqlQuery, setSqlQuery] = useState(''); const [executingSQL, setExecutingSQL] = useState(false); @@ -97,10 +99,10 @@ export function SQLConsole({ dbType, onClose }: SQLConsoleProps) { try { const result = await DbManageService.executeSQL(sqlQuery); setSqlResult(result); - toast.success('SQL 执行成功'); + toast.success(t('sqlExecSuccess')); } catch (err) { setSqlError(err instanceof Error ? err.message : '未知执行错误'); - toast.error('SQL 执行失败'); + toast.error(t('sqlExecFailed')); } finally { setExecutingSQL(false); } @@ -134,7 +136,7 @@ export function SQLConsole({ dbType, onClose }: SQLConsoleProps) {

- SQL 查询终端 + {t('sqlQueryTerminal')}

@@ -153,34 +155,34 @@ export function SQLConsole({ dbType, onClose }: SQLConsoleProps) {
- SQL 编辑器 + {t('sqlEditor')}
{/* 快速模板 */}
- 快速模板: + {t('quickTemplates')} @@ -204,7 +206,7 @@ export function SQLConsole({ dbType, onClose }: SQLConsoleProps) { disabled={executingSQL || !sqlQuery.trim()} > - {executingSQL ? '运行中...' : '运行'} + {executingSQL ? t('running') : t('run')}
@@ -245,10 +247,10 @@ export function SQLConsole({ dbType, onClose }: SQLConsoleProps) {
{/* 结果栏工具提示 */}
- 执行输出 + {t('executionOutput')} {sqlResult && ( - 类型: {sqlResult.type.toUpperCase()} | 耗时:{' '} + {t('type')}: {sqlResult.type.toUpperCase()} | {t('duration')}:{' '} {sqlResult.execution_time_ms} ms )} @@ -260,7 +262,7 @@ export function SQLConsole({ dbType, onClose }: SQLConsoleProps) {
- 正在在数据库执行查询,请稍候... + {t('executingQuery')}
)} @@ -319,7 +321,7 @@ export function SQLConsole({ dbType, onClose }: SQLConsoleProps) { colSpan={sqlResult.columns.length} className='text-center py-10 text-muted-foreground' > - 查询结果为空 + {t('queryResultEmpty')} )} @@ -331,11 +333,13 @@ export function SQLConsole({ dbType, onClose }: SQLConsoleProps) { {sqlResult.type === 'exec' && (

- SQL 执行成功 + {t('sqlExecSuccessTitle')}

- 受影响行数: {sqlResult.affected_rows} 行,耗时{' '} - {sqlResult.execution_time_ms} 毫秒。 + {t('affectedRows', { + rows: sqlResult.affected_rows, + ms: sqlResult.execution_time_ms, + })}

)} @@ -345,9 +349,7 @@ export function SQLConsole({ dbType, onClose }: SQLConsoleProps) { {!executingSQL && !sqlResult && !sqlError && (
- - 编辑器就绪,请在上方编写 SQL 并运行查看结果 - + {t('editorReady')}
)}
diff --git a/frontend/app/(main)/admin/database/components/table-browser.tsx b/frontend/app/(main)/admin/database/components/table-browser.tsx index fe63a2d9..d5101fc7 100644 --- a/frontend/app/(main)/admin/database/components/table-browser.tsx +++ b/frontend/app/(main)/admin/database/components/table-browser.tsx @@ -4,6 +4,7 @@ import * as React from 'react'; import { useCallback, useEffect, useState } from 'react'; import { toast } from 'sonner'; import { Layers, RefreshCw } from 'lucide-react'; +import { useTranslations } from 'next-intl'; import { Card, @@ -67,6 +68,7 @@ export function TableBrowser({ loadingTables, refreshTrigger, }: TableBrowserProps) { + const t = useTranslations('admin.database.tableBrowser'); const [selectedTable, setSelectedTable] = useState(''); const [tableData, setTableData] = useState(null); const [page, setPage] = useState(1); @@ -125,9 +127,11 @@ export function TableBrowser({
- 数据表浏览器 + + {t('tableBrowser')} + - 浏览数据库中的物理数据表详情及内容 + {t('tableBrowserDesc')}
@@ -137,7 +141,7 @@ export function TableBrowser({ ) : ( setFormData((prev) => ({ ...prev, name: e.target.value })) } - placeholder='输入要创建的任务描述...' + placeholder={t('taskNamePlaceholder')} className='h-8 text-xs shadow-none bg-background focus-visible:ring-1' />

- 任务名称将用于后台执行记录的可读性显示 + {t('taskNameHint')}

{/* 下拉框 */}
@@ -107,10 +109,10 @@ export function ControlsTab() { htmlFor='allow-notify' className='text-xs font-medium text-foreground cursor-pointer select-none leading-none' > - 允许触发异步通知推送 + {t('allowNotify')}

- 勾选后,在当前后台任务顺利执行结束时,会关联派发多渠道通知推送。 + {t('allowNotifyDesc')}

@@ -118,32 +120,32 @@ export function ControlsTab() { {/* 按钮变体 */}
- 按钮样式对齐: + {t('buttonStyles')}
@@ -155,13 +157,13 @@ export function ControlsTab() { - 表单设计规范 + {t('formDesignSpec')}

- 1. 统一控件高度与投影: + {t('spec.unifiedHeight')}

输入框 ( @@ -188,7 +190,7 @@ export function ControlsTab() {

- 2. Checkbox 与文本的对齐: + {t('spec.checkboxAlignment')}

Checkbox 应在左侧垂直偏上对齐,使用容器{' '} @@ -203,7 +205,7 @@ export function ControlsTab() {

- 3. 按钮使用策略: + {t('spec.buttonStrategy')}

普通主表单及新增/保存按钮,一律使用默认 Primary 按钮或次要{' '} diff --git a/frontend/app/(main)/admin/demo/tabs/dashboard-tab.tsx b/frontend/app/(main)/admin/demo/tabs/dashboard-tab.tsx index fcc81455..9837d581 100644 --- a/frontend/app/(main)/admin/demo/tabs/dashboard-tab.tsx +++ b/frontend/app/(main)/admin/demo/tabs/dashboard-tab.tsx @@ -4,11 +4,13 @@ 'use client'; import * as React from 'react'; +import { useTranslations } from 'next-intl'; import { Code, Cpu, Database, HardDrive } from 'lucide-react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Progress } from '@/components/ui/progress'; export function DashboardTab() { + const t = useTranslations('admin.demo.dashboard'); return (

@@ -16,7 +18,7 @@ export function DashboardTab() { - 容器 CPU 使用率 + {t('cpuUsage')} @@ -25,7 +27,7 @@ export function DashboardTab() {

- 容器运行正常,负载平稳 + {t('cpuStatus')}

@@ -34,7 +36,7 @@ export function DashboardTab() { - 存储已用容量 + {t('storageUsage')} @@ -45,7 +47,7 @@ export function DashboardTab() {

- 数据已同步远端 S3 存储桶,支持自动过期 + {t('storageStatus')}

@@ -54,7 +56,7 @@ export function DashboardTab() { - 物理数据库 + {t('database')} @@ -67,17 +69,15 @@ export function DashboardTab() {
- 活跃连接:{' '} + {t('activeConnections')}{' '} 12
- 总表数量:{' '} + {t('totalTables')}{' '} 34
-

- 链接池自动维护中,最大连接限制为 100 -

+

{t('dbStatus')}

@@ -87,45 +87,45 @@ export function DashboardTab() { - 指标卡片 & 仪表盘设计规范 + {t('dashboardDesignSpec')}
  • - 指标卡片统一采用{' '} + {t('spec.dashedBorder')} border-dashed shadow-none {' '} - 作为边框和投影。 + {t('spec.dashedBorder2')}
  • - 头部使用{' '} + {t('spec.headerLayout')} flex flex-row items-center justify-between pb-2 {' '} - 布局,左侧为标题小字,右侧为直接呈现的 Lucide 图标。 + {t('spec.headerLayout2')}
  • - 数值强调使用且仅使用{' '} + {t('spec.valueStyle')} text-2xl font-semibold tracking-tight {' '} - 展示。 + {t('spec.valueStyle2')}
  • - 进度条使用{' '} + {t('spec.progressBar')} h-1.5 {' '} - 细条高度,不喧宾夺主。 + {t('spec.progressBar2')}
  • - 底部的次要说明信息必须统一使用{' '} + {t('spec.secondaryText')} text-[10px] text-muted-foreground {' '} - 等辅助类。 + {t('spec.secondaryText2')}
diff --git a/frontend/app/(main)/admin/demo/tabs/table-tab.tsx b/frontend/app/(main)/admin/demo/tabs/table-tab.tsx
index f5a60c16..29f71b56 100644
--- a/frontend/app/(main)/admin/demo/tabs/table-tab.tsx
+++ b/frontend/app/(main)/admin/demo/tabs/table-tab.tsx
@@ -4,6 +4,7 @@
 'use client';
 
 import * as React from 'react';
+import { useTranslations } from 'next-intl';
 import { Code, Edit, ListFilter, Plus, Search, Trash2 } from 'lucide-react';
 import { Button } from '@/components/ui/button';
 import { Input } from '@/components/ui/input';
@@ -19,27 +20,28 @@ import { Badge } from '@/components/ui/badge';
 import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
 
 export function TableTab() {
+  const t = useTranslations('admin.demo.table');
   const mockTableData = [
     {
       id: 'TX-1002',
-      name: '用户信息批量同步导出',
-      type: '数据同步',
+      name: t('mockData.syncExport'),
+      type: t('mockType.dataSync'),
       status: 'success',
       duration: '1.2s',
       time: '2026-06-15 14:22',
     },
     {
       id: 'TX-1003',
-      name: '系统数据库每周自动备份归档',
-      type: '数据库运维',
+      name: t('mockData.dbBackup'),
+      type: t('mockType.dbOps'),
       status: 'running',
-      duration: '在执行中',
+      duration: t('mockDuration.running'),
       time: '2026-06-15 15:00',
     },
     {
       id: 'TX-1004',
-      name: '远端S3过期缓存对象自动清理任务',
-      type: '磁盘清理',
+      name: t('mockData.s3Cleanup'),
+      type: t('mockType.diskCleanup'),
       status: 'failed',
       duration: '0.4s',
       time: '2026-06-15 10:05',
@@ -54,7 +56,7 @@ export function TableTab() {
           
@@ -64,12 +66,12 @@ export function TableTab() { className='h-8 border-dashed text-xs shadow-none' > - 过滤 + {t('filter')} @@ -79,25 +81,25 @@ export function TableTab() { - 任务编号 + {t('columns.taskId')} - 执行任务名称 + {t('columns.taskName')} - 任务类型 + {t('columns.taskType')} - 状态 + {t('columns.status')} - 执行耗时 + {t('columns.duration')} - 触发时间 + {t('columns.triggerTime')} - 操作 + {t('columns.actions')} @@ -123,7 +125,7 @@ export function TableTab() { className='text-[10px] bg-emerald-500/10 border-emerald-500/20 text-emerald-600 rounded-full py-0 px-2 font-medium' > - 成功 + {t('status.success')} )} {row.status === 'running' && ( @@ -132,7 +134,7 @@ export function TableTab() { className='text-[10px] bg-blue-500/10 border-blue-500/20 text-blue-600 rounded-full py-0 px-2 font-medium' > - 运行中 + {t('status.running')} )} {row.status === 'failed' && ( @@ -141,7 +143,7 @@ export function TableTab() { className='text-[10px] bg-destructive/10 border-destructive/20 text-destructive rounded-full py-0 px-2 font-medium' > - 失败 + {t('status.failed')} )} @@ -180,13 +182,13 @@ export function TableTab() { - 表格设计规范与代码模板 + {t('tableDesignSpec')}
  • - 表格容器必须统一使用带有虚线边框的容器,即类名: + {t('spec.container')} border border-dashed shadow-none rounded-lg overflow-hidden bg-background @@ -194,40 +196,40 @@ export function TableTab() { 。
  • - 表头背景设为{' '} + {t('spec.headerBg')} bg-muted/40 - 。表头表项字号统一为小字粗体{' '} + {t('spec.headerStyle')} text-xs font-semibold 。
  • - 单元格行与列之间使用虚线分割( + {t('spec.cellDivider')} border-dashed - )。表格行悬浮交互采用温和过渡色{' '} + {t('spec.rowHover')} hover:bg-muted/10 transition-colors 。
  • - 编码、主键或唯一 ID 类字段一律采用等宽字体呈现: + {t('spec.monoFont')} font-mono text-xs 。
  • - 状态标签(Badge)尽量圆角化( + {t('spec.badgeStyle')} rounded-full py-0 px-2 - ),并以浅背景配以各自对应的图标圆点,以实现现代、高对比度的视觉质感。 + {t('spec.badgeStyle2')}
diff --git a/frontend/app/(main)/admin/files/components/file-list.tsx b/frontend/app/(main)/admin/files/components/file-list.tsx
index eea4f8a3..134cc3bc 100644
--- a/frontend/app/(main)/admin/files/components/file-list.tsx
+++ b/frontend/app/(main)/admin/files/components/file-list.tsx
@@ -1,6 +1,7 @@
 'use client';
 
 import * as React from 'react';
+import { useTranslations } from 'next-intl';
 import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
 import { AnimatePresence, motion } from 'motion/react';
 import {
@@ -81,6 +82,7 @@ function formatDate(dateStr: string) {
 }
 
 export function FileList() {
+  const t = useTranslations('admin.files');
   const queryClient = useQueryClient();
   const [keyword, setKeyword] = React.useState('');
   const [debouncedKeyword, setDebouncedKeyword] = React.useState('');
@@ -157,10 +159,10 @@ export function FileList() {
     onSuccess: () => {
       void queryClient.invalidateQueries({ queryKey: ['files', 'my'] });
       void queryClient.invalidateQueries({ queryKey: ['files', 'stats'] });
-      toast.success('文件已删除');
+      toast.success(t('list.deleteSuccess'));
       setDeleteTarget(null);
     },
-    onError: (err: Error) => toast.error(err.message || '删除失败'),
+    onError: (err: Error) => toast.error(err.message || t('list.deleteFailed')),
   });
 
   // 批量 ZIP 下载
@@ -173,9 +175,9 @@ export function FileList() {
       a.download = 'batch_download.zip';
       a.click();
       URL.revokeObjectURL(url);
-      toast.success('批量下载已开始');
+      toast.success(t('list.batchDownloadSuccess'));
     },
-    onError: () => toast.error('批量下载失败'),
+    onError: () => toast.error(t('list.batchDownloadFailed')),
   });
 
   const toggleSelect = (id: string) => {
@@ -209,7 +211,7 @@ export function FileList() {
         
setKeyword(e.target.value)} @@ -232,7 +234,9 @@ export function FileList() { selectedIds.size === files.length ? clearSelection : selectAll } > - {selectedIds.size === files.length ? '取消全选' : '全选本页'} + {selectedIds.size === files.length + ? t('list.deselectAll') + : t('list.selectAll')} )} @@ -247,7 +251,7 @@ export function FileList() { className='flex items-center gap-2' > - 已选 {selectedIds.size} 个 + {t('list.selectedCount', { count: selectedIds.size })}
@@ -292,7 +296,7 @@ export function FileList() {

- {debouncedKeyword ? '没有匹配的文件' : '您还没有上传任何文件'} + {debouncedKeyword ? t('list.noMatch') : t('list.noFiles')}

) : ( @@ -309,18 +313,28 @@ export function FileList() { onCheckedChange={handleSelectAll} /> - 预览 - ID - 文件名 - - 路径 + + {t('list.colPreview')} + + ID + {t('list.colName')} + + {t('list.colPath')} + + + {t('list.colType')} + + + {t('list.colMime')} + + + {t('list.colSize')} + + + {t('list.colUploadedAt')} - 业务类别 - MIME类型 - 大小 - 上传时间 - 操作 + {t('list.colActions')} @@ -396,7 +410,7 @@ export function FileList() { size='icon' variant='ghost' className='h-7 w-7 rounded-md hover:bg-muted text-muted-foreground hover:text-foreground' - title='查看详情' + title={t('list.viewDetail')} onClick={() => setDetailTarget(file)} > @@ -405,7 +419,7 @@ export function FileList() { size='icon' variant='ghost' className='h-7 w-7 rounded-md hover:bg-muted' - title='下载' + title={t('list.download')} onClick={() => handleDownload(file)} > @@ -414,7 +428,7 @@ export function FileList() { size='icon' variant='ghost' className='h-7 w-7 rounded-md hover:bg-destructive/10 text-muted-foreground hover:text-destructive' - title='删除' + title={t('list.delete')} onClick={() => setDeleteTarget(file)} > @@ -439,7 +453,7 @@ export function FileList() { disabled={page <= 1} onClick={() => setPage((p) => p - 1)} > - 上一页 + {t('list.prevPage')} {page} / {totalPages} @@ -451,7 +465,7 @@ export function FileList() { disabled={page >= totalPages} onClick={() => setPage((p) => p + 1)} > - 下一页 + {t('list.nextPage')} )} @@ -463,10 +477,8 @@ export function FileList() { > - 文件详情 - - 查看文件的完整元数据与配置属性。 - + {t('list.detailTitle')} + {t('list.detailDesc')} {detailTarget && ( @@ -495,7 +507,7 @@ export function FileList() {
- 文件 ID + {t('list.detailId')} {detailTarget.id} @@ -504,7 +516,7 @@ export function FileList() {
- 文件名 + {t('list.detailName')} {detailTarget.file_name} @@ -513,7 +525,7 @@ export function FileList() {
- 存储路径 + {t('list.detailPath')} {detailTarget.file_path} @@ -522,17 +534,17 @@ export function FileList() {
- 大小 + {t('list.detailSize')} {formatFileSize(detailTarget.file_size)} ( - {detailTarget.file_size.toLocaleString()} 字节) + {detailTarget.file_size.toLocaleString()} {t('list.bytes')})
- MIME 类型 + {t('list.detailMime')} {detailTarget.mime_type} @@ -541,7 +553,7 @@ export function FileList() {
- 文件后缀 + {t('list.detailExtension')} {detailTarget.extension} @@ -559,7 +571,7 @@ export function FileList() {
- 业务类型 + {t('list.detailBizType')} - 存储驱动 + {t('list.detailDriver')} {storageDriverQuery.data ?? 'local'} @@ -582,7 +594,7 @@ export function FileList() {
- 上传者 ID + {t('list.detailUploaderId')} {detailTarget.user_id} @@ -591,7 +603,7 @@ export function FileList() {
- 上传时间 + {t('list.detailUploadedAt')} {formatDate(detailTarget.created_at)} @@ -602,7 +614,7 @@ export function FileList() { Object.keys(detailTarget.metadata).length > 0 && (
- 额外元数据 + {t('list.detailMetadata')}
                         {JSON.stringify(detailTarget.metadata, null, 2)}
@@ -622,18 +634,14 @@ export function FileList() {
       >
         
           
-            确认删除文件
+            {t('list.deleteTitle')}
             
-              确定要删除文件{' '}
-              
-                「{deleteTarget?.file_name}」
-              {' '}
-              吗?此操作不可撤销。
+              {t('list.deleteConfirm', { name: deleteTarget?.file_name || '' })}
             
           
           
             
-              取消
+              {t('list.cancel')}
             
             
@@ -645,7 +653,7 @@ export function FileList() {
               {deleteMutation.isPending && (
                 
               )}
-              确认删除
+              {t('list.confirmDelete')}
             
           
         
diff --git a/frontend/app/(main)/admin/files/components/file-stats.tsx b/frontend/app/(main)/admin/files/components/file-stats.tsx
index ab608bf4..6000c6a0 100644
--- a/frontend/app/(main)/admin/files/components/file-stats.tsx
+++ b/frontend/app/(main)/admin/files/components/file-stats.tsx
@@ -1,6 +1,7 @@
 'use client';
 
 import * as React from 'react';
+import { useTranslations } from 'next-intl';
 import { useQuery } from '@tanstack/react-query';
 import {
   Database,
@@ -51,40 +52,41 @@ const categoryMap: Record = {
 
 const statsChartConfig = {
   images: {
-    label: '图片',
+    label: 'images',
     color: 'hsl(var(--chart-1))',
   },
   videos: {
-    label: '视频',
+    label: 'videos',
     color: 'hsl(var(--chart-2))',
   },
   audio: {
-    label: '音频',
+    label: 'audio',
     color: 'hsl(var(--chart-3))',
   },
   documents: {
-    label: '文档',
+    label: 'documents',
     color: 'hsl(var(--chart-4))',
   },
   archives: {
-    label: '压缩包',
+    label: 'archives',
     color: 'hsl(var(--chart-5))',
   },
   others: {
-    label: '其他',
+    label: 'others',
     color: 'hsl(var(--chart-6))',
   },
   count: {
-    label: '新增文件数',
+    label: 'count',
     color: 'hsl(var(--primary))',
   },
   size: {
-    label: '新增大小',
+    label: 'size',
     color: 'hsl(var(--chart-2))',
   },
 } satisfies ChartConfig;
 
 export function FileStats() {
+  const t = useTranslations('admin.files');
   const [trendMetric, setTrendMetric] = React.useState<'count' | 'size'>(
     'count',
   );
@@ -182,9 +184,9 @@ export function FileStats() {
       
-

暂无上传数据

+

{t('stats.noData')}

- 上传文件后即可在此查看容量与文件分布分析。 + {t('stats.noDataDesc')}

@@ -198,7 +200,7 @@ export function FileStats() { - 总文件数 + {t('stats.totalFiles')} @@ -206,11 +208,11 @@ export function FileStats() {
{stats.total_count.toLocaleString()}{' '} - 个 + {t('stats.countUnit')}

- 您上传的可用文件数量汇总 + {t('stats.totalFilesDesc')}

@@ -218,7 +220,7 @@ export function FileStats() { - 占用存储容量 + {t('stats.totalSize')} @@ -227,7 +229,7 @@ export function FileStats() { {formatFileSize(stats.total_size)}

- 文件实际在磁盘/S3中占用的总空间 + {t('stats.totalSizeDesc')}

@@ -235,7 +237,7 @@ export function FileStats() { - 近 7 天新增文件 + {t('stats.weeklyNewFiles')} @@ -243,11 +245,11 @@ export function FileStats() {
+{trendSummary.count.toLocaleString()}{' '} - 个 + {t('stats.countUnit')}

- 最近 7 天上传成功的文件总数 + {t('stats.weeklyNewFilesDesc')}

@@ -255,7 +257,7 @@ export function FileStats() { - 近 7 天新增大小 + {t('stats.weeklyNewSize')} @@ -264,7 +266,7 @@ export function FileStats() { +{formatFileSize(trendSummary.size)}

- 最近 7 天上传所消耗的存储带宽 + {t('stats.weeklyNewSizeDesc')}

@@ -275,10 +277,10 @@ export function FileStats() {
- 最近 7 天新增趋势 + {t('stats.trendTitle')} - 按日统计近一周期内的上传波动 + {t('stats.trendDesc')}
@@ -288,7 +290,7 @@ export function FileStats() { className='h-7 text-xs px-2.5 rounded-md' onClick={() => setTrendMetric('count')} > - 新增数量 (个) + {t('stats.trendCount')}
@@ -361,7 +363,9 @@ export function FileStats() { hideLabel formatter={(value) => { const label = - trendMetric === 'count' ? '新增文件数' : '新增大小'; + trendMetric === 'count' + ? t('stats.chartLabelCount') + : t('stats.chartLabelSize'); const formattedValue = trendMetric === 'size' ? formatFileSize(Number(value)) @@ -419,16 +423,16 @@ export function FileStats() { - 文件格式数量分布 + {t('stats.countDistTitle')} - 各种格式类型的文件个数占比 + {t('stats.countDistDesc')} {categoryCountData.length === 0 ? (
- 暂无分类数据 + {t('stats.noCategoryData')}
) : (
@@ -494,15 +498,10 @@ export function FileStats() {

- 文件类型中数量最多的是{' '} - - 「{maxStats.maxCountName}」 - - ,累计上传了{' '} - - {maxStats.maxCount} - {' '} - 个文件。 + {t('stats.maxCountHint', { + name: maxStats.maxCountName, + count: maxStats.maxCount, + })}

@@ -512,16 +511,16 @@ export function FileStats() { - 文件占用容量分布 + {t('stats.sizeDistTitle')} - 各种格式类型的文件大小占比 + {t('stats.sizeDistDesc')} {categorySizeData.length === 0 ? (
- 暂无容量数据 + {t('stats.noSizeData')}
) : (
@@ -587,15 +586,10 @@ export function FileStats() {

- 文件类型中占用存储最大的是{' '} - - 「{maxStats.maxSizeName}」 - - ,共消耗了{' '} - - {formatFileSize(maxStats.maxSize)} - {' '} - 存储容量。 + {t('stats.maxSizeHint', { + name: maxStats.maxSizeName, + size: formatFileSize(maxStats.maxSize), + })}

diff --git a/frontend/app/(main)/admin/files/components/storage-config-tab.tsx b/frontend/app/(main)/admin/files/components/storage-config-tab.tsx index d6a1b741..62da26bc 100644 --- a/frontend/app/(main)/admin/files/components/storage-config-tab.tsx +++ b/frontend/app/(main)/admin/files/components/storage-config-tab.tsx @@ -1,6 +1,7 @@ 'use client'; import * as React from 'react'; +import { useTranslations } from 'next-intl'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { Database, Loader2, Play, Save } from 'lucide-react'; import { toast } from 'sonner'; @@ -44,13 +45,13 @@ import type { const storageConfigKey = 'storage_config'; const storageMigrationTaskType = 'storage_migration'; -const driverLabels: Record = { - local: '本地文件系统', - s3: 'AWS S3', - r2: 'Cloudflare R2', - minio: 'MinIO', - oss: '阿里云 OSS', - webdav: 'WebDAV', +const driverLabelKeys: Record = { + local: 'driverLocal', + s3: 'driverS3', + r2: 'driverR2', + minio: 'driverMinio', + oss: 'driverOss', + webdav: 'driverWebdav', }; const emptyObjectConfig: ObjectStorageConfig = { @@ -112,8 +113,8 @@ function parseMigrationPayload(payload: string): StorageConfig | undefined { } } -function driverLabel(driver?: StorageDriver): string { - return driver ? driverLabels[driver] : '未知'; +function driverLabelKey(driver?: StorageDriver): string { + return driver ? driverLabelKeys[driver] : 'driverUnknown'; } function latestMigration( @@ -136,6 +137,7 @@ function latestMigration( } export function StorageConfigTab() { + const t = useTranslations('admin.files'); const queryClient = useQueryClient(); const [config, setConfig] = React.useState(null); const query = useQuery({ @@ -186,13 +188,13 @@ export function StorageConfigTab() { }); }, onSuccess: () => { - toast.success('存储配置已保存'); + toast.success(t('storage.saveSuccess')); void queryClient.invalidateQueries({ queryKey: ['admin', 'storage-config'], }); }, onError: (error: Error) => { - toast.error(error.message || '保存存储配置失败'); + toast.error(error.message || t('storage.saveFailed')); void queryClient.invalidateQueries({ queryKey: ['admin', 'storage-config'], }); @@ -208,13 +210,13 @@ export function StorageConfigTab() { }); }, onSuccess: () => { - toast.success('存储迁移任务已下发'); + toast.success(t('storage.migrateSuccess')); void queryClient.invalidateQueries({ queryKey: ['admin', 'storage-config'], }); }, onError: (error: Error) => { - toast.error(error.message || '下发存储迁移任务失败'); + toast.error(error.message || t('storage.migrateFailed')); void queryClient.invalidateQueries({ queryKey: ['admin', 'storage-config'], }); @@ -224,12 +226,13 @@ export function StorageConfigTab() { mutationFn: (executionID: string) => services.adminTask.retryTaskExecution(executionID), onSuccess: () => { - toast.success('存储迁移任务已重新下发'); + toast.success(t('storage.retrySuccess')); void queryClient.invalidateQueries({ queryKey: ['admin', 'storage-config'], }); }, - onError: (error: Error) => toast.error(error.message || '运行存储迁移失败'), + onError: (error: Error) => + toast.error(error.message || t('storage.retryFailed')), }); if (query.isPending || !config) { @@ -269,7 +272,7 @@ export function StorageConfigTab() { - 存储维护模式 + {t('storage.maintenanceMode')} - {driverLabel(migration.source_driver)} →{' '} - {driverLabel(migration.target_driver)} - 。迁移期间文件只允许读取,禁止上传、删除和清理。 + {t('storage.maintenanceDesc', { + source: t(`storage.${driverLabelKey(migration.source_driver)}`), + target: t(`storage.${driverLabelKey(migration.target_driver)}`), + })} @@ -289,7 +293,9 @@ export function StorageConfigTab() { value={migration.state === 'succeeded' ? 100 : undefined} />

- 迁移进度请查看任务执行日志:{migration.task_id || '尚未下发'} + {t('storage.migrationProgress', { + taskId: migration.task_id || t('storage.notDispatched'), + })}

{migration.error && (

{migration.error}

@@ -310,7 +316,7 @@ export function StorageConfigTab() { ) : ( )} - 重试迁移 + {t('storage.retryMigration')}
@@ -324,10 +330,10 @@ export function StorageConfigTab() {
- 文件存储 + {t('storage.fileStorage')} - 默认使用本地存储。配置系统文件的存储媒介,切换存储类型且已有文件时,系统会自动进入维护模式并迁移文件。 + {t('storage.fileStorageDesc')}
@@ -337,7 +343,7 @@ export function StorageConfigTab() {
- 存储类型 + {t('storage.storageType')} setUsernameFilter(e.target.value)} className='h-8 text-xs' @@ -239,11 +239,11 @@ export function AccessLogs() {
setPathFilter(e.target.value)} className='h-8 text-xs' @@ -251,7 +251,7 @@ export function AccessLogs() {
@@ -295,26 +295,27 @@ export function AccessLogs() { {loading && logs.length === 0 ? ( ) : logs.length === 0 ? ( - + ) : (
- 请求方法 - 路径 - 用户 - IP - 状态 - 耗时 - 请求时间 - 详情 + {t('colMethod')} + {t('colPath')} + {t('colUser')} + {t('colIp')} + {t('colStatus')} + {t('colLatency')} + + {t('colRequestTime')} + + + {t('colDetail')} + @@ -334,7 +335,7 @@ export function AccessLogs() {
- {log.username || '未知用户'} + {log.username || t('unknownUser')} {log.nickname && ( @@ -381,7 +382,7 @@ export function AccessLogs() { {logs.length > 0 && (
- 共 {total} 条记录,当前第 {page}/{totalPages} 页 + {t('paginationInfo', { total, page, totalPages })}
@@ -412,7 +413,9 @@ export function AccessLogs() { - 访问日志详情 + + {t('accessLogDetail')} + 访问 ID: {selectedLog?.id} @@ -424,7 +427,7 @@ export function AccessLogs() {
- 请求方法 + {t('method')}
{selectedLog.method} @@ -432,7 +435,7 @@ export function AccessLogs() {
- 响应状态 + {t('responseStatus')}
@@ -442,7 +445,7 @@ export function AccessLogs() {
- 耗时 + {t('latency')}
{formatLatency(selectedLog.latency)} @@ -450,18 +453,18 @@ export function AccessLogs() {
- IP 地址 + {t('ipAddress')}
{selectedLog.ip}
- 用户 + {t('user')}
{selectedLog.username - ? `${selectedLog.username} (${selectedLog.nickname || '无昵称'})` - : '未知/游客'} + ? `${selectedLog.username} (${selectedLog.nickname || t('noNickname')})` + : t('unknownGuest')} ID: {selectedLog.user_id} @@ -472,7 +475,7 @@ export function AccessLogs() { {/* Path */}
{selectedLog.path} @@ -482,7 +485,7 @@ export function AccessLogs() { {/* Request Time */}
{formatDateTime(selectedLog.created_at)} @@ -542,7 +545,7 @@ export function AccessLogs() {
)} diff --git a/frontend/app/(main)/admin/logs/components/app-logs.tsx b/frontend/app/(main)/admin/logs/components/app-logs.tsx index bd51880a..2429166d 100644 --- a/frontend/app/(main)/admin/logs/components/app-logs.tsx +++ b/frontend/app/(main)/admin/logs/components/app-logs.tsx @@ -22,6 +22,7 @@ import { toast } from 'sonner'; import { ArrowDown, ChevronUp, Loader2 } from 'lucide-react'; import services from '@/lib/services'; +import { useTranslations } from 'next-intl'; import { ErrorInline } from '@/components/layout/error'; import { LoadingStateWithBorder } from '@/components/layout/loading'; import { Button } from '@/components/ui/button'; @@ -92,6 +93,7 @@ function parseLogLevel( } export function AppLogs() { + const t = useTranslations('admin.logs.appLogs'); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -300,12 +302,12 @@ export function AppLogs() { {loadingMore ? ( <> - 加载中... + {t('loading')} ) : ( <> - 加载更早日志 + {t('loadOlderLogs')} )} @@ -313,7 +315,9 @@ export function AppLogs() { )} {logs.length === 0 ? ( -
暂无日志
+
+ {t('noLogs')} +
) : (
- 回到最新 + {t('backToLatest')}
)} diff --git a/frontend/app/(main)/admin/logs/page-client.tsx b/frontend/app/(main)/admin/logs/page-client.tsx index 3f6a5a8f..5e65ffee 100644 --- a/frontend/app/(main)/admin/logs/page-client.tsx +++ b/frontend/app/(main)/admin/logs/page-client.tsx @@ -2,6 +2,7 @@ import dynamic from 'next/dynamic'; import { Activity, BarChart3, Terminal } from 'lucide-react'; +import { useTranslations } from 'next-intl'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; @@ -26,13 +27,16 @@ const AppLogs = dynamic( ); export function LogsPageClient() { + const t = useTranslations('admin.logs'); return (
{/* Header */}
-

系统日志

+

+ {t('pageTitle')} +

{/* Tabs Layout */} @@ -43,21 +47,21 @@ export function LogsPageClient() { className='px-0 pb-2 text-xs font-semibold flex items-center gap-1.5' > - 访问分析 + {t('accessAnalytics')} - 用户访问日志 + {t('userAccessLogs')} - 应用运行日志 + {t('appRuntimeLogs')} diff --git a/frontend/app/(main)/admin/push/components/events-tab.tsx b/frontend/app/(main)/admin/push/components/events-tab.tsx index 79f52e92..bbfad4d9 100644 --- a/frontend/app/(main)/admin/push/components/events-tab.tsx +++ b/frontend/app/(main)/admin/push/components/events-tab.tsx @@ -57,6 +57,8 @@ import { PopoverTrigger, } from '@/components/ui/popover'; +import { useTranslations } from 'next-intl'; + import { ErrorInline } from '@/components/layout/error'; import { LoadingStateWithBorder } from '@/components/layout/loading'; import { EmptyStateWithBorder } from '@/components/layout/empty'; @@ -70,6 +72,7 @@ import type { import { PushService } from '@/lib/services/push'; export function EventsTab() { + const t = useTranslations('admin.push.events'); const queryClient = useQueryClient(); // --- 获取所有自定义消息通道 --- @@ -107,12 +110,12 @@ export function EventsTab() { mutationFn: ({ id, data }: { id: number; data: UpdatePushEventRequest }) => PushService.updateEvent(id, data), onSuccess: () => { - toast.success('事件更新成功'); + toast.success(t('eventUpdateSuccess')); queryClient.invalidateQueries({ queryKey: ['admin', 'push-events'] }); setEditEventOpen(false); }, onError: (err: unknown) => { - toast.error('事件更新失败: ' + (err as Error).message); + toast.error(t('eventUpdateFailed') + ': ' + (err as Error).message); }, }); @@ -122,14 +125,14 @@ export function EventsTab() { queryClient.invalidateQueries({ queryKey: ['admin', 'push-events'] }); }, onError: (err: unknown) => { - toast.error('操作失败: ' + (err as Error).message); + toast.error(t('operationFailed') + ': ' + (err as Error).message); }, }); const createEventMutation = useMutation({ mutationFn: (data: CreatePushEventRequest) => PushService.createEvent(data), onSuccess: () => { - toast.success('事件创建成功'); + toast.success(t('eventCreateSuccess')); queryClient.invalidateQueries({ queryKey: ['admin', 'push-events'] }); setCreateEventOpen(false); setNewEventKey(''); @@ -139,18 +142,18 @@ export function EventsTab() { setNewEventEnabled(true); }, onError: (err: unknown) => { - toast.error('事件创建失败: ' + (err as Error).message); + toast.error(t('eventCreateFailed') + ': ' + (err as Error).message); }, }); const deleteEventMutation = useMutation({ mutationFn: (id: number) => PushService.deleteEvent(id), onSuccess: () => { - toast.success('配置删除成功'); + toast.success(t('configDeleteSuccess')); queryClient.invalidateQueries({ queryKey: ['admin', 'push-events'] }); }, onError: (err: unknown) => { - toast.error('配置删除失败: ' + (err as Error).message); + toast.error(t('configDeleteFailed') + ': ' + (err as Error).message); }, }); @@ -198,7 +201,7 @@ export function EventsTab() { try { JSON.parse(eventTemplate); } catch { - toast.error('消息模板不是合法的 JSON 格式'); + toast.error(t('templateInvalidJson')); return; } @@ -258,11 +261,11 @@ export function EventsTab() { const handleCreateEvent = () => { if (newEventType === 'builtin' && !newEventKey) { - toast.error('请选择系统事件'); + toast.error(t('selectSystemEvent')); return; } if (newEventType === 'task' && !newEventTaskType) { - toast.error('请选择异步任务'); + toast.error(t('selectAsyncTask')); return; } @@ -270,7 +273,7 @@ export function EventsTab() { try { JSON.parse(newEventTemplate); } catch { - toast.error('内容渲染模板不是合法的 JSON 格式'); + toast.error(t('templateInvalidJson')); return; } } @@ -295,11 +298,14 @@ export function EventsTab() {
{eventsQuery.isLoading ? ( - + ) : eventsQuery.isError ? (
) : (eventsQuery.data ?? []).length === 0 ? ( - + ) : (
@@ -322,19 +325,19 @@ export function EventsTab() { ID - 通知事件 + {t('colEvent')} - 关联渠道 + {t('colChannels')} - 推送目标 + {t('colTargets')} - 状态 + {t('colStatus')} - 操作 + {t('colActions')} @@ -362,7 +365,7 @@ export function EventsTab() { variant='outline' className='text-[8px] h-3.5 px-1 bg-blue-50/50 text-blue-600 border-blue-200' > - 任务 + {t('taskBadge')} )} @@ -375,7 +378,7 @@ export function EventsTab() {
{(event.channels ?? []).length === 0 ? ( - 未指定渠道 + {t('noChannelSpecified')} ) : ( event.channels.map((ch) => ( @@ -384,7 +387,7 @@ export function EventsTab() { variant='secondary' className='text-[10px] py-0 px-1.5 h-4.5' > - {ch === 'email' ? '邮件' : ch} + {ch === 'email' ? t('emailChannel') : ch} )) )} @@ -439,7 +442,7 @@ export function EventsTab() { - 配置 + {t('configure')} @@ -453,7 +456,7 @@ export function EventsTab() { className='h-6 w-6 text-muted-foreground hover:text-destructive hover:bg-destructive/10' disabled={deleteEventMutation.isPending} onClick={() => { - if (confirm('确定要删除该通知事件配置吗?')) { + if (confirm(t('deleteEventConfirm'))) { deleteEventMutation.mutate(event.id); } }} @@ -462,7 +465,7 @@ export function EventsTab() { - 删除 + {t('delete')} @@ -479,15 +482,13 @@ export function EventsTab() { - 新增通知事件 - - 选择系统内置的事件并绑定推送渠道,创建后即可开始生效 - + {t('addEventDialogTitle')} + {t('addEventDialogDesc')}
- +
{newEventType === 'builtin' ? (
- + {builtInEventsQuery.isLoading ? (
- 加载系统事件中... + {t('loadingSystemEvents')}
) : availableBuiltInEvents.length === 0 ? (
- 所有内置事件都已配置,没有可新增的事件。 + {t('allBuiltinEventsConfigured')}
) : ( - + {(taskTypesQuery.data ?? []).map((taskMeta) => ( @@ -593,11 +602,11 @@ export function EventsTab() { {newEventType === 'builtin' && newEventKey && (
- 事件说明: + {t('eventDescription')} {availableBuiltInEvents.find((e) => e.key === newEventKey) - ?.description || '无描述信息'} + ?.description || t('noDescription')}
)} @@ -605,18 +614,20 @@ export function EventsTab() { {newEventType === 'task' && newEventTaskType && (
- 任务说明: + {t('taskDescription')} {(taskTypesQuery.data ?? []).find( (t) => t.asynq_task === newEventTaskType, - )?.description || '无描述信息'} + )?.description || t('noDescription')}
)}
- +
)}
@@ -671,7 +682,7 @@ export function EventsTab() {
{newEventType === 'task' - ? '支持变量:{{task_name}}, {{task_status}}, {{task_duration}}, {{user.username}}' - : '支持变量:{{user.username}}, {{ip}}, {{time}}'} + ? t('taskTemplateVars') + : t('eventTemplateVars')}