feat: 完成AccountsView页面完整国际化

- 添加useI18n导入并替换100+硬编码中文文本
- 扩展三种语言文件的accounts翻译键(150+条)
- 更新下拉选项为响应式计算属性支持动态翻译
- 国际化页面标题、表格列头、筛选器和操作按钮
- 处理状态文本、错误消息和工具提示
- 更新JavaScript函数返回值使用翻译键
- 完整支持桌面端和移动端视图的国际化
- 修正货币符号和时间格式化的参数化翻译

涵盖组件:
- 账户管理主界面(标题、描述、筛选器)
- 桌面端表格视图(列头、状态、操作按钮)
- 移动端卡片视图(标签、按钮、状态)
- 错误处理和确认对话框
- 时间和数值格式化函数
This commit is contained in:
Wangnov
2025-09-08 21:15:34 +08:00
parent cd7959f3bf
commit 27c0804219
4 changed files with 664 additions and 138 deletions

View File

@@ -316,5 +316,179 @@ export default {
date: 'Date', date: 'Date',
tokenQuantity: 'Token Quantity', tokenQuantity: 'Token Quantity',
requestsQuantity: 'Requests Count' requestsQuantity: 'Requests Count'
},
// Accounts page
accounts: {
title: 'Account Management',
description: 'Manage your Claude, Gemini, OpenAI and Azure OpenAI accounts and proxy configurations',
// Filters and sorting
sortBy: 'Select Sort',
selectPlatform: 'Select Platform',
selectGroup: 'Select Group',
refresh: 'Refresh',
refreshTooltip: 'Refresh data (Ctrl/⌘+click to force refresh all caches)',
addAccount: 'Add Account',
// Sort options
sortByName: 'Sort by Name',
sortByDailyTokens: 'Sort by Daily Tokens',
sortByDailyRequests: 'Sort by Daily Requests',
sortByTotalTokens: 'Sort by Total Tokens',
sortByLastUsed: 'Sort by Last Used',
// Platform options
allPlatforms: 'All Platforms',
claudePlatform: 'Claude',
claudeConsolePlatform: 'Claude Console',
geminiPlatform: 'Gemini',
openaiPlatform: 'OpenAI',
azureOpenaiPlatform: 'Azure OpenAI',
bedrockPlatform: 'Bedrock',
// Group options
allAccounts: 'All Accounts',
ungroupedAccounts: 'Ungrouped Accounts',
// Loading states
loadingAccounts: 'Loading accounts...',
noAccounts: 'No Accounts',
noAccountsHint: 'Click the button above to add your first account',
// Table headers
name: 'Name',
platformType: 'Platform/Type',
status: 'Status',
priority: 'Priority',
proxy: 'Proxy',
dailyUsage: 'Daily Usage',
sessionWindow: 'Session Window',
lastUsed: 'Last Used',
actions: 'Actions',
// Account types
dedicated: 'Dedicated',
groupScheduling: 'Group Scheduling',
shared: 'Shared',
belongsToGroup: 'Belongs to group: {name}',
// Platform labels
unknown: 'Unknown',
apiKey: 'API Key',
oauth: 'OAuth',
setup: 'Setup',
aws: 'AWS',
// Account status
normal: 'Normal',
abnormal: 'Abnormal',
blocked: 'Blocked',
tempError: 'Temporary Error',
rateLimited: 'Rate Limited',
notSchedulable: 'Not Schedulable',
bound: 'Bound: {count} API Keys',
// Proxy status
noProxy: 'No Proxy',
// Usage statistics
requests: ' requests',
noData: 'No Data',
averageRpm: 'Average {rpm} RPM',
// Session window tooltip
sessionWindowTooltip: {
title: 'Session window progress indicates the time progress of the 5-hour window',
normal: 'Normal: Requests processed normally',
warning: 'Warning: Approaching limit',
rejected: 'Rejected: Rate limit reached'
},
// Session window status
remaining: 'Remaining {time}',
ended: 'Ended',
// Console quota
quotaProgress: 'Quota Progress',
remainingQuota: 'Remaining $${amount}',
reset: 'Reset {time}',
// Mobile view labels
dailyUsageLabel: 'Daily Usage',
sessionWindowLabel: 'Session Window',
lastUsedLabel: 'Last Used',
proxyLabel: 'Proxy',
priorityLabel: 'Priority',
neverUsed: 'Never Used',
sessionWindowTooltipMobile: 'Session window progress does not represent usage, it only shows the remaining time until the next 5-hour window',
// Action buttons
resetStatus: 'Reset Status',
resetting: 'Resetting...',
resetStatusTooltip: 'Reset all abnormal statuses',
scheduling: 'Scheduling',
disabled: 'Disabled',
enableTooltip: 'Click to enable scheduling',
disableTooltip: 'Click to disable scheduling',
edit: 'Edit',
editTooltip: 'Edit account',
delete: 'Delete',
deleteTooltip: 'Delete account',
pause: 'Pause',
enable: 'Enable',
// Time formatting
justNow: 'Just now',
minutesAgo: '{minutes} minutes ago',
hoursAgo: '{hours} hours ago',
daysAgo: '{days} days ago',
hoursAndMinutes: '{hours} hours {minutes} minutes',
hoursOnly: '{hours} hours',
minutesOnly: '{minutes} minutes',
daysAndHours: '{days} days {hours} hours',
daysOnly: '{days} days',
// Rate limit time
rateLimitTime: '({time})',
// Messages and confirmations
resetStatusConfirmTitle: 'Reset Account Status',
resetStatusConfirmMessage: 'Are you sure you want to reset all abnormal statuses for this account? This will clear rate limit status, 401 error counts, and all other abnormal flags.',
resetStatusConfirmButton: 'Confirm Reset',
resetStatusCancelButton: 'Cancel',
statusResetSuccess: 'Account status has been reset',
statusResetFailed: 'Status reset failed',
deleteAccountTitle: 'Delete Account',
deleteAccountMessage: 'Are you sure you want to delete account "{name}"?\n\nThis action cannot be undone.',
deleteAccountButton: 'Delete',
deleteAccountCancel: 'Cancel',
cannotDeleteBoundAccount: 'Cannot delete this account, {count} API Keys are bound to this account, please unbind all API Keys first',
accountDeleted: 'Account deleted',
deleteFailed: 'Delete failed',
enabledScheduling: 'Scheduling enabled',
disabledScheduling: 'Scheduling disabled',
schedulingToggleFailed: 'Failed to toggle scheduling status',
unsupportedAccountType: 'This account type does not support scheduling control',
operationFailed: 'Operation failed',
accountCreateSuccess: 'Account created successfully',
accountUpdateSuccess: 'Account updated successfully',
loadAccountsFailed: 'Failed to load accounts',
unsupportedAccountTypeReset: 'Unsupported account type',
// Schedulable reasons
invalidApiKey: 'API Key invalid or expired (401 error)',
serviceOverload: 'Service overloaded (529 error)',
rateLimitTriggered: 'Rate limit triggered (429 error)',
authFailed: 'Authentication failed (401 error)',
manualStop: 'Manually stopped scheduling',
// Account type display
claudeMax: 'Claude Max',
claudePro: 'Claude Pro',
claudeFree: 'Claude Free'
} }
} }

View File

@@ -316,5 +316,179 @@ export default {
date: '日期', date: '日期',
tokenQuantity: 'Token数量', tokenQuantity: 'Token数量',
requestsQuantity: '请求次数' requestsQuantity: '请求次数'
},
// Accounts page
accounts: {
title: '账户管理',
description: '管理您的 Claude、Gemini、OpenAI 和 Azure OpenAI 账户及代理配置',
// Filters and sorting
sortBy: '选择排序',
selectPlatform: '选择平台',
selectGroup: '选择分组',
refresh: '刷新',
refreshTooltip: '刷新数据 (Ctrl/⌘+点击强制刷新所有缓存)',
addAccount: '添加账户',
// Sort options
sortByName: '按名称排序',
sortByDailyTokens: '按今日Token排序',
sortByDailyRequests: '按今日请求数排序',
sortByTotalTokens: '按总Token排序',
sortByLastUsed: '按最后使用排序',
// Platform options
allPlatforms: '所有平台',
claudePlatform: 'Claude',
claudeConsolePlatform: 'Claude Console',
geminiPlatform: 'Gemini',
openaiPlatform: 'OpenAi',
azureOpenaiPlatform: 'Azure OpenAI',
bedrockPlatform: 'Bedrock',
// Group options
allAccounts: '所有账户',
ungroupedAccounts: '未分组账户',
// Loading states
loadingAccounts: '正在加载账户...',
noAccounts: '暂无账户',
noAccountsHint: '点击上方按钮添加您的第一个账户',
// Table headers
name: '名称',
platformType: '平台/类型',
status: '状态',
priority: '优先级',
proxy: '代理',
dailyUsage: '今日使用',
sessionWindow: '会话窗口',
lastUsed: '最后使用',
actions: '操作',
// Account types
dedicated: '专属',
groupScheduling: '分组调度',
shared: '共享',
belongsToGroup: '所属分组: {name}',
// Platform labels
unknown: '未知',
apiKey: 'API Key',
oauth: 'OAuth',
setup: 'Setup',
aws: 'AWS',
// Account status
normal: '正常',
abnormal: '异常',
blocked: '已封锁',
tempError: '临时异常',
rateLimited: '限流中',
notSchedulable: '不可调度',
bound: '绑定: {count} 个API Key',
// Proxy status
noProxy: '无代理',
// Usage statistics
requests: '次',
noData: '暂无数据',
averageRpm: '平均 {rpm} RPM',
// Session window tooltip
sessionWindowTooltip: {
title: '会话窗口进度表示5小时窗口的时间进度',
normal: '正常:请求正常处理',
warning: '警告:接近限制',
rejected: '拒绝:达到速率限制'
},
// Session window status
remaining: '剩余 {time}',
ended: '已结束',
// Console quota
quotaProgress: '额度进度',
remainingQuota: '剩余 $${amount}',
reset: '重置 {time}',
// Mobile view labels
dailyUsageLabel: '今日使用',
sessionWindowLabel: '会话窗口',
lastUsedLabel: '最后使用',
proxyLabel: '代理',
priorityLabel: '优先级',
neverUsed: '从未使用',
sessionWindowTooltipMobile: '会话窗口进度不代表使用量仅表示距离下一个5小时窗口的剩余时间',
// Action buttons
resetStatus: '重置状态',
resetting: '重置中...',
resetStatusTooltip: '重置所有异常状态',
scheduling: '调度',
disabled: '停用',
enableTooltip: '点击启用调度',
disableTooltip: '点击禁用调度',
edit: '编辑',
editTooltip: '编辑账户',
delete: '删除',
deleteTooltip: '删除账户',
pause: '暂停',
enable: '启用',
// Time formatting
justNow: '刚刚',
minutesAgo: '{minutes} 分钟前',
hoursAgo: '{hours} 小时前',
daysAgo: '{days} 天前',
hoursAndMinutes: '{hours}小时{minutes}分钟',
hoursOnly: '{hours}小时',
minutesOnly: '{minutes}分钟',
daysAndHours: '{days}天{hours}小时',
daysOnly: '{days}天',
// Rate limit time
rateLimitTime: '({time})',
// Messages and confirmations
resetStatusConfirmTitle: '重置账户状态',
resetStatusConfirmMessage: '确定要重置此账户的所有异常状态吗这将清除限流状态、401错误计数等所有异常标记。',
resetStatusConfirmButton: '确定重置',
resetStatusCancelButton: '取消',
statusResetSuccess: '账户状态已重置',
statusResetFailed: '状态重置失败',
deleteAccountTitle: '删除账户',
deleteAccountMessage: '确定要删除账户 "{name}" 吗?\n\n此操作不可恢复。',
deleteAccountButton: '删除',
deleteAccountCancel: '取消',
cannotDeleteBoundAccount: '无法删除此账号,有 {count} 个API Key绑定到此账号请先解绑所有API Key',
accountDeleted: '账户已删除',
deleteFailed: '删除失败',
enabledScheduling: '已启用调度',
disabledScheduling: '已禁用调度',
schedulingToggleFailed: '切换调度状态失败',
unsupportedAccountType: '该账户类型暂不支持调度控制',
operationFailed: '操作失败',
accountCreateSuccess: '账户创建成功',
accountUpdateSuccess: '账户更新成功',
loadAccountsFailed: '加载账户失败',
unsupportedAccountTypeReset: '不支持的账户类型',
// Schedulable reasons
invalidApiKey: 'API Key无效或已过期401错误',
serviceOverload: '服务过载529错误',
rateLimitTriggered: '触发限流429错误',
authFailed: '认证失败401错误',
manualStop: '手动停止调度',
// Account type display
claudeMax: 'Claude Max',
claudePro: 'Claude Pro',
claudeFree: 'Claude Free'
} }
} }

View File

@@ -316,5 +316,179 @@ export default {
date: '日期', date: '日期',
tokenQuantity: 'Token數量', tokenQuantity: 'Token數量',
requestsQuantity: '請求次數' requestsQuantity: '請求次數'
},
// Accounts page
accounts: {
title: '帳戶管理',
description: '管理您的 Claude、Gemini、OpenAI 和 Azure OpenAI 帳戶及代理配置',
// Filters and sorting
sortBy: '選擇排序',
selectPlatform: '選擇平台',
selectGroup: '選擇分組',
refresh: '刷新',
refreshTooltip: '刷新資料 (Ctrl/⌘+點擊強制刷新所有快取)',
addAccount: '添加帳戶',
// Sort options
sortByName: '按名稱排序',
sortByDailyTokens: '按今日Token排序',
sortByDailyRequests: '按今日請求數排序',
sortByTotalTokens: '按總Token排序',
sortByLastUsed: '按最後使用排序',
// Platform options
allPlatforms: '所有平台',
claudePlatform: 'Claude',
claudeConsolePlatform: 'Claude Console',
geminiPlatform: 'Gemini',
openaiPlatform: 'OpenAi',
azureOpenaiPlatform: 'Azure OpenAI',
bedrockPlatform: 'Bedrock',
// Group options
allAccounts: '所有帳戶',
ungroupedAccounts: '未分組帳戶',
// Loading states
loadingAccounts: '正在載入帳戶...',
noAccounts: '暫無帳戶',
noAccountsHint: '點擊上方按鈕添加您的第一個帳戶',
// Table headers
name: '名稱',
platformType: '平台/類型',
status: '狀態',
priority: '優先級',
proxy: '代理',
dailyUsage: '今日使用',
sessionWindow: '會話窗口',
lastUsed: '最後使用',
actions: '操作',
// Account types
dedicated: '專屬',
groupScheduling: '分組調度',
shared: '共享',
belongsToGroup: '所屬分組: {name}',
// Platform labels
unknown: '未知',
apiKey: 'API Key',
oauth: 'OAuth',
setup: 'Setup',
aws: 'AWS',
// Account status
normal: '正常',
abnormal: '異常',
blocked: '已封鎖',
tempError: '臨時異常',
rateLimited: '限流中',
notSchedulable: '不可調度',
bound: '綁定: {count} 個API Key',
// Proxy status
noProxy: '無代理',
// Usage statistics
requests: '次',
noData: '暫無資料',
averageRpm: '平均 {rpm} RPM',
// Session window tooltip
sessionWindowTooltip: {
title: '會話窗口進度表示5小時窗口的時間進度',
normal: '正常:請求正常處理',
warning: '警告:接近限制',
rejected: '拒絕:達到速率限制'
},
// Session window status
remaining: '剩餘 {time}',
ended: '已結束',
// Console quota
quotaProgress: '額度進度',
remainingQuota: '剩餘 $${amount}',
reset: '重置 {time}',
// Mobile view labels
dailyUsageLabel: '今日使用',
sessionWindowLabel: '會話窗口',
lastUsedLabel: '最後使用',
proxyLabel: '代理',
priorityLabel: '優先級',
neverUsed: '從未使用',
sessionWindowTooltipMobile: '會話窗口進度不代表使用量僅表示距離下一個5小時窗口的剩餘時間',
// Action buttons
resetStatus: '重置狀態',
resetting: '重置中...',
resetStatusTooltip: '重置所有異常狀態',
scheduling: '調度',
disabled: '停用',
enableTooltip: '點擊啟用調度',
disableTooltip: '點擊禁用調度',
edit: '編輯',
editTooltip: '編輯帳戶',
delete: '刪除',
deleteTooltip: '刪除帳戶',
pause: '暫停',
enable: '啟用',
// Time formatting
justNow: '剛剛',
minutesAgo: '{minutes} 分鐘前',
hoursAgo: '{hours} 小時前',
daysAgo: '{days} 天前',
hoursAndMinutes: '{hours}小時{minutes}分鐘',
hoursOnly: '{hours}小時',
minutesOnly: '{minutes}分鐘',
daysAndHours: '{days}天{hours}小時',
daysOnly: '{days}天',
// Rate limit time
rateLimitTime: '({time})',
// Messages and confirmations
resetStatusConfirmTitle: '重置帳戶狀態',
resetStatusConfirmMessage: '確定要重置此帳戶的所有異常狀態嗎這將清除限流狀態、401錯誤計數等所有異常標記。',
resetStatusConfirmButton: '確定重置',
resetStatusCancelButton: '取消',
statusResetSuccess: '帳戶狀態已重置',
statusResetFailed: '狀態重置失敗',
deleteAccountTitle: '刪除帳戶',
deleteAccountMessage: '確定要刪除帳戶 "{name}" 嗎?\n\n此操作不可恢復。',
deleteAccountButton: '刪除',
deleteAccountCancel: '取消',
cannotDeleteBoundAccount: '無法刪除此帳號,有 {count} 個API Key綁定到此帳號請先解綁所有API Key',
accountDeleted: '帳戶已刪除',
deleteFailed: '刪除失敗',
enabledScheduling: '已啟用調度',
disabledScheduling: '已禁用調度',
schedulingToggleFailed: '切換調度狀態失敗',
unsupportedAccountType: '該帳戶類型暫不支持調度控制',
operationFailed: '操作失敗',
accountCreateSuccess: '帳戶創建成功',
accountUpdateSuccess: '帳戶更新成功',
loadAccountsFailed: '載入帳戶失敗',
unsupportedAccountTypeReset: '不支持的帳戶類型',
// Schedulable reasons
invalidApiKey: 'API Key無效或已過期401錯誤',
serviceOverload: '服務過載529錯誤',
rateLimitTriggered: '觸發限流429錯誤',
authFailed: '認證失敗401錯誤',
manualStop: '手動停止調度',
// Account type display
claudeMax: 'Claude Max',
claudePro: 'Claude Pro',
claudeFree: 'Claude Free'
} }
} }

View File

@@ -4,10 +4,10 @@
<div class="mb-4 flex flex-col gap-4 sm:mb-6"> <div class="mb-4 flex flex-col gap-4 sm:mb-6">
<div> <div>
<h3 class="mb-1 text-lg font-bold text-gray-900 dark:text-gray-100 sm:mb-2 sm:text-xl"> <h3 class="mb-1 text-lg font-bold text-gray-900 dark:text-gray-100 sm:mb-2 sm:text-xl">
账户管理 {{ t('accounts.title') }}
</h3> </h3>
<p class="text-sm text-gray-600 dark:text-gray-400 sm:text-base"> <p class="text-sm text-gray-600 dark:text-gray-400 sm:text-base">
管理您的 ClaudeGeminiOpenAIAzure OpenAIOpenAI-Responses CCR 账户及代理配置 {{ t('accounts.description') }}
</p> </p>
</div> </div>
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"> <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
@@ -23,7 +23,7 @@
icon="fa-sort-amount-down" icon="fa-sort-amount-down"
icon-color="text-indigo-500" icon-color="text-indigo-500"
:options="sortOptions" :options="sortOptions"
placeholder="选择排序" :placeholder="t('accounts.sortBy')"
@change="sortAccounts()" @change="sortAccounts()"
/> />
</div> </div>
@@ -38,7 +38,7 @@
icon="fa-server" icon="fa-server"
icon-color="text-blue-500" icon-color="text-blue-500"
:options="platformOptions" :options="platformOptions"
placeholder="选择平台" :placeholder="t('accounts.selectPlatform')"
@change="filterByPlatform" @change="filterByPlatform"
/> />
</div> </div>
@@ -53,7 +53,7 @@
icon="fa-layer-group" icon="fa-layer-group"
icon-color="text-purple-500" icon-color="text-purple-500"
:options="groupOptions" :options="groupOptions"
placeholder="选择分组" :placeholder="t('accounts.selectGroup')"
@change="filterByGroup" @change="filterByGroup"
/> />
</div> </div>
@@ -61,7 +61,7 @@
<!-- 刷新按钮 --> <!-- 刷新按钮 -->
<div class="relative"> <div class="relative">
<el-tooltip <el-tooltip
content="刷新数据 (Ctrl/⌘+点击强制刷新所有缓存)" :content="t('accounts.refreshTooltip')"
effect="dark" effect="dark"
placement="bottom" placement="bottom"
> >
@@ -81,7 +81,7 @@
accountsLoading ? 'fa-spinner fa-spin' : 'fa-sync-alt' accountsLoading ? 'fa-spinner fa-spin' : 'fa-sync-alt'
]" ]"
/> />
<span class="relative">刷新</span> <span class="relative">{{ t('accounts.refresh') }}</span>
</button> </button>
</el-tooltip> </el-tooltip>
</div> </div>
@@ -93,14 +93,14 @@
@click.stop="openCreateAccountModal" @click.stop="openCreateAccountModal"
> >
<i class="fas fa-plus"></i> <i class="fas fa-plus"></i>
<span>添加账户</span> <span>{{ t('accounts.addAccount') }}</span>
</button> </button>
</div> </div>
</div> </div>
<div v-if="accountsLoading" class="py-12 text-center"> <div v-if="accountsLoading" class="py-12 text-center">
<div class="loading-spinner mx-auto mb-4" /> <div class="loading-spinner mx-auto mb-4" />
<p class="text-gray-500 dark:text-gray-400">正在加载账户...</p> <p class="text-gray-500 dark:text-gray-400">{{ t('accounts.loadingAccounts') }}</p>
</div> </div>
<div v-else-if="sortedAccounts.length === 0" class="py-12 text-center"> <div v-else-if="sortedAccounts.length === 0" class="py-12 text-center">
@@ -109,8 +109,8 @@
> >
<i class="fas fa-user-circle text-xl text-gray-400" /> <i class="fas fa-user-circle text-xl text-gray-400" />
</div> </div>
<p class="text-lg text-gray-500 dark:text-gray-400">暂无账户</p> <p class="text-lg text-gray-500 dark:text-gray-400">{{ t('accounts.noAccounts') }}</p>
<p class="mt-2 text-sm text-gray-400 dark:text-gray-500">点击上方按钮添加您的第一个账户</p> <p class="mt-2 text-sm text-gray-400 dark:text-gray-500">{{ t('accounts.noAccountsHint') }}</p>
</div> </div>
<!-- 桌面端表格视图 --> <!-- 桌面端表格视图 -->
@@ -122,7 +122,7 @@
class="w-[22%] min-w-[180px] cursor-pointer px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600" class="w-[22%] min-w-[180px] cursor-pointer px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600"
@click="sortAccounts('name')" @click="sortAccounts('name')"
> >
名称 {{ t('accounts.name') }}
<i <i
v-if="accountsSortBy === 'name'" v-if="accountsSortBy === 'name'"
:class="[ :class="[
@@ -137,7 +137,7 @@
class="w-[15%] min-w-[120px] cursor-pointer px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600" class="w-[15%] min-w-[120px] cursor-pointer px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600"
@click="sortAccounts('platform')" @click="sortAccounts('platform')"
> >
平台/类型 {{ t('accounts.platformType') }}
<i <i
v-if="accountsSortBy === 'platform'" v-if="accountsSortBy === 'platform'"
:class="[ :class="[
@@ -152,7 +152,7 @@
class="w-[12%] min-w-[100px] cursor-pointer px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600" class="w-[12%] min-w-[100px] cursor-pointer px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600"
@click="sortAccounts('status')" @click="sortAccounts('status')"
> >
状态 {{ t('accounts.status') }}
<i <i
v-if="accountsSortBy === 'status'" v-if="accountsSortBy === 'status'"
:class="[ :class="[
@@ -167,7 +167,7 @@
class="w-[8%] min-w-[80px] cursor-pointer px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600" class="w-[8%] min-w-[80px] cursor-pointer px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600"
@click="sortAccounts('priority')" @click="sortAccounts('priority')"
> >
优先级 {{ t('accounts.priority') }}
<i <i
v-if="accountsSortBy === 'priority'" v-if="accountsSortBy === 'priority'"
:class="[ :class="[
@@ -181,40 +181,40 @@
<th <th
class="w-[10%] min-w-[100px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300" class="w-[10%] min-w-[100px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
> >
代理 {{ t('accounts.proxy') }}
</th> </th>
<th <th
class="w-[10%] min-w-[90px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300" class="w-[10%] min-w-[90px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
> >
今日使用 {{ t('accounts.dailyUsage') }}
</th> </th>
<th <th
class="w-[10%] min-w-[100px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300" class="w-[10%] min-w-[100px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
> >
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span>会话窗口</span> <span>{{ t('accounts.sessionWindow') }}</span>
<el-tooltip placement="top"> <el-tooltip placement="top">
<template #content> <template #content>
<div class="space-y-2"> <div class="space-y-2">
<div>会话窗口进度表示5小时窗口的时间进度</div> <div>{{ t('accounts.sessionWindowTooltip.title') }}</div>
<div class="space-y-1 text-xs"> <div class="space-y-1 text-xs">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div <div
class="h-2 w-16 rounded bg-gradient-to-r from-blue-500 to-indigo-600" class="h-2 w-16 rounded bg-gradient-to-r from-blue-500 to-indigo-600"
></div> ></div>
<span>正常请求正常处理</span> <span>{{ t('accounts.sessionWindowTooltip.normal') }}</span>
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div <div
class="h-2 w-16 rounded bg-gradient-to-r from-yellow-500 to-orange-500" class="h-2 w-16 rounded bg-gradient-to-r from-yellow-500 to-orange-500"
></div> ></div>
<span>警告接近限制</span> <span>{{ t('accounts.sessionWindowTooltip.warning') }}</span>
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div <div
class="h-2 w-16 rounded bg-gradient-to-r from-red-500 to-red-600" class="h-2 w-16 rounded bg-gradient-to-r from-red-500 to-red-600"
></div> ></div>
<span>拒绝达到速率限制</span> <span>{{ t('accounts.sessionWindowTooltip.rejected') }}</span>
</div> </div>
</div> </div>
</div> </div>
@@ -228,12 +228,12 @@
<th <th
class="w-[8%] min-w-[80px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300" class="w-[8%] min-w-[80px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
> >
最后使用 {{ t('accounts.lastUsed') }}
</th> </th>
<th <th
class="w-[15%] min-w-[180px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300" class="w-[15%] min-w-[180px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
> >
操作 {{ t('accounts.actions') }}
</th> </th>
</tr> </tr>
</thead> </thead>
@@ -258,19 +258,19 @@
v-if="account.accountType === 'dedicated'" v-if="account.accountType === 'dedicated'"
class="inline-flex items-center rounded-full bg-purple-100 px-2 py-0.5 text-xs font-medium text-purple-800" class="inline-flex items-center rounded-full bg-purple-100 px-2 py-0.5 text-xs font-medium text-purple-800"
> >
<i class="fas fa-lock mr-1" />专属 <i class="fas fa-lock mr-1" />{{ t('accounts.dedicated') }}
</span> </span>
<span <span
v-else-if="account.accountType === 'group'" v-else-if="account.accountType === 'group'"
class="inline-flex items-center rounded-full bg-blue-100 px-2 py-0.5 text-xs font-medium text-blue-800" class="inline-flex items-center rounded-full bg-blue-100 px-2 py-0.5 text-xs font-medium text-blue-800"
> >
<i class="fas fa-layer-group mr-1" />分组调度 <i class="fas fa-layer-group mr-1" />{{ t('accounts.groupScheduling') }}
</span> </span>
<span <span
v-else v-else
class="inline-flex items-center rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800" class="inline-flex items-center rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800"
> >
<i class="fas fa-share-alt mr-1" />共享 <i class="fas fa-share-alt mr-1" />{{ t('accounts.shared') }}
</span> </span>
</div> </div>
<!-- 显示所有分组 - 换行显示 --> <!-- 显示所有分组 - 换行显示 -->
@@ -282,7 +282,7 @@
v-for="group in account.groupInfos" v-for="group in account.groupInfos"
:key="group.id" :key="group.id"
class="inline-flex items-center rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-600 dark:bg-gray-700 dark:text-gray-400" class="inline-flex items-center rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-600 dark:bg-gray-700 dark:text-gray-400"
:title="`所属分组: ${group.name}`" :title="t('accounts.belongsToGroup', { name: group.name })"
> >
<i class="fas fa-folder mr-1" />{{ group.name }} <i class="fas fa-folder mr-1" />{{ group.name }}
</span> </span>
@@ -390,7 +390,7 @@
class="flex items-center gap-1.5 rounded-lg border border-gray-200 bg-gradient-to-r from-gray-100 to-gray-200 px-2.5 py-1" class="flex items-center gap-1.5 rounded-lg border border-gray-200 bg-gradient-to-r from-gray-100 to-gray-200 px-2.5 py-1"
> >
<i class="fas fa-question text-xs text-gray-700" /> <i class="fas fa-question text-xs text-gray-700" />
<span class="text-xs font-semibold text-gray-800">未知</span> <span class="text-xs font-semibold text-gray-800">{{ t('accounts.unknown') }}</span>
</div> </div>
</div> </div>
</td> </td>
@@ -426,14 +426,14 @@
/> />
{{ {{
account.status === 'blocked' account.status === 'blocked'
? '已封锁' ? t('accounts.blocked')
: account.status === 'unauthorized' : account.status === 'unauthorized'
? '异常' ? t('accounts.abnormal')
: account.status === 'temp_error' : account.status === 'temp_error'
? '临时异常' ? t('accounts.tempError')
: account.isActive : account.isActive
? '正常' ? t('accounts.normal')
: '异常' : t('accounts.abnormal')
}} }}
</span> </span>
<span <span
@@ -444,14 +444,14 @@
class="inline-flex items-center rounded-full bg-yellow-100 px-3 py-1 text-xs font-semibold text-yellow-800" class="inline-flex items-center rounded-full bg-yellow-100 px-3 py-1 text-xs font-semibold text-yellow-800"
> >
<i class="fas fa-exclamation-triangle mr-1" /> <i class="fas fa-exclamation-triangle mr-1" />
限流中 {{ t('accounts.rateLimited') }}
<span <span
v-if=" v-if="
account.rateLimitStatus && account.rateLimitStatus &&
typeof account.rateLimitStatus === 'object' && typeof account.rateLimitStatus === 'object' &&
account.rateLimitStatus.minutesRemaining > 0 account.rateLimitStatus.minutesRemaining > 0
" "
>({{ formatRateLimitTime(account.rateLimitStatus.minutesRemaining) }})</span >({{ t('accounts.rateLimitTime', { time: formatRateLimitTime(account.rateLimitStatus.minutesRemaining) }) }})</span
> >
</span> </span>
<span <span
@@ -459,7 +459,7 @@
class="inline-flex items-center rounded-full bg-gray-100 px-3 py-1 text-xs font-semibold text-gray-700" class="inline-flex items-center rounded-full bg-gray-100 px-3 py-1 text-xs font-semibold text-gray-700"
> >
<i class="fas fa-pause-circle mr-1" /> <i class="fas fa-pause-circle mr-1" />
不可调度 {{ t('accounts.notSchedulable') }}
<el-tooltip <el-tooltip
v-if="getSchedulableReason(account)" v-if="getSchedulableReason(account)"
:content="getSchedulableReason(account)" :content="getSchedulableReason(account)"
@@ -480,7 +480,7 @@
v-if="account.accountType === 'dedicated'" v-if="account.accountType === 'dedicated'"
class="text-xs text-gray-500 dark:text-gray-400" class="text-xs text-gray-500 dark:text-gray-400"
> >
绑定: {{ account.boundApiKeysCount || 0 }} 个API Key {{ t('accounts.bound', { count: account.boundApiKeysCount || 0 }) }}
</span> </span>
</div> </div>
</td> </td>
@@ -520,14 +520,14 @@
> >
{{ formatProxyDisplay(account.proxy) }} {{ formatProxyDisplay(account.proxy) }}
</div> </div>
<div v-else class="text-gray-400">无代理</div> <div v-else class="text-gray-400">{{ t('accounts.noProxy') }}</div>
</td> </td>
<td class="whitespace-nowrap px-3 py-4 text-sm"> <td class="whitespace-nowrap px-3 py-4 text-sm">
<div v-if="account.usage && account.usage.daily" class="space-y-1"> <div v-if="account.usage && account.usage.daily" class="space-y-1">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div class="h-2 w-2 rounded-full bg-blue-500" /> <div class="h-2 w-2 rounded-full bg-blue-500" />
<span class="text-sm font-medium text-gray-900 dark:text-gray-100" <span class="text-sm font-medium text-gray-900 dark:text-gray-100"
>{{ account.usage.daily.requests || 0 }} </span >{{ account.usage.daily.requests || 0 }} {{ t('accounts.requests') }}</span
> >
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
@@ -546,10 +546,10 @@
v-if="account.usage.averages && account.usage.averages.rpm > 0" v-if="account.usage.averages && account.usage.averages.rpm > 0"
class="text-xs text-gray-500 dark:text-gray-400" class="text-xs text-gray-500 dark:text-gray-400"
> >
平均 {{ account.usage.averages.rpm.toFixed(2) }} RPM {{ t('accounts.averageRpm', { rpm: account.usage.averages.rpm.toFixed(2) }) }}
</div> </div>
</div> </div>
<div v-else class="text-xs text-gray-400">暂无数据</div> <div v-else class="text-xs text-gray-400">{{ t('accounts.noData') }}</div>
</td> </td>
<td class="whitespace-nowrap px-3 py-4"> <td class="whitespace-nowrap px-3 py-4">
<div <div
@@ -609,7 +609,7 @@
v-if="account.sessionWindow.remainingTime > 0" v-if="account.sessionWindow.remainingTime > 0"
class="font-medium text-indigo-600 dark:text-indigo-400" class="font-medium text-indigo-600 dark:text-indigo-400"
> >
剩余 {{ formatRemainingTime(account.sessionWindow.remainingTime) }} {{ t('accounts.remaining', { time: formatRemainingTime(account.sessionWindow.remainingTime) }) }}
</div> </div>
</div> </div>
</div> </div>
@@ -617,7 +617,7 @@
<div v-else-if="account.platform === 'claude-console'" class="space-y-2"> <div v-else-if="account.platform === 'claude-console'" class="space-y-2">
<div v-if="Number(account.dailyQuota) > 0"> <div v-if="Number(account.dailyQuota) > 0">
<div class="flex items-center justify-between text-xs"> <div class="flex items-center justify-between text-xs">
<span class="text-gray-600 dark:text-gray-300">额度进度</span> <span class="text-gray-600 dark:text-gray-300">{{ t('accounts.quotaProgress') }}</span>
<span class="font-medium text-gray-700 dark:text-gray-200"> <span class="font-medium text-gray-700 dark:text-gray-200">
{{ getQuotaUsagePercent(account).toFixed(1) }}% {{ getQuotaUsagePercent(account).toFixed(1) }}%
</span> </span>
@@ -641,9 +641,9 @@
</span> </span>
</div> </div>
<div class="text-xs text-gray-600 dark:text-gray-400"> <div class="text-xs text-gray-600 dark:text-gray-400">
剩余 ${{ formatRemainingQuota(account) }} {{ t('accounts.remainingQuota', { amount: formatRemainingQuota(account) }) }}
<span class="ml-2 text-gray-400" <span class="ml-2 text-gray-400"
>重置 {{ account.quotaResetTime || '00:00' }}</span >{{ t('accounts.reset', { time: account.quotaResetTime || '00:00' }) }}</span
> >
</div> </div>
</div> </div>
@@ -682,11 +682,11 @@
: 'bg-yellow-100 text-yellow-700 hover:bg-yellow-200' : 'bg-yellow-100 text-yellow-700 hover:bg-yellow-200'
]" ]"
:disabled="account.isResetting" :disabled="account.isResetting"
:title="account.isResetting ? '重置中...' : '重置所有异常状态'" :title="account.isResetting ? t('accounts.resetting') : t('accounts.resetStatusTooltip')"
@click="resetAccountStatus(account)" @click="resetAccountStatus(account)"
> >
<i :class="['fas fa-redo', account.isResetting ? 'animate-spin' : '']" /> <i :class="['fas fa-redo', account.isResetting ? 'animate-spin' : '']" />
<span class="ml-1">重置状态</span> <span class="ml-1">{{ t('accounts.resetStatus') }}</span>
</button> </button>
<button <button
:class="[ :class="[
@@ -698,27 +698,27 @@
: 'bg-gray-100 text-gray-700 hover:bg-gray-200' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'
]" ]"
:disabled="account.isTogglingSchedulable" :disabled="account.isTogglingSchedulable"
:title="account.schedulable ? '点击禁用调度' : '点击启用调度'" :title="account.schedulable ? t('accounts.disableTooltip') : t('accounts.enableTooltip')"
@click="toggleSchedulable(account)" @click="toggleSchedulable(account)"
> >
<i :class="['fas', account.schedulable ? 'fa-toggle-on' : 'fa-toggle-off']" /> <i :class="['fas', account.schedulable ? 'fa-toggle-on' : 'fa-toggle-off']" />
<span class="ml-1">{{ account.schedulable ? '调度' : '停用' }}</span> <span class="ml-1">{{ account.schedulable ? t('accounts.scheduling') : t('accounts.disabled') }}</span>
</button> </button>
<button <button
class="rounded bg-blue-100 px-2.5 py-1 text-xs font-medium text-blue-700 transition-colors hover:bg-blue-200" class="rounded bg-blue-100 px-2.5 py-1 text-xs font-medium text-blue-700 transition-colors hover:bg-blue-200"
:title="'编辑账户'" :title="t('accounts.editTooltip')"
@click="editAccount(account)" @click="editAccount(account)"
> >
<i class="fas fa-edit" /> <i class="fas fa-edit" />
<span class="ml-1">编辑</span> <span class="ml-1">{{ t('accounts.edit') }}</span>
</button> </button>
<button <button
class="rounded bg-red-100 px-2.5 py-1 text-xs font-medium text-red-700 transition-colors hover:bg-red-200" class="rounded bg-red-100 px-2.5 py-1 text-xs font-medium text-red-700 transition-colors hover:bg-red-200"
:title="'删除账户'" :title="t('accounts.deleteTooltip')"
@click="deleteAccount(account)" @click="deleteAccount(account)"
> >
<i class="fas fa-trash" /> <i class="fas fa-trash" />
<span class="ml-1">删除</span> <span class="ml-1">{{ t('accounts.delete') }}</span>
</button> </button>
</div> </div>
</td> </td>
@@ -799,12 +799,12 @@
<!-- 使用统计 --> <!-- 使用统计 -->
<div class="mb-3 grid grid-cols-2 gap-3"> <div class="mb-3 grid grid-cols-2 gap-3">
<div> <div>
<p class="text-xs text-gray-500 dark:text-gray-400">今日使用</p> <p class="text-xs text-gray-500 dark:text-gray-400">{{ t('accounts.dailyUsageLabel') }}</p>
<div class="space-y-1"> <div class="space-y-1">
<div class="flex items-center gap-1.5"> <div class="flex items-center gap-1.5">
<div class="h-1.5 w-1.5 rounded-full bg-blue-500" /> <div class="h-1.5 w-1.5 rounded-full bg-blue-500" />
<p class="text-sm font-semibold text-gray-900 dark:text-gray-100"> <p class="text-sm font-semibold text-gray-900 dark:text-gray-100">
{{ account.usage?.daily?.requests || 0 }} {{ account.usage?.daily?.requests || 0 }} {{ t('accounts.requests') }}
</p> </p>
</div> </div>
<div class="flex items-center gap-1.5"> <div class="flex items-center gap-1.5">
@@ -822,7 +822,7 @@
</div> </div>
</div> </div>
<div> <div>
<p class="text-xs text-gray-500 dark:text-gray-400">会话窗口</p> <p class="text-xs text-gray-500 dark:text-gray-400">{{ t('accounts.sessionWindowLabel') }}</p>
<div v-if="account.usage && account.usage.sessionWindow" class="space-y-1"> <div v-if="account.usage && account.usage.sessionWindow" class="space-y-1">
<div class="flex items-center gap-1.5"> <div class="flex items-center gap-1.5">
<div class="h-1.5 w-1.5 rounded-full bg-purple-500" /> <div class="h-1.5 w-1.5 rounded-full bg-purple-500" />
@@ -854,9 +854,9 @@
> >
<div class="flex items-center justify-between text-xs"> <div class="flex items-center justify-between text-xs">
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<span class="font-medium text-gray-600 dark:text-gray-300">会话窗口</span> <span class="font-medium text-gray-600 dark:text-gray-300">{{ t('accounts.sessionWindowLabel') }}</span>
<el-tooltip <el-tooltip
content="会话窗口进度不代表使用量仅表示距离下一个5小时窗口的剩余时间" :content="t('accounts.sessionWindowTooltipMobile')"
placement="top" placement="top"
> >
<i <i
@@ -890,17 +890,17 @@
v-if="account.sessionWindow.remainingTime > 0" v-if="account.sessionWindow.remainingTime > 0"
class="font-medium text-indigo-600" class="font-medium text-indigo-600"
> >
剩余 {{ formatRemainingTime(account.sessionWindow.remainingTime) }} {{ t('accounts.remaining', { time: formatRemainingTime(account.sessionWindow.remainingTime) }) }}
</span> </span>
<span v-else class="text-gray-500"> 已结束 </span> <span v-else class="text-gray-500"> {{ t('accounts.ended') }} </span>
</div> </div>
</div> </div>
<!-- 最后使用时间 --> <!-- 最后使用时间 -->
<div class="flex items-center justify-between text-xs"> <div class="flex items-center justify-between text-xs">
<span class="text-gray-500 dark:text-gray-400">最后使用</span> <span class="text-gray-500 dark:text-gray-400">{{ t('accounts.lastUsedLabel') }}</span>
<span class="text-gray-700 dark:text-gray-200"> <span class="text-gray-700 dark:text-gray-200">
{{ account.lastUsedAt ? formatRelativeTime(account.lastUsedAt) : '从未使用' }} {{ account.lastUsedAt ? formatRelativeTime(account.lastUsedAt) : t('accounts.neverUsed') }}
</span> </span>
</div> </div>
@@ -909,7 +909,7 @@
v-if="account.proxyConfig && account.proxyConfig.type !== 'none'" v-if="account.proxyConfig && account.proxyConfig.type !== 'none'"
class="flex items-center justify-between text-xs" class="flex items-center justify-between text-xs"
> >
<span class="text-gray-500 dark:text-gray-400">代理</span> <span class="text-gray-500 dark:text-gray-400">{{ t('accounts.proxyLabel') }}</span>
<span class="text-gray-700 dark:text-gray-200"> <span class="text-gray-700 dark:text-gray-200">
{{ account.proxyConfig.type.toUpperCase() }} {{ account.proxyConfig.type.toUpperCase() }}
</span> </span>
@@ -917,7 +917,7 @@
<!-- 调度优先级 --> <!-- 调度优先级 -->
<div class="flex items-center justify-between text-xs"> <div class="flex items-center justify-between text-xs">
<span class="text-gray-500 dark:text-gray-400">优先级</span> <span class="text-gray-500 dark:text-gray-400">{{ t('accounts.priorityLabel') }}</span>
<span class="font-medium text-gray-700 dark:text-gray-200"> <span class="font-medium text-gray-700 dark:text-gray-200">
{{ account.priority || 50 }} {{ account.priority || 50 }}
</span> </span>
@@ -937,7 +937,7 @@
@click="toggleSchedulable(account)" @click="toggleSchedulable(account)"
> >
<i :class="['fas', account.schedulable ? 'fa-pause' : 'fa-play']" /> <i :class="['fas', account.schedulable ? 'fa-pause' : 'fa-play']" />
{{ account.schedulable ? '暂停' : '启用' }} {{ account.schedulable ? t('accounts.pause') : t('accounts.enable') }}
</button> </button>
<button <button
@@ -945,7 +945,7 @@
@click="editAccount(account)" @click="editAccount(account)"
> >
<i class="fas fa-edit mr-1" /> <i class="fas fa-edit mr-1" />
编辑 {{ t('accounts.edit') }}
</button> </button>
<button <button
@@ -1001,6 +1001,7 @@
<script setup> <script setup>
import { ref, computed, onMounted, watch } from 'vue' import { ref, computed, onMounted, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { showToast } from '@/utils/toast' import { showToast } from '@/utils/toast'
import { apiClient } from '@/config/api' import { apiClient } from '@/config/api'
import { useConfirm } from '@/composables/useConfirm' import { useConfirm } from '@/composables/useConfirm'
@@ -1009,6 +1010,9 @@ import CcrAccountForm from '@/components/accounts/CcrAccountForm.vue'
import ConfirmModal from '@/components/common/ConfirmModal.vue' import ConfirmModal from '@/components/common/ConfirmModal.vue'
import CustomDropdown from '@/components/common/CustomDropdown.vue' import CustomDropdown from '@/components/common/CustomDropdown.vue'
// 国际化
const { t } = useI18n()
// 使用确认弹窗 // 使用确认弹窗
const { showConfirmModal, confirmOptions, showConfirm, handleConfirm, handleCancel } = useConfirm() const { showConfirmModal, confirmOptions, showConfirm, handleConfirm, handleCancel } = useConfirm()
@@ -1030,30 +1034,30 @@ const groupMembersLoaded = ref(false)
const accountGroupMap = ref(new Map()) // Map<accountId, Array<groupInfo>> const accountGroupMap = ref(new Map()) // Map<accountId, Array<groupInfo>>
// 下拉选项数据 // 下拉选项数据
const sortOptions = ref([ const sortOptions = computed(() => [
{ value: 'name', label: '按名称排序', icon: 'fa-font' }, { value: 'name', label: t('accounts.sortByName'), icon: 'fa-font' },
{ value: 'dailyTokens', label: '按今日Token排序', icon: 'fa-coins' }, { value: 'dailyTokens', label: t('accounts.sortByDailyTokens'), icon: 'fa-coins' },
{ value: 'dailyRequests', label: '按今日请求数排序', icon: 'fa-chart-line' }, { value: 'dailyRequests', label: t('accounts.sortByDailyRequests'), icon: 'fa-chart-line' },
{ value: 'totalTokens', label: '按总Token排序', icon: 'fa-database' }, { value: 'totalTokens', label: t('accounts.sortByTotalTokens'), icon: 'fa-database' },
{ value: 'lastUsed', label: '按最后使用排序', icon: 'fa-clock' } { value: 'lastUsed', label: t('accounts.sortByLastUsed'), icon: 'fa-clock' }
]) ])
const platformOptions = ref([ const platformOptions = computed(() => [
{ value: 'all', label: '所有平台', icon: 'fa-globe' }, { value: 'all', label: t('accounts.allPlatforms'), icon: 'fa-globe' },
{ value: 'claude', label: 'Claude', icon: 'fa-brain' }, { value: 'claude', label: t('accounts.claudePlatform'), icon: 'fa-brain' },
{ value: 'claude-console', label: 'Claude Console', icon: 'fa-terminal' }, { value: 'claude-console', label: t('accounts.claudeConsolePlatform'), icon: 'fa-terminal' },
{ value: 'gemini', label: 'Gemini', icon: 'fa-google' }, { value: 'gemini', label: t('accounts.geminiPlatform'), icon: 'fa-google' },
{ value: 'openai', label: 'OpenAi', icon: 'fa-openai' }, { value: 'openai', label: t('accounts.openaiPlatform'), icon: 'fa-openai' },
{ value: 'azure_openai', label: 'Azure OpenAI', icon: 'fab fa-microsoft' }, { value: 'azure_openai', label: t('accounts.azureOpenaiPlatform'), icon: 'fab fa-microsoft' },
{ value: 'bedrock', label: 'Bedrock', icon: 'fab fa-aws' }, { value: 'bedrock', label: t('accounts.bedrockPlatform'), icon: 'fab fa-aws' },
{ value: 'openai-responses', label: 'OpenAI-Responses', icon: 'fa-server' }, { value: 'openai-responses', label: t('accounts.openaiResponsesPlatform'), icon: 'fa-server' },
{ value: 'ccr', label: 'CCR', icon: 'fa-code-branch' } { value: 'ccr', label: t('accounts.ccrPlatform'), icon: 'fa-code-branch' }
]) ])
const groupOptions = computed(() => { const groupOptions = computed(() => {
const options = [ const options = [
{ value: 'all', label: '所有账户', icon: 'fa-globe' }, { value: 'all', label: t('accounts.allAccounts'), icon: 'fa-globe' },
{ value: 'ungrouped', label: '未分组账户', icon: 'fa-user' } { value: 'ungrouped', label: t('accounts.ungroupedAccounts'), icon: 'fa-user' }
] ]
accountGroups.value.forEach((group) => { accountGroups.value.forEach((group) => {
options.push({ options.push({
@@ -1393,7 +1397,7 @@ const loadAccounts = async (forceReload = false) => {
accounts.value = filteredAccounts accounts.value = filteredAccounts
} catch (error) { } catch (error) {
showToast('加载账户失败', 'error') showToast(t('accounts.loadAccountsFailed'), 'error')
} finally { } finally {
accountsLoading.value = false accountsLoading.value = false
} }
@@ -1425,16 +1429,16 @@ const formatNumber = (num) => {
// 格式化最后使用时间 // 格式化最后使用时间
const formatLastUsed = (dateString) => { const formatLastUsed = (dateString) => {
if (!dateString) return '从未使用' if (!dateString) return t('accounts.neverUsed')
const date = new Date(dateString) const date = new Date(dateString)
const now = new Date() const now = new Date()
const diff = now - date const diff = now - date
if (diff < 60000) return '刚刚' if (diff < 60000) return t('accounts.justNow')
if (diff < 3600000) return `${Math.floor(diff / 60000)} 分钟前` if (diff < 3600000) return t('accounts.minutesAgo', { minutes: Math.floor(diff / 60000) })
if (diff < 86400000) return `${Math.floor(diff / 3600000)} 小时前` if (diff < 86400000) return t('accounts.hoursAgo', { hours: Math.floor(diff / 3600000) })
if (diff < 604800000) return `${Math.floor(diff / 86400000)} 天前` if (diff < 604800000) return t('accounts.daysAgo', { days: Math.floor(diff / 86400000) })
return date.toLocaleDateString('zh-CN') return date.toLocaleDateString('zh-CN')
} }
@@ -1531,15 +1535,15 @@ const formatSessionWindow = (windowStart, windowEnd) => {
// 格式化剩余时间 // 格式化剩余时间
const formatRemainingTime = (minutes) => { const formatRemainingTime = (minutes) => {
if (!minutes || minutes <= 0) return '已结束' if (!minutes || minutes <= 0) return t('accounts.ended')
const hours = Math.floor(minutes / 60) const hours = Math.floor(minutes / 60)
const mins = minutes % 60 const mins = minutes % 60
if (hours > 0) { if (hours > 0) {
return `${hours}小时${mins}分钟` return t('accounts.hoursAndMinutes', { hours, minutes: mins })
} }
return `${mins}分钟` return t('accounts.minutesOnly', { minutes: mins })
} }
// 格式化限流时间(支持显示天数) // 格式化限流时间(支持显示天数)
@@ -1559,18 +1563,18 @@ const formatRateLimitTime = (minutes) => {
if (days > 0) { if (days > 0) {
// 超过1天显示天数和小时 // 超过1天显示天数和小时
if (hours > 0) { if (hours > 0) {
return `${days}天${hours}小时` return t('accounts.daysAndHours', { days, hours })
} }
return `${days}天` return t('accounts.daysOnly', { days })
} else if (hours > 0) { } else if (hours > 0) {
// 超过1小时但不到1天显示小时和分钟 // 超过1小时但不到1天显示小时和分钟
if (mins > 0) { if (mins > 0) {
return `${hours}小时${mins}分钟` return t('accounts.hoursAndMinutes', { hours, minutes: mins })
} }
return `${hours}小时` return t('accounts.hoursOnly', { hours })
} else { } else {
// 不到1小时只显示分钟 // 不到1小时只显示分钟
return `${mins}分钟` return t('accounts.minutesOnly', { minutes: mins })
} }
} }
@@ -1607,17 +1611,17 @@ const deleteAccount = async (account) => {
if (boundKeysCount > 0) { if (boundKeysCount > 0) {
showToast( showToast(
`无法删除此账号,有 ${boundKeysCount} 个API Key绑定到此账号请先解绑所有API Key`, t('accounts.cannotDeleteBoundAccount', { count: boundKeysCount }),
'error' 'error'
) )
return return
} }
const confirmed = await showConfirm( const confirmed = await showConfirm(
'删除账户', t('accounts.deleteAccountTitle'),
`确定要删除账户 "${account.name}" \n\n此操作不可恢复`, t('accounts.deleteAccountMessage', { name: account.name }),
'删除', t('accounts.deleteAccountButton'),
'取消' t('accounts.deleteAccountCancel')
) )
if (!confirmed) return if (!confirmed) return
@@ -1645,15 +1649,15 @@ const deleteAccount = async (account) => {
const data = await apiClient.delete(endpoint) const data = await apiClient.delete(endpoint)
if (data.success) { if (data.success) {
showToast('账户已删除', 'success') showToast(t('accounts.accountDeleted'), 'success')
// 清空分组成员缓存,因为账户可能从分组中移除 // 清空分组成员缓存,因为账户可能从分组中移除
groupMembersLoaded.value = false groupMembersLoaded.value = false
loadAccounts() loadAccounts()
} else { } else {
showToast(data.message || '删除失败', 'error') showToast(data.message || t('accounts.deleteFailed'), 'error')
} }
} catch (error) { } catch (error) {
showToast('删除失败', 'error') showToast(t('accounts.deleteFailed'), 'error')
} }
} }
@@ -1664,13 +1668,13 @@ const resetAccountStatus = async (account) => {
let confirmed = false let confirmed = false
if (window.showConfirm) { if (window.showConfirm) {
confirmed = await window.showConfirm( confirmed = await window.showConfirm(
'重置账户状态', t('accounts.resetStatusConfirmTitle'),
'确定要重置此账户的所有异常状态吗这将清除限流状态、401错误计数等所有异常标记。', t('accounts.resetStatusConfirmMessage'),
'确定重置', t('accounts.resetStatusConfirmButton'),
'取消' t('accounts.resetStatusCancelButton')
) )
} else { } else {
confirmed = confirm('确定要重置此账户的所有异常状态吗?') confirmed = confirm(t('accounts.resetStatusConfirmMessage'))
} }
if (!confirmed) return if (!confirmed) return
@@ -1691,7 +1695,7 @@ const resetAccountStatus = async (account) => {
} else if (account.platform === 'ccr') { } else if (account.platform === 'ccr') {
endpoint = `/admin/ccr-accounts/${account.id}/reset-status` endpoint = `/admin/ccr-accounts/${account.id}/reset-status`
} else { } else {
showToast('不支持的账户类型', 'error') showToast(t('accounts.unsupportedAccountTypeReset'), 'error')
account.isResetting = false account.isResetting = false
return return
} }
@@ -1699,14 +1703,14 @@ const resetAccountStatus = async (account) => {
const data = await apiClient.post(endpoint) const data = await apiClient.post(endpoint)
if (data.success) { if (data.success) {
showToast('账户状态已重置', 'success') showToast(t('accounts.statusResetSuccess'), 'success')
// 强制刷新,绕过前端缓存,确保最终一致性 // 强制刷新,绕过前端缓存,确保最终一致性
loadAccounts(true) loadAccounts(true)
} else { } else {
showToast(data.message || '状态重置失败', 'error') showToast(data.message || t('accounts.statusResetFailed'), 'error')
} }
} catch (error) { } catch (error) {
showToast('状态重置失败', 'error') showToast(t('accounts.statusResetFailed'), 'error')
} finally { } finally {
account.isResetting = false account.isResetting = false
} }
@@ -1737,7 +1741,7 @@ const toggleSchedulable = async (account) => {
} else if (account.platform === 'ccr') { } else if (account.platform === 'ccr') {
endpoint = `/admin/ccr-accounts/${account.id}/toggle-schedulable` endpoint = `/admin/ccr-accounts/${account.id}/toggle-schedulable`
} else { } else {
showToast('该账户类型暂不支持调度控制', 'warning') showToast(t('accounts.unsupportedAccountType'), 'warning')
return return
} }
@@ -1745,12 +1749,12 @@ const toggleSchedulable = async (account) => {
if (data.success) { if (data.success) {
account.schedulable = data.schedulable account.schedulable = data.schedulable
showToast(data.schedulable ? '已启用调度' : '已禁用调度', 'success') showToast(data.schedulable ? t('accounts.enabledScheduling') : t('accounts.disabledScheduling'), 'success')
} else { } else {
showToast(data.message || '操作失败', 'error') showToast(data.message || t('accounts.operationFailed'), 'error')
} }
} catch (error) { } catch (error) {
showToast('切换调度状态失败', 'error') showToast(t('accounts.schedulingToggleFailed'), 'error')
} finally { } finally {
account.isTogglingSchedulable = false account.isTogglingSchedulable = false
} }
@@ -1759,7 +1763,7 @@ const toggleSchedulable = async (account) => {
// 处理创建成功 // 处理创建成功
const handleCreateSuccess = () => { const handleCreateSuccess = () => {
showCreateAccountModal.value = false showCreateAccountModal.value = false
showToast('账户创建成功', 'success') showToast(t('accounts.accountCreateSuccess'), 'success')
// 清空缓存,因为可能涉及分组关系变化 // 清空缓存,因为可能涉及分组关系变化
clearCache() clearCache()
loadAccounts() loadAccounts()
@@ -1768,7 +1772,7 @@ const handleCreateSuccess = () => {
// 处理编辑成功 // 处理编辑成功
const handleEditSuccess = () => { const handleEditSuccess = () => {
showEditAccountModal.value = false showEditAccountModal.value = false
showToast('账户更新成功', 'success') showToast(t('accounts.accountUpdateSuccess'), 'success')
// 清空分组成员缓存,因为账户类型和分组可能发生变化 // 清空分组成员缓存,因为账户类型和分组可能发生变化
groupMembersLoaded.value = false groupMembersLoaded.value = false
loadAccounts() loadAccounts()
@@ -1810,11 +1814,11 @@ const getClaudeAccountType = (account) => {
// 根据 has_claude_max 和 has_claude_pro 判断 // 根据 has_claude_max 和 has_claude_pro 判断
if (info.hasClaudeMax === true) { if (info.hasClaudeMax === true) {
return 'Claude Max' return t('accounts.claudeMax')
} else if (info.hasClaudePro === true) { } else if (info.hasClaudePro === true) {
return 'Claude Pro' return t('accounts.claudePro')
} else { } else {
return 'Claude Free' return t('accounts.claudeFree')
} }
} catch (e) { } catch (e) {
// 解析失败,返回默认值 // 解析失败,返回默认值
@@ -1833,13 +1837,13 @@ const getSchedulableReason = (account) => {
// Claude Console 账户的错误状态 // Claude Console 账户的错误状态
if (account.platform === 'claude-console') { if (account.platform === 'claude-console') {
if (account.status === 'unauthorized') { if (account.status === 'unauthorized') {
return 'API Key无效或已过期401错误' return t('accounts.invalidApiKey')
} }
if (account.overloadStatus === 'overloaded') { if (account.overloadStatus === 'overloaded') {
return '服务过载529错误' return t('accounts.serviceOverload')
} }
if (account.rateLimitStatus === 'limited') { if (account.rateLimitStatus === 'limited') {
return '触发限流429错误' return t('accounts.rateLimitTriggered')
} }
if (account.status === 'blocked' && account.errorMessage) { if (account.status === 'blocked' && account.errorMessage) {
return account.errorMessage return account.errorMessage
@@ -1849,7 +1853,7 @@ const getSchedulableReason = (account) => {
// Claude 官方账户的错误状态 // Claude 官方账户的错误状态
if (account.platform === 'claude') { if (account.platform === 'claude') {
if (account.status === 'unauthorized') { if (account.status === 'unauthorized') {
return '认证失败401错误' return t('accounts.authFailed')
} }
if (account.status === 'temp_error' && account.errorMessage) { if (account.status === 'temp_error' && account.errorMessage) {
return account.errorMessage return account.errorMessage
@@ -1858,7 +1862,7 @@ const getSchedulableReason = (account) => {
return account.errorMessage return account.errorMessage
} }
if (account.isRateLimited) { if (account.isRateLimited) {
return '触发限流429错误' return t('accounts.rateLimitTriggered')
} }
// 自动停止调度的原因 // 自动停止调度的原因
if (account.stoppedReason) { if (account.stoppedReason) {
@@ -1912,15 +1916,15 @@ const getSchedulableReason = (account) => {
} }
// 默认为手动停止 // 默认为手动停止
return '手动停止调度' return t('accounts.manualStop')
} }
// 获取账户状态文本 // 获取账户状态文本
const getAccountStatusText = (account) => { const getAccountStatusText = (account) => {
// 检查是否被封锁 // 检查是否被封锁
if (account.status === 'blocked') return '已封锁' if (account.status === 'blocked') return t('accounts.blocked')
// 检查是否未授权401错误 // 检查是否未授权401错误
if (account.status === 'unauthorized') return '异常' if (account.status === 'unauthorized') return t('accounts.abnormal')
// 检查是否限流 // 检查是否限流
if ( if (
account.isRateLimited || account.isRateLimited ||
@@ -1928,15 +1932,15 @@ const getAccountStatusText = (account) => {
(account.rateLimitStatus && account.rateLimitStatus.isRateLimited) || (account.rateLimitStatus && account.rateLimitStatus.isRateLimited) ||
account.rateLimitStatus === 'limited' account.rateLimitStatus === 'limited'
) )
return '限流中' return t('accounts.rateLimited')
// 检查是否临时错误 // 检查是否临时错误
if (account.status === 'temp_error') return '临时异常' if (account.status === 'temp_error') return t('accounts.tempError')
// 检查是否错误 // 检查是否错误
if (account.status === 'error' || !account.isActive) return '错误' if (account.status === 'error' || !account.isActive) return t('accounts.abnormal')
// 检查是否可调度 // 检查是否可调度
if (account.schedulable === false) return '已暂停' if (account.schedulable === false) return t('accounts.disabled')
// 否则正常 // 否则正常
return '正常' return t('accounts.normal')
} }
// 获取账户状态样式类 // 获取账户状态样式类