From 5f5826ce568db02ded0124bfa9421bbe267db964 Mon Sep 17 00:00:00 2001 From: Wangnov Date: Wed, 10 Sep 2025 18:04:09 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=9F=BA=E7=A1=80=E6=9C=AC=E5=9C=B0?= =?UTF-8?q?=E5=8C=96=E6=94=AF=E6=8C=81=E4=B8=8E=E9=80=9A=E7=94=A8=E9=94=AE?= =?UTF-8?q?=E8=A1=A5=E5=85=85=EF=BC=88useConfirm/useChartConfig/format/api?= =?UTF-8?q?Stats=20=E5=9B=9E=E9=80=80=20+=20common.time/errors=20=E7=AD=89?= =?UTF-8?q?=20i18n=20=E9=94=AE=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/composables/useChartConfig.js | 5 +- web/admin-spa/src/composables/useConfirm.js | 12 +- web/admin-spa/src/config/apiStats.js | 6 +- web/admin-spa/src/i18n/locales/en.js | 409 +++++++++++++++- web/admin-spa/src/i18n/locales/zh-cn.js | 444 +++++++++++++++++- web/admin-spa/src/i18n/locales/zh-tw.js | 416 +++++++++++++++- web/admin-spa/src/utils/format.js | 12 +- 7 files changed, 1274 insertions(+), 30 deletions(-) diff --git a/web/admin-spa/src/composables/useChartConfig.js b/web/admin-spa/src/composables/useChartConfig.js index 4cd403b9..351ceb3a 100644 --- a/web/admin-spa/src/composables/useChartConfig.js +++ b/web/admin-spa/src/composables/useChartConfig.js @@ -1,4 +1,5 @@ import { Chart } from 'chart.js/auto' +import i18n from '@/i18n' export function useChartConfig() { // 设置Chart.js默认配置 @@ -51,7 +52,9 @@ export function useChartConfig() { label += ': ' } if (context.parsed.y !== null) { - label += new Intl.NumberFormat('zh-CN').format(context.parsed.y) + const localeMap = { 'zh-cn': 'zh-CN', 'zh-tw': 'zh-TW', en: 'en-US' } + const currentLocale = localeMap[i18n.global.locale.value] || 'en-US' + label += new Intl.NumberFormat(currentLocale).format(context.parsed.y) } return label } diff --git a/web/admin-spa/src/composables/useConfirm.js b/web/admin-spa/src/composables/useConfirm.js index d08fbdde..15041bfb 100644 --- a/web/admin-spa/src/composables/useConfirm.js +++ b/web/admin-spa/src/composables/useConfirm.js @@ -1,16 +1,22 @@ import { ref } from 'vue' +import i18n from '@/i18n' const showConfirmModal = ref(false) const confirmOptions = ref({ title: '', message: '', - confirmText: '继续', - cancelText: '取消' + confirmText: i18n.global.t('common.confirmModal.continue'), + cancelText: i18n.global.t('common.confirmModal.cancel') }) const confirmResolve = ref(null) export function useConfirm() { - const showConfirm = (title, message, confirmText = '继续', cancelText = '取消') => { + const showConfirm = ( + title, + message, + confirmText = i18n.global.t('common.confirmModal.continue'), + cancelText = i18n.global.t('common.confirmModal.cancel') + ) => { return new Promise((resolve) => { confirmOptions.value = { title, diff --git a/web/admin-spa/src/config/apiStats.js b/web/admin-spa/src/config/apiStats.js index f581b1fe..e6999e8f 100644 --- a/web/admin-spa/src/config/apiStats.js +++ b/web/admin-spa/src/config/apiStats.js @@ -1,6 +1,8 @@ // API Stats 专用 API 客户端 // 与管理员 API 隔离,不需要认证 +import i18n from '@/i18n' + class ApiStatsClient { constructor() { this.baseURL = window.location.origin @@ -26,7 +28,9 @@ class ApiStatsClient { const data = await response.json() if (!response.ok) { - throw new Error(data.message || `请求失败: ${response.status}`) + throw new Error( + data.message || i18n.global.t('common.errors.requestFailed', { status: response.status }) + ) } return data diff --git a/web/admin-spa/src/i18n/locales/en.js b/web/admin-spa/src/i18n/locales/en.js index b694865e..7a987fd8 100644 --- a/web/admin-spa/src/i18n/locales/en.js +++ b/web/admin-spa/src/i18n/locales/en.js @@ -1,4 +1,47 @@ export default { + layout: { + mainLayout: { + comments: { + topNavigation: 'Top Navigation', + mainContentArea: 'Main Content Area', + tabBar: 'Tab Bar', + contentArea: 'Content Area' + }, + routing: { + routeChangeError: 'Route change failed:', + routeNotFound: 'Route not found', + defaultToTab: 'Default to dashboard' + } + }, + tabBar: { + tabs: { + dashboard: { + name: 'Dashboard', + shortName: 'Dashboard' + }, + apiKeys: { + name: 'API Keys', + shortName: 'API' + }, + accounts: { + name: 'Account Management', + shortName: 'Accounts' + }, + userManagement: { + name: 'User Management', + shortName: 'Users' + }, + tutorial: { + name: 'Tutorial', + shortName: 'Tutorial' + }, + settings: { + name: 'System Settings', + shortName: 'Settings' + } + } + } + }, common: { save: 'Save', cancel: 'Cancel', @@ -10,7 +53,89 @@ export default { update: 'Update', search: 'Search', reset: 'Reset', - locale: 'en' + locale: 'en', + toastNotification: { + defaultTitles: { + success: 'Success', + error: 'Error', + warning: 'Warning', + info: 'Information' + } + }, + confirmDialog: { + confirm: 'Confirm', + cancel: 'Cancel' + }, + confirmModal: { + continue: 'Continue', + cancel: 'Cancel' + }, + themeToggle: { + light: { + label: 'Light Mode', + shortLabel: 'Light' + }, + dark: { + label: 'Dark Mode', + shortLabel: 'Dark' + }, + auto: { + label: 'Follow System', + shortLabel: 'Auto' + }, + toggleTheme: 'Toggle Theme', + clickToSwitch: 'Click to switch theme' + }, + logoTitle: { + logoAlt: 'Logo' + }, + languageSwitch: { + zhCnName: 'Simplified Chinese', + zhTwName: 'Traditional Chinese', + enName: 'English', + zhCnFlag: 'CN', + zhTwFlag: 'TW', + enFlag: 'EN' + }, + accountSelector: { + searchPlaceholder: 'Search account name...', + schedulingGroups: 'Scheduling Groups', + membersUnit: ' members', + claudeOAuthAccounts: 'Claude OAuth Dedicated Accounts', + oauthAccounts: 'OAuth Dedicated Accounts', + claudeConsoleAccounts: 'Claude Console Dedicated Accounts', + noResultsFound: 'No matching accounts found', + selectAccount: 'Please select an account', + useSharedPool: 'Use shared account pool', + accountStatus: { + unknown: 'Unknown', + unauthorized: 'Unauthorized', + tokenError: 'Token Error', + pending: 'Pending', + rateLimited: 'Rate Limited', + error: 'Error', + active: 'Active' + }, + dateFormat: { + today: 'Created today', + yesterday: 'Created yesterday', + daysAgo: ' days ago' + } + }, + customDropdown: { + placeholder: 'Please select' + }, + // Common time and errors + time: { + justNow: 'Just now', + minutesAgo: '{minutes} minutes ago', + hoursAgo: '{hours} hours ago', + daysAgo: '{days} days ago' + }, + errors: { + requestFailed: 'Request failed: {status}', + loadSupportedClientsFailed: 'Failed to load supported clients' + } }, language: { zh: '简体中文', @@ -210,7 +335,15 @@ export default { securityNoticeMulti: 'Your API Keys are only used to query statistical data and will not be stored. Some individual information will not be displayed in aggregate mode.', multiKeyTip: - 'Tip: Supports querying up to 30 API Keys simultaneously. Use Ctrl+Enter for quick query.' + 'Tip: Supports querying up to 30 API Keys simultaneously. Use Ctrl+Enter for quick query.', + errors: { + queryStatsFailed: 'Failed to query statistics, please check your API Key', + enterAtLeastOneKey: 'Please enter at least one valid API Key', + batchQueryFailed: 'Batch query failed', + batchModelStatsFailed: 'Failed to load batch model statistics', + loadModelStatsFailed: 'Failed to load model statistics', + allInvalidKeys: 'All API Keys are invalid' + } }, // Login page @@ -221,7 +354,9 @@ export default { password: 'Password', passwordPlaceholder: 'Please enter password', loginButton: 'Login', - loggingIn: 'Logging in...' + loggingIn: 'Logging in...', + loginFailed: 'Login failed', + loginFailedCheck: 'Login failed, please check username and password' }, // Dashboard page @@ -262,6 +397,12 @@ export default { tokensPerMinute: 'Tokens per Minute', historicalData: 'Historical Data', minutes: 'minutes', + // Uptime display formats + uptimeFormat: { + daysHours: '{days} days {hours} hours', + hoursMinutes: '{hours} hours {minutes} minutes', + minutes: '{minutes} minutes' + }, // Charts section modelDistributionAndTrend: 'Model Usage Distribution & Token Usage Trends', @@ -269,6 +410,7 @@ export default { // Date filter presets today: 'Today', yesterday: 'Yesterday', + dayBefore: 'Day before yesterday', last7Days: 'Last 7 Days', last30Days: 'Last 30 Days', thisWeek: 'This Week', @@ -322,7 +464,53 @@ export default { time: 'Time', date: 'Date', tokenQuantity: 'Token Quantity', - requestsQuantity: 'Requests Count' + requestsQuantity: 'Requests Count', + + // Usage Trend component + usageTrend: { + title: 'Usage Trend', + granularity: { + byDay: 'By Day', + byHour: 'By Hour' + }, + periodOptions: { + last24Hours: '24 Hours', + last7Days: '7 Days', + last30Days: '30 Days', + recentDays: 'Last {days} Days' + }, + chartLabels: { + requests: 'Request Count', + tokens: 'Token Usage', + requestsAxis: 'Request Count', + tokensAxis: 'Token Usage' + } + }, + + // Model Distribution component + modelDistribution: { + title: 'Model Usage Distribution', + periods: { + daily: 'Today', + total: 'Total' + }, + noData: 'No model usage data available', + units: { + requests: 'requests', + tokens: 'tokens' + }, + chart: { + tooltip: { + requests: 'Requests', + tokens: 'Tokens' + } + } + }, + errors: { + rangeTooLongHour: 'For hourly granularity, date range cannot exceed 24 hours', + rangeTooLongDay: 'Date range cannot exceed 31 days', + rangeTooLongHourSwitched: 'Hourly range cannot exceed 24 hours, switched to last 24 hours' + } }, // Accounts page @@ -1316,6 +1504,52 @@ export default { loadStatsFailed: 'Failed to load API keys stats' }, + // User API Keys Manager + userApiKeysManager: { + title: 'My API Keys', + description: 'Manage your API keys to access Claude Relay services', + loading: 'Loading API keys...', + warnings: { + maxKeysReached: + 'You have reached the maximum number of API keys ({maxApiKeys}). Please delete an existing key to create a new one.' + }, + status: { + deleted: 'Deleted', + noDescription: 'No description', + neverUsed: 'Never used' + }, + dateLabels: { + created: 'Created', + deleted: 'Deleted', + lastUsed: 'Last used', + expires: 'Expires' + }, + usage: { + requests: 'requests' + }, + actions: { + viewApiKey: 'View API Key', + deleteApiKey: 'Delete API Key' + }, + buttons: { + createApiKey: 'Create API Key', + delete: 'Delete' + }, + emptyState: { + title: 'No API keys', + description: 'Get started by creating your first API key.' + }, + confirmDelete: { + title: 'Delete API Key', + message: "Are you sure you want to delete '{name}'? This action cannot be undone." + }, + messages: { + loadFailed: 'Failed to load API keys', + deleteSuccess: 'API key deleted successfully', + deleteFailed: 'Failed to delete API key' + } + }, + // User Login login: { title: 'User Sign In', @@ -1331,7 +1565,50 @@ export default { // Validation and error messages requiredFields: 'Please enter both username and password', loginSuccess: 'Login successful!', - loginFailed: 'Login failed' + loginFailed: 'Login failed', + accountDisabled: 'Your account has been disabled' + }, + + // View API Key Modal + viewApiKeyModal: { + title: 'API Key Details', + fields: { + name: 'Name', + description: 'Description', + apiKey: 'API Key', + status: 'Status', + usageStatistics: 'Usage Statistics' + }, + apiKeyDisplay: { + notAvailable: 'Not available', + keyPreview: 'cr_****', + fullKeyNotice: 'Full API key is only shown when first created or regenerated' + }, + buttons: { + hide: 'Hide', + show: 'Show', + copy: 'Copy', + close: 'Close' + }, + status: { + active: 'Active', + disabled: 'Disabled' + }, + usageStats: { + requests: 'Requests', + inputTokens: 'Input Tokens', + outputTokens: 'Output Tokens', + totalCost: 'Total Cost' + }, + timestamps: { + created: 'Created', + lastUsed: 'Last Used', + expires: 'Expires' + }, + messages: { + copySuccess: 'Copied to clipboard!', + copyFailed: 'Failed to copy to clipboard' + } }, // User Management @@ -1501,6 +1778,124 @@ export default { // Success message roleUpdated: 'User role updated to {role}' + }, + + // User Usage Statistics + userUsageStats: { + // Page header + title: 'Usage Statistics', + subtitle: 'View your API usage statistics and costs', + + // Time period selection + periodSelection: { + day: 'Last 24 Hours', + week: 'Last 7 Days', + month: 'Last 30 Days', + quarter: 'Last 90 Days' + }, + + // Loading state + loadingStats: 'Loading usage statistics...', + + // Statistics cards + statsCards: { + totalRequests: 'Total Requests', + inputTokens: 'Input Tokens', + outputTokens: 'Output Tokens', + totalCost: 'Total Cost' + }, + + // Daily usage trend chart + usageTrend: { + title: 'Daily Usage Trend', + chartTitle: 'Usage Chart', + dailyTrendsDescription: 'Daily usage trends would be displayed here', + chartIntegrationNote: + '(Chart integration can be added with Chart.js, D3.js, or similar library)' + }, + + // Usage by model section + modelUsage: { + title: 'Usage by Model', + requests: 'requests', + requestsCount: '{count} requests' + }, + + // Usage by API key table + apiKeyUsage: { + title: 'Usage by API Key', + headers: { + apiKey: 'API Key', + requests: 'Requests', + inputTokens: 'Input Tokens', + outputTokens: 'Output Tokens', + cost: 'Cost', + status: 'Status' + }, + status: { + active: 'Active', + disabled: 'Disabled', + deleted: 'Deleted' + } + }, + + // No data state + noData: { + title: 'No usage data', + description: + "You haven't made any API requests yet. Create an API key and start using the service to see usage statistics." + }, + + // Error messages + loadFailed: 'Failed to load usage statistics' + }, + + // Create API Key Modal + createApiKeyModal: { + title: 'Create New API Key', + + // Form labels and placeholders + form: { + nameLabel: 'Name', + nameRequired: '*', + namePlaceholder: 'Enter API key name', + descriptionLabel: 'Description', + descriptionPlaceholder: 'Optional description' + }, + + // Button text + buttons: { + cancel: 'Cancel', + creating: 'Creating...', + createApiKey: 'Create API Key', + copy: 'Copy', + done: 'Done' + }, + + // Success state + success: { + title: 'API Key Created Successfully!', + warning: { + important: 'Important:', + message: "Copy your API key now. You won't be able to see it again!" + } + }, + + // Error and validation messages + validation: { + nameRequired: 'API key name is required' + }, + + errors: { + createFailed: 'Failed to create API key' + }, + + // Toast messages + messages: { + createSuccess: 'API key created successfully!', + copySuccess: 'API key copied to clipboard!', + copyFailed: 'Failed to copy to clipboard' + } } }, @@ -1527,6 +1922,10 @@ export default { removeIcon: 'Remove', iconFormats: 'Supports .ico, .png, .jpg, .svg formats, max 350KB', iconPreview: 'Icon preview', + validation: { + iconTooLarge: 'Icon file size must not exceed 350KB', + iconTypeNotSupported: 'Unsupported file type, please choose .ico, .png, .jpg or .svg' + }, adminEntry: 'Admin Entry', adminEntryDescription: 'Login button display', diff --git a/web/admin-spa/src/i18n/locales/zh-cn.js b/web/admin-spa/src/i18n/locales/zh-cn.js index 1b536858..dbb9ef06 100644 --- a/web/admin-spa/src/i18n/locales/zh-cn.js +++ b/web/admin-spa/src/i18n/locales/zh-cn.js @@ -1,4 +1,47 @@ export default { + layout: { + mainLayout: { + comments: { + topNavigation: '顶部导航', + mainContentArea: '主内容区域', + tabBar: '标签栏', + contentArea: '内容区域' + }, + routing: { + routeChangeError: '路由切换失败:', + routeNotFound: '路由未找到', + defaultToTab: '默认选中仪表板' + } + }, + tabBar: { + tabs: { + dashboard: { + name: '仪表板', + shortName: '仪表板' + }, + apiKeys: { + name: 'API Keys', + shortName: 'API' + }, + accounts: { + name: '账户管理', + shortName: '账户' + }, + userManagement: { + name: '用户管理', + shortName: '用户' + }, + tutorial: { + name: '使用教程', + shortName: '教程' + }, + settings: { + name: '系统设置', + shortName: '设置' + } + } + } + }, common: { save: '保存', cancel: '取消', @@ -10,7 +53,89 @@ export default { update: '更新', search: '搜索', reset: '重置', - locale: 'zh-CN' + locale: 'zh-CN', + toastNotification: { + defaultTitles: { + success: '成功', + error: '错误', + warning: '警告', + info: '信息' + } + }, + confirmDialog: { + confirm: '确认', + cancel: '取消' + }, + confirmModal: { + continue: '继续', + cancel: '取消' + }, + themeToggle: { + light: { + label: '浅色模式', + shortLabel: '浅色' + }, + dark: { + label: '深色模式', + shortLabel: '深色' + }, + auto: { + label: '跟随系统', + shortLabel: '自动' + }, + toggleTheme: '切换主题', + clickToSwitch: '点击切换主题' + }, + logoTitle: { + logoAlt: '标志' + }, + languageSwitch: { + zhCnName: '简体中文', + zhTwName: '繁体中文', + enName: '英语', + zhCnFlag: '简', + zhTwFlag: '繁', + enFlag: 'EN' + }, + accountSelector: { + searchPlaceholder: '搜索账号名称...', + schedulingGroups: '调度分组', + membersUnit: '个成员', + claudeOAuthAccounts: 'Claude OAuth 专属账号', + oauthAccounts: 'OAuth 专属账号', + claudeConsoleAccounts: 'Claude Console 专属账号', + noResultsFound: '没有找到匹配的账号', + selectAccount: '请选择账号', + useSharedPool: '使用共享账号池', + accountStatus: { + unknown: '未知', + unauthorized: '未授权', + tokenError: 'Token错误', + pending: '待验证', + rateLimited: '限流中', + error: '异常', + active: '正常' + }, + dateFormat: { + today: '今天创建', + yesterday: '昨天创建', + daysAgo: '天前' + } + }, + customDropdown: { + placeholder: '请选择' + }, + // 通用时间与错误 + time: { + justNow: '刚刚', + minutesAgo: '{minutes}分钟前', + hoursAgo: '{hours}小时前', + daysAgo: '{days}天前' + }, + errors: { + requestFailed: '请求失败: {status}', + loadSupportedClientsFailed: '加载支持的客户端失败' + } }, language: { zh: '简体中文', @@ -19,6 +144,38 @@ export default { current: '当前语言', switch: '切换语言' }, + + layout: { + tabBar: { + tabs: { + dashboard: { + name: '仪表板', + shortName: '仪表板' + }, + apiKeys: { + name: 'API Keys', + shortName: 'API' + }, + accounts: { + name: '账户管理', + shortName: '账户' + }, + userManagement: { + name: '用户管理', + shortName: '用户' + }, + tutorial: { + name: '使用教程', + shortName: '教程' + }, + settings: { + name: '系统设置', + shortName: '设置' + } + } + } + }, + header: { adminPanel: '管理后台', userMenu: '用户菜单', @@ -204,7 +361,15 @@ export default { securityNoticeSingle: '您的 API Key 仅用于查询自己的统计数据,不会被存储或用于其他用途', securityNoticeMulti: '您的 API Keys 仅用于查询统计数据,不会被存储。聚合模式下部分个体化信息将不显示。', - multiKeyTip: '提示:最多支持同时查询 30 个 API Keys。使用 Ctrl+Enter 快速查询。' + multiKeyTip: '提示:最多支持同时查询 30 个 API Keys。使用 Ctrl+Enter 快速查询。', + errors: { + queryStatsFailed: '查询统计数据失败,请检查您的 API Key 是否正确', + enterAtLeastOneKey: '请输入至少一个有效的 API Key', + batchQueryFailed: '批量查询失败', + batchModelStatsFailed: '加载批量模型统计失败', + loadModelStatsFailed: '加载模型统计失败', + allInvalidKeys: '所有 API Key 都无效' + } }, // Login page @@ -215,7 +380,9 @@ export default { password: '密码', passwordPlaceholder: '请输入密码', loginButton: '登录', - loggingIn: '登录中...' + loggingIn: '登录中...', + loginFailed: '登录失败', + loginFailedCheck: '登录失败,请检查用户名和密码' }, // Dashboard page @@ -256,6 +423,12 @@ export default { tokensPerMinute: '每分钟Token数', historicalData: '历史数据', minutes: '分钟', + // Uptime display formats + uptimeFormat: { + daysHours: '{days}天 {hours}小时', + hoursMinutes: '{hours}小时 {minutes}分钟', + minutes: '{minutes}分钟' + }, // Charts section modelDistributionAndTrend: '模型使用分布与Token使用趋势', @@ -263,8 +436,9 @@ export default { // Date filter presets (would be populated from dateFilter.presetOptions) today: '今日', yesterday: '昨日', - last7Days: '迗 7 天', - last30Days: '迗 30 天', + dayBefore: '前天', + last7Days: '近7天', + last30Days: '近30天', thisWeek: '本周', lastWeek: '上周', thisMonth: '本月', @@ -316,7 +490,53 @@ export default { time: '时间', date: '日期', tokenQuantity: 'Token数量', - requestsQuantity: '请求次数' + requestsQuantity: '请求次数', + + // Usage Trend component + usageTrend: { + title: '使用趋势', + granularity: { + byDay: '按天', + byHour: '按小时' + }, + periodOptions: { + last24Hours: '24小时', + last7Days: '7天', + last30Days: '30天', + recentDays: '最近{days}天' + }, + chartLabels: { + requests: '请求次数', + tokens: 'Token使用量', + requestsAxis: '请求次数', + tokensAxis: 'Token使用量' + } + }, + + // Model Distribution component + modelDistribution: { + title: '模型使用分布', + periods: { + daily: '今日', + total: '累计' + }, + noData: '暂无模型使用数据', + units: { + requests: '请求', + tokens: 'tokens' + }, + chart: { + tooltip: { + requests: '请求', + tokens: 'Tokens' + } + } + }, + errors: { + rangeTooLongHour: '小时粒度下日期范围不能超过24小时', + rangeTooLongDay: '日期范围不能超过 31 天', + rangeTooLongHourSwitched: '小时粒度下日期范围不能超过24小时,已切换到近24小时' + } }, // Accounts page @@ -1285,6 +1505,52 @@ export default { loadStatsFailed: 'Failed to load API keys stats' }, + // User API Keys Manager + userApiKeysManager: { + title: '我的 API Keys', + description: '管理您的 API Keys 以访问 Claude Relay 服务', + loading: '正在加载 API Keys...', + warnings: { + maxKeysReached: + '您已达到 API Keys 的最大数量限制({maxApiKeys} 个)。请删除现有的 Key 以创建新的。' + }, + status: { + deleted: '已删除', + noDescription: '无描述', + neverUsed: '从未使用' + }, + dateLabels: { + created: '创建时间', + deleted: '删除时间', + lastUsed: '最后使用', + expires: '到期时间' + }, + usage: { + requests: '次请求' + }, + actions: { + viewApiKey: '查看 API Key', + deleteApiKey: '删除 API Key' + }, + buttons: { + createApiKey: '创建 API Key', + delete: '删除' + }, + emptyState: { + title: '无 API Keys', + description: '创建您的第一个 API Key 开始使用。' + }, + confirmDelete: { + title: '删除 API Key', + message: "确定要删除 '{name}' 吗?此操作无法撤销。" + }, + messages: { + loadFailed: '加载 API Keys 失败', + deleteSuccess: 'API Key 删除成功', + deleteFailed: '删除 API Key 失败' + } + }, + // User Login login: { title: 'User Sign In', @@ -1300,7 +1566,50 @@ export default { // Validation and error messages requiredFields: 'Please enter both username and password', loginSuccess: 'Login successful!', - loginFailed: 'Login failed' + loginFailed: 'Login failed', + accountDisabled: '您的账号已被禁用' + }, + + // View API Key Modal + viewApiKeyModal: { + title: 'API Key 详情', + fields: { + name: '名称', + description: '描述', + apiKey: 'API Key', + status: '状态', + usageStatistics: '使用统计' + }, + apiKeyDisplay: { + notAvailable: '不可用', + keyPreview: 'cr_****', + fullKeyNotice: '完整 API Key 仅在首次创建或重新生成时显示' + }, + buttons: { + hide: '隐藏', + show: '显示', + copy: '复制', + close: '关闭' + }, + status: { + active: '启用', + disabled: '禁用' + }, + usageStats: { + requests: '请求次数', + inputTokens: '输入令牌', + outputTokens: '输出令牌', + totalCost: '总费用' + }, + timestamps: { + created: '创建时间', + lastUsed: '最后使用', + expires: '过期时间' + }, + messages: { + copySuccess: '已复制到剪贴板!', + copyFailed: '复制到剪贴板失败' + } }, // User Management @@ -1469,6 +1778,123 @@ export default { // Success message roleUpdated: '用户角色已更新为 {role}' + }, + + // User Usage Statistics + userUsageStats: { + // Page header + title: '使用统计', + subtitle: '查看您的 API 使用统计和费用', + + // Time period selection + periodSelection: { + day: '最近24小时', + week: '最近7天', + month: '最近30天', + quarter: '最近90天' + }, + + // Loading state + loadingStats: '正在加载使用统计...', + + // Statistics cards + statsCards: { + totalRequests: '总请求数', + inputTokens: '输入Token', + outputTokens: '输出Token', + totalCost: '总费用' + }, + + // Daily usage trend chart + usageTrend: { + title: '日使用趋势', + chartTitle: '使用图表', + dailyTrendsDescription: '这里将显示日使用趋势', + chartIntegrationNote: '(可集成 Chart.js、D3.js 或类似图表库)' + }, + + // Usage by model section + modelUsage: { + title: '按模型使用情况', + requests: '请求', + requestsCount: '{count} 请求' + }, + + // Usage by API key table + apiKeyUsage: { + title: '按 API Key 使用情况', + headers: { + apiKey: 'API Key', + requests: '请求数', + inputTokens: '输入Token', + outputTokens: '输出Token', + cost: '费用', + status: '状态' + }, + status: { + active: '活跃', + disabled: '已禁用', + deleted: '已删除' + } + }, + + // No data state + noData: { + title: '暂无使用数据', + description: + '您还没有发起任何 API 请求。创建一个 API Key 并开始使用服务后,就能看到使用统计了。' + }, + + // Error messages + loadFailed: '加载使用统计失败' + }, + + // Create API Key Modal + createApiKeyModal: { + title: '创建新的 API Key', + + // 表单标签和占位符 + form: { + nameLabel: '名称', + nameRequired: '*', + namePlaceholder: '为您的 API Key 取一个名称', + descriptionLabel: '备注', + descriptionPlaceholder: '可选的备注信息' + }, + + // 按钮文本 + buttons: { + cancel: '取消', + creating: '创建中...', + createApiKey: '创建 API Key', + copy: '复制', + done: '完成' + }, + + // 成功状态 + success: { + title: 'API Key 创建成功!', + warning: { + important: '重要提示:', + message: '请立即复制您的 API Key,您将无法再次查看!' + } + }, + + // 错误和验证消息 + validation: { + nameRequired: 'API Key 名称是必填项' + }, + + errors: { + createFailed: '创建 API Key 失败' + }, + + // Toast 消息 + messages: { + createSuccess: 'API Key 创建成功!', + copySuccess: 'API Key 已复制到剪贴板!', + copyFailed: '复制到剪贴板失败' + } } }, @@ -1495,6 +1921,10 @@ export default { removeIcon: '删除', iconFormats: '支持 .ico, .png, .jpg, .svg 格式,最大 350KB', iconPreview: '图标预览', + validation: { + iconTooLarge: '图标文件大小不能超过 350KB', + iconTypeNotSupported: '不支持的文件类型,请选择 .ico, .png, .jpg 或 .svg 文件' + }, adminEntry: '管理入口', adminEntryDescription: '登录按钮显示', diff --git a/web/admin-spa/src/i18n/locales/zh-tw.js b/web/admin-spa/src/i18n/locales/zh-tw.js index d7a378fb..5768df67 100644 --- a/web/admin-spa/src/i18n/locales/zh-tw.js +++ b/web/admin-spa/src/i18n/locales/zh-tw.js @@ -1,4 +1,47 @@ export default { + layout: { + mainLayout: { + comments: { + topNavigation: '頂部導航', + mainContentArea: '主內容區域', + tabBar: '標籤欄', + contentArea: '內容區域' + }, + routing: { + routeChangeError: '路由切換失敗:', + routeNotFound: '路由未找到', + defaultToTab: '預設選中儀表板' + } + }, + tabBar: { + tabs: { + dashboard: { + name: '儀表板', + shortName: '儀表板' + }, + apiKeys: { + name: 'API Keys', + shortName: 'API' + }, + accounts: { + name: '帳戶管理', + shortName: '帳戶' + }, + userManagement: { + name: '用戶管理', + shortName: '用戶' + }, + tutorial: { + name: '使用教程', + shortName: '教程' + }, + settings: { + name: '系統設置', + shortName: '設置' + } + } + } + }, common: { save: '保存', cancel: '取消', @@ -10,7 +53,89 @@ export default { update: '更新', search: '搜尋', reset: '重置', - locale: 'zh-TW' + locale: 'zh-TW', + toastNotification: { + defaultTitles: { + success: '成功', + error: '錯誤', + warning: '警告', + info: '資訊' + } + }, + confirmDialog: { + confirm: '確認', + cancel: '取消' + }, + confirmModal: { + continue: '繼續', + cancel: '取消' + }, + themeToggle: { + light: { + label: '淺色模式', + shortLabel: '淺色' + }, + dark: { + label: '深色模式', + shortLabel: '深色' + }, + auto: { + label: '跟隨系統', + shortLabel: '自動' + }, + toggleTheme: '切換主題', + clickToSwitch: '點擊切換主題' + }, + logoTitle: { + logoAlt: '標誌' + }, + languageSwitch: { + zhCnName: '簡體中文', + zhTwName: '繁體中文', + enName: '英語', + zhCnFlag: '簡', + zhTwFlag: '繁', + enFlag: 'EN' + }, + accountSelector: { + searchPlaceholder: '搜尋帳號名稱...', + schedulingGroups: '調度分組', + membersUnit: '個成員', + claudeOAuthAccounts: 'Claude OAuth 專屬帳號', + oauthAccounts: 'OAuth 專屬帳號', + claudeConsoleAccounts: 'Claude Console 專屬帳號', + noResultsFound: '沒有找到匹配的帳號', + selectAccount: '請選擇帳號', + useSharedPool: '使用共享帳號池', + accountStatus: { + unknown: '未知', + unauthorized: '未授權', + tokenError: 'Token錯誤', + pending: '待驗證', + rateLimited: '限流中', + error: '異常', + active: '正常' + }, + dateFormat: { + today: '今天建立', + yesterday: '昨天建立', + daysAgo: '天前' + } + }, + customDropdown: { + placeholder: '請選擇' + }, + // 通用時間與錯誤 + time: { + justNow: '剛剛', + minutesAgo: '{minutes}分鐘前', + hoursAgo: '{hours}小時前', + daysAgo: '{days}天前' + }, + errors: { + requestFailed: '請求失敗: {status}', + loadSupportedClientsFailed: '載入支援的客戶端失敗' + } }, language: { zh: '簡體中文', @@ -204,7 +329,15 @@ export default { securityNoticeSingle: '您的 API Key 僅用於查詢自己的統計資料,不會被儲存或用於其他用途', securityNoticeMulti: '您的 API Keys 僅用於查詢統計資料,不會被儲存。彙整模式下部分個體化資訊將不顯示。', - multiKeyTip: '提示:最多支援同時查詢 30 個 API Keys。使用 Ctrl+Enter 快速查詢。' + multiKeyTip: '提示:最多支援同時查詢 30 個 API Keys。使用 Ctrl+Enter 快速查詢。', + errors: { + queryStatsFailed: '查詢統計資料失敗,請檢查您的 API Key 是否正確', + enterAtLeastOneKey: '請輸入至少一個有效的 API Key', + batchQueryFailed: '批次查詢失敗', + batchModelStatsFailed: '載入批次模型統計失敗', + loadModelStatsFailed: '載入模型統計失敗', + allInvalidKeys: '所有 API Key 都無效' + } }, // Login page @@ -215,7 +348,9 @@ export default { password: '密碼', passwordPlaceholder: '請輸入密碼', loginButton: '登錄', - loggingIn: '登錄中...' + loggingIn: '登錄中...', + loginFailed: '登入失敗', + loginFailedCheck: '登入失敗,請檢查使用者名稱與密碼' }, // Dashboard page @@ -255,7 +390,13 @@ export default { requestsPerMinute: '每分钟請求數', tokensPerMinute: '每分钟Token數', historicalData: '歷史資料', - minutes: '分钟', + minutes: '分鐘', + // Uptime display formats + uptimeFormat: { + daysHours: '{days}天 {hours}小時', + hoursMinutes: '{hours}小時 {minutes}分鐘', + minutes: '{minutes}分鐘' + }, // Charts section modelDistributionAndTrend: '模型使用分佈與Token使用趋勢', @@ -263,8 +404,9 @@ export default { // Date filter presets today: '今日', yesterday: '昨日', - last7Days: '近 7 天', - last30Days: '近 30 天', + dayBefore: '前天', + last7Days: '近7天', + last30Days: '近30天', thisWeek: '本週', lastWeek: '上週', thisMonth: '本月', @@ -280,6 +422,13 @@ export default { dateSeparator: '至', maxHours24: '最多24小時', + // Errors + errors: { + rangeTooLongHour: '小時粒度下日期範圍不能超過24小時', + rangeTooLongDay: '日期範圍不能超過 31 天', + rangeTooLongHourSwitched: '小時粒度超過24小時,已切換為近24小時' + }, + // Auto refresh controls autoRefresh: '自動刷新', refresh: '刷新', @@ -316,7 +465,48 @@ export default { time: '時間', date: '日期', tokenQuantity: 'Token數量', - requestsQuantity: '請求次數' + requestsQuantity: '請求次數', + + // Usage Trend component + usageTrend: { + title: '使用趨勢', + granularity: { + byDay: '按天', + byHour: '按小時' + }, + periodOptions: { + last24Hours: '24小時', + last7Days: '7天', + last30Days: '30天', + recentDays: '最近{days}天' + }, + chartLabels: { + requests: '請求次數', + tokens: 'Token使用量', + requestsAxis: '請求次數', + tokensAxis: 'Token使用量' + } + }, + + // Model Distribution component + modelDistribution: { + title: '模型使用分佈', + periods: { + daily: '今日', + total: '累計' + }, + noData: '暫無模型使用資料', + units: { + requests: '請求', + tokens: 'tokens' + }, + chart: { + tooltip: { + requests: '請求', + tokens: 'Tokens' + } + } + } }, // Accounts page @@ -1246,6 +1436,52 @@ export default { loadStatsFailed: 'Failed to load API keys stats' }, + // User API Keys Manager + userApiKeysManager: { + title: '我的 API Keys', + description: '管理您的 API Keys 以存取 Claude Relay 服務', + loading: '正在載入 API Keys...', + warnings: { + maxKeysReached: + '您已達到 API Keys 的最大數量限制({maxApiKeys} 個)。請刪除現有的 Key 以建立新的。' + }, + status: { + deleted: '已刪除', + noDescription: '無描述', + neverUsed: '從未使用' + }, + dateLabels: { + created: '建立時間', + deleted: '刪除時間', + lastUsed: '最後使用', + expires: '到期時間' + }, + usage: { + requests: '次請求' + }, + actions: { + viewApiKey: '檢視 API Key', + deleteApiKey: '刪除 API Key' + }, + buttons: { + createApiKey: '建立 API Key', + delete: '刪除' + }, + emptyState: { + title: '無 API Keys', + description: '建立您的第一個 API Key 開始使用。' + }, + confirmDelete: { + title: '刪除 API Key', + message: "確定要刪除 '{name}' 嗎?此操作無法撤銷。" + }, + messages: { + loadFailed: '載入 API Keys 失败', + deleteSuccess: 'API Key 刪除成功', + deleteFailed: '刪除 API Key 失败' + } + }, + // User Login login: { title: 'User Sign In', @@ -1261,7 +1497,50 @@ export default { // Validation and error messages requiredFields: 'Please enter both username and password', loginSuccess: 'Login successful!', - loginFailed: 'Login failed' + loginFailed: 'Login failed', + accountDisabled: '您的帳號已被停用' + }, + + // View API Key Modal + viewApiKeyModal: { + title: 'API Key 詳情', + fields: { + name: '名稱', + description: '描述', + apiKey: 'API Key', + status: '狀態', + usageStatistics: '使用統計' + }, + apiKeyDisplay: { + notAvailable: '不可用', + keyPreview: 'cr_****', + fullKeyNotice: '完整 API Key 僅在首次建立或重新產生時顯示' + }, + buttons: { + hide: '隱藏', + show: '顯示', + copy: '複製', + close: '關閉' + }, + status: { + active: '啟用', + disabled: '停用' + }, + usageStats: { + requests: '請求次數', + inputTokens: '輸入權杖', + outputTokens: '輸出權杖', + totalCost: '總費用' + }, + timestamps: { + created: '建立時間', + lastUsed: '最後使用', + expires: '過期時間' + }, + messages: { + copySuccess: '已複製到剪貼簿!', + copyFailed: '複製到剪貼簿失敗' + } }, // User Management @@ -1432,6 +1711,75 @@ export default { roleUpdated: '使用者角色已更新為 {role}' }, + // User Usage Statistics + userUsageStats: { + // Page header + title: '使用統計', + subtitle: '檢視您的 API 使用統計和費用', + + // Time period selection + periodSelection: { + day: '最近24小時', + week: '最近7天', + month: '最近30天', + quarter: '最近90天' + }, + + // Loading state + loadingStats: '正在載入使用統計...', + + // Statistics cards + statsCards: { + totalRequests: '總請求數', + inputTokens: '輸入Token', + outputTokens: '輸出Token', + totalCost: '總費用' + }, + + // Daily usage trend chart + usageTrend: { + title: '日使用趨勢', + chartTitle: '使用圖表', + dailyTrendsDescription: '這裡將顯示日使用趨勢', + chartIntegrationNote: '(可整合 Chart.js、D3.js 或類似圖表庫)' + }, + + // Usage by model section + modelUsage: { + title: '按模型使用情況', + requests: '請求', + requestsCount: '{count} 請求' + }, + + // Usage by API key table + apiKeyUsage: { + title: '按 API Key 使用情況', + headers: { + apiKey: 'API Key', + requests: '請求數', + inputTokens: '輸入Token', + outputTokens: '輸出Token', + cost: '費用', + status: '狀態' + }, + status: { + active: '活躍', + disabled: '已停用', + deleted: '已刪除' + } + }, + + // No data state + noData: { + title: '暫無使用資料', + description: + '您還沒有發起任何 API 請求。建立一個 API Key 並開始使用服務後,就能看到使用統計了。' + }, + + // Error messages + loadFailed: '載入使用統計失敗' + }, + // Usage Detail Modal usageDetailModal: { title: '使用統計詳情', @@ -1470,6 +1818,54 @@ export default { // Progress indicators usedPercentage: '已使用 {percentage}%' + }, + + // Create API Key Modal + createApiKeyModal: { + title: '建立新的 API Key', + + // 表單標籤和占位符 + form: { + nameLabel: '名稱', + nameRequired: '*', + namePlaceholder: '為您的 API Key 取一個名稱', + descriptionLabel: '備註', + descriptionPlaceholder: '可選的備註資訊' + }, + + // 按鈕文本 + buttons: { + cancel: '取消', + creating: '建立中...', + createApiKey: '建立 API Key', + copy: '複製', + done: '完成' + }, + + // 成功狀態 + success: { + title: 'API Key 建立成功!', + warning: { + important: '重要提示:', + message: '請立即複製您的 API Key,您將無法再次查看!' + } + }, + + // 錯誤和驗證訊息 + validation: { + nameRequired: 'API Key 名稱是必填項' + }, + + errors: { + createFailed: '建立 API Key 失敗' + }, + + // Toast 訊息 + messages: { + createSuccess: 'API Key 建立成功!', + copySuccess: 'API Key 已複製到剪貼簿!', + copyFailed: '複製到剪貼簿失敗' + } } }, @@ -1496,6 +1892,10 @@ export default { removeIcon: '刪除', iconFormats: '支援 .ico, .png, .jpg, .svg 格式,最大 350KB', iconPreview: '圖標預覽', + validation: { + iconTooLarge: '圖標文件大小不能超過 350KB', + iconTypeNotSupported: '不支援的文件類型,請選擇 .ico, .png, .jpg 或 .svg 文件' + }, adminEntry: '管理入口', adminEntryDescription: '登入按鈕顯示', diff --git a/web/admin-spa/src/utils/format.js b/web/admin-spa/src/utils/format.js index dba9a58b..5ee258c3 100644 --- a/web/admin-spa/src/utils/format.js +++ b/web/admin-spa/src/utils/format.js @@ -37,7 +37,9 @@ export function formatDate(date, format = 'YYYY-MM-DD HH:mm:ss') { .replace('ss', seconds) } -// 相对时间格式化 +// 相对时间格式化(使用 i18n) +import i18n from '@/i18n' + export function formatRelativeTime(date) { if (!date) return '' @@ -50,13 +52,13 @@ export function formatRelativeTime(date) { const diffDays = Math.floor(diffHours / 24) if (diffDays > 0) { - return `${diffDays}天前` + return i18n.global.t('common.time.daysAgo', { days: diffDays }) } else if (diffHours > 0) { - return `${diffHours}小时前` + return i18n.global.t('common.time.hoursAgo', { hours: diffHours }) } else if (diffMins > 0) { - return `${diffMins}分钟前` + return i18n.global.t('common.time.minutesAgo', { minutes: diffMins }) } else { - return '刚刚' + return i18n.global.t('common.time.justNow') } }