feat: 完成web/admin-spa/src/components/apikeys的国际化并修复语法错误和警告

This commit is contained in:
Wangnov
2025-09-10 16:03:01 +08:00
parent 9836f88068
commit 97b94eeff9
35 changed files with 4766 additions and 2061 deletions

View File

@@ -60,11 +60,7 @@
<!-- 刷新按钮 -->
<div class="relative">
<el-tooltip
:content="t('accounts.refreshTooltip')"
effect="dark"
placement="bottom"
>
<el-tooltip :content="t('accounts.refreshTooltip')" effect="dark" placement="bottom">
<button
class="group relative flex items-center justify-center gap-2 rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm transition-all duration-200 hover:border-gray-300 hover:shadow-md disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:border-gray-500 sm:w-auto"
:disabled="accountsLoading"
@@ -110,7 +106,9 @@
<i class="fas fa-user-circle text-xl text-gray-400" />
</div>
<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">{{ t('accounts.noAccountsHint') }}</p>
<p class="mt-2 text-sm text-gray-400 dark:text-gray-500">
{{ t('accounts.noAccountsHint') }}
</p>
</div>
<!-- 桌面端表格视图 -->
@@ -390,7 +388,9 @@
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" />
<span class="text-xs font-semibold text-gray-800">{{ t('accounts.unknown') }}</span>
<span class="text-xs font-semibold text-gray-800">{{
t('accounts.unknown')
}}</span>
</div>
</div>
</td>
@@ -451,7 +451,11 @@
typeof account.rateLimitStatus === 'object' &&
account.rateLimitStatus.minutesRemaining > 0
"
>({{ t('accounts.rateLimitTime', { time: formatRateLimitTime(account.rateLimitStatus.minutesRemaining) }) }})</span
>({{
t('accounts.rateLimitTime', {
time: formatRateLimitTime(account.rateLimitStatus.minutesRemaining)
})
}})</span
>
</span>
<span
@@ -609,7 +613,11 @@
v-if="account.sessionWindow.remainingTime > 0"
class="font-medium text-indigo-600 dark:text-indigo-400"
>
{{ t('accounts.remaining', { time: formatRemainingTime(account.sessionWindow.remainingTime) }) }}
{{
t('accounts.remaining', {
time: formatRemainingTime(account.sessionWindow.remainingTime)
})
}}
</div>
</div>
</div>
@@ -617,7 +625,9 @@
<div v-else-if="account.platform === 'claude-console'" class="space-y-2">
<div v-if="Number(account.dailyQuota) > 0">
<div class="flex items-center justify-between text-xs">
<span class="text-gray-600 dark:text-gray-300">{{ t('accounts.quotaProgress') }}</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">
{{ getQuotaUsagePercent(account).toFixed(1) }}%
</span>
@@ -642,9 +652,9 @@
</div>
<div class="text-xs text-gray-600 dark:text-gray-400">
{{ t('accounts.remainingQuota', { amount: formatRemainingQuota(account) }) }}
<span class="ml-2 text-gray-400"
>{{ t('accounts.reset', { time: account.quotaResetTime || '00:00' }) }}</span
>
<span class="ml-2 text-gray-400">{{
t('accounts.reset', { time: account.quotaResetTime || '00:00' })
}}</span>
</div>
</div>
<div v-else class="text-sm text-gray-400">
@@ -682,7 +692,11 @@
: 'bg-yellow-100 text-yellow-700 hover:bg-yellow-200'
]"
:disabled="account.isResetting"
:title="account.isResetting ? t('accounts.resetting') : t('accounts.resetStatusTooltip')"
:title="
account.isResetting
? t('accounts.resetting')
: t('accounts.resetStatusTooltip')
"
@click="resetAccountStatus(account)"
>
<i :class="['fas fa-redo', account.isResetting ? 'animate-spin' : '']" />
@@ -698,11 +712,17 @@
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
]"
:disabled="account.isTogglingSchedulable"
:title="account.schedulable ? t('accounts.disableTooltip') : t('accounts.enableTooltip')"
:title="
account.schedulable
? t('accounts.disableTooltip')
: t('accounts.enableTooltip')
"
@click="toggleSchedulable(account)"
>
<i :class="['fas', account.schedulable ? 'fa-toggle-on' : 'fa-toggle-off']" />
<span class="ml-1">{{ account.schedulable ? t('accounts.scheduling') : t('accounts.disabled') }}</span>
<span class="ml-1">{{
account.schedulable ? t('accounts.scheduling') : t('accounts.disabled')
}}</span>
</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"
@@ -799,7 +819,9 @@
<!-- 使用统计 -->
<div class="mb-3 grid grid-cols-2 gap-3">
<div>
<p class="text-xs text-gray-500 dark:text-gray-400">{{ t('accounts.dailyUsageLabel') }}</p>
<p class="text-xs text-gray-500 dark:text-gray-400">
{{ t('accounts.dailyUsageLabel') }}
</p>
<div class="space-y-1">
<div class="flex items-center gap-1.5">
<div class="h-1.5 w-1.5 rounded-full bg-blue-500" />
@@ -822,7 +844,9 @@
</div>
</div>
<div>
<p class="text-xs text-gray-500 dark:text-gray-400">{{ t('accounts.sessionWindowLabel') }}</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 class="flex items-center gap-1.5">
<div class="h-1.5 w-1.5 rounded-full bg-purple-500" />
@@ -854,11 +878,10 @@
>
<div class="flex items-center justify-between text-xs">
<div class="flex items-center gap-1">
<span class="font-medium text-gray-600 dark:text-gray-300">{{ t('accounts.sessionWindowLabel') }}</span>
<el-tooltip
:content="t('accounts.sessionWindowTooltipMobile')"
placement="top"
>
<span class="font-medium text-gray-600 dark:text-gray-300">{{
t('accounts.sessionWindowLabel')
}}</span>
<el-tooltip :content="t('accounts.sessionWindowTooltipMobile')" placement="top">
<i
class="fas fa-question-circle cursor-help text-xs text-gray-400 hover:text-gray-600"
/>
@@ -890,7 +913,11 @@
v-if="account.sessionWindow.remainingTime > 0"
class="font-medium text-indigo-600"
>
{{ t('accounts.remaining', { time: formatRemainingTime(account.sessionWindow.remainingTime) }) }}
{{
t('accounts.remaining', {
time: formatRemainingTime(account.sessionWindow.remainingTime)
})
}}
</span>
<span v-else class="text-gray-500"> {{ t('accounts.ended') }} </span>
</div>
@@ -898,9 +925,15 @@
<!-- 最后使用时间 -->
<div class="flex items-center justify-between text-xs">
<span class="text-gray-500 dark:text-gray-400">{{ t('accounts.lastUsedLabel') }}</span>
<span class="text-gray-500 dark:text-gray-400">{{
t('accounts.lastUsedLabel')
}}</span>
<span class="text-gray-700 dark:text-gray-200">
{{ account.lastUsedAt ? formatRelativeTime(account.lastUsedAt) : t('accounts.neverUsed') }}
{{
account.lastUsedAt
? formatRelativeTime(account.lastUsedAt)
: t('accounts.neverUsed')
}}
</span>
</div>
@@ -917,7 +950,9 @@
<!-- 调度优先级 -->
<div class="flex items-center justify-between text-xs">
<span class="text-gray-500 dark:text-gray-400">{{ t('accounts.priorityLabel') }}</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">
{{ account.priority || 50 }}
</span>
@@ -1610,10 +1645,7 @@ const deleteAccount = async (account) => {
).length
if (boundKeysCount > 0) {
showToast(
t('accounts.cannotDeleteBoundAccount', { count: boundKeysCount }),
'error'
)
showToast(t('accounts.cannotDeleteBoundAccount', { count: boundKeysCount }), 'error')
return
}
@@ -1749,7 +1781,10 @@ const toggleSchedulable = async (account) => {
if (data.success) {
account.schedulable = data.schedulable
showToast(data.schedulable ? t('accounts.enabledScheduling') : t('accounts.disabledScheduling'), 'success')
showToast(
data.schedulable ? t('accounts.enabledScheduling') : t('accounts.disabledScheduling'),
'success'
)
} else {
showToast(data.message || t('accounts.operationFailed'), 'error')
}

View File

@@ -68,7 +68,10 @@
icon="fa-calendar-alt"
icon-color="text-blue-500"
:options="timeRangeOptions"
:placeholder="timeRangeOptions.find(o => o.value === 'today')?.label || t('apiKeys.timeRange.today')"
:placeholder="
timeRangeOptions.find((o) => o.value === 'today')?.label ||
t('apiKeys.timeRange.today')
"
@change="loadApiKeys()"
/>
</div>
@@ -84,7 +87,9 @@
icon="fa-tags"
icon-color="text-purple-500"
:options="tagOptions"
:placeholder="tagOptions.find(o => o.value === '')?.label || t('apiKeys.allTags')"
:placeholder="
tagOptions.find((o) => o.value === '')?.label || t('apiKeys.allTags')
"
@change="currentPage = 1"
/>
<span
@@ -105,7 +110,11 @@
<input
v-model="searchKeyword"
class="w-full rounded-lg border border-gray-200 bg-white px-3 py-2 pl-9 text-sm text-gray-700 placeholder-gray-400 shadow-sm transition-all duration-200 hover:border-gray-300 focus:border-cyan-500 focus:outline-none focus:ring-2 focus:ring-cyan-500/20 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200 dark:placeholder-gray-500 dark:hover:border-gray-500"
:placeholder="isLdapEnabled ? t('apiKeys.searchPlaceholderWithOwner') : t('apiKeys.searchPlaceholder')"
:placeholder="
isLdapEnabled
? t('apiKeys.searchPlaceholderWithOwner')
: t('apiKeys.searchPlaceholder')
"
type="text"
@input="currentPage = 1"
/>
@@ -148,7 +157,9 @@
class="absolute -inset-0.5 rounded-lg bg-gradient-to-r from-blue-500 to-indigo-500 opacity-0 blur transition duration-300 group-hover:opacity-20"
></div>
<i class="fas fa-edit relative text-blue-600 dark:text-blue-400" />
<span class="relative">{{ t('apiKeys.bulkEdit') }} ({{ selectedApiKeys.length }})</span>
<span class="relative"
>{{ t('apiKeys.bulkEdit') }} ({{ selectedApiKeys.length }})</span
>
</button>
<!-- 批量删除按钮 - 移到刷新按钮旁边 -->
@@ -161,7 +172,9 @@
class="absolute -inset-0.5 rounded-lg bg-gradient-to-r from-red-500 to-pink-500 opacity-0 blur transition duration-300 group-hover:opacity-20"
></div>
<i class="fas fa-trash relative text-red-600 dark:text-red-400" />
<span class="relative">{{ t('apiKeys.bulkDelete') }} ({{ selectedApiKeys.length }})</span>
<span class="relative"
>{{ t('apiKeys.bulkDelete') }} ({{ selectedApiKeys.length }})</span
>
</button>
</div>
@@ -471,25 +484,34 @@
<!-- 今日使用统计 -->
<div class="mb-2">
<div class="mb-1 flex items-center justify-between text-sm">
<span class="text-gray-600 dark:text-gray-400">{{ t('apiKeys.dailyRequests') }}</span>
<span class="text-gray-600 dark:text-gray-400">{{
t('apiKeys.dailyRequests')
}}</span>
<span class="font-semibold text-gray-900 dark:text-gray-100"
>{{ formatNumber(key.usage?.daily?.requests || 0) }}{{ t('apiKeys.requests') }}</span
>{{ formatNumber(key.usage?.daily?.requests || 0)
}}{{ t('apiKeys.requests') }}</span
>
</div>
<div class="flex items-center justify-between text-sm">
<span class="text-gray-600 dark:text-gray-400">{{ t('apiKeys.dailyCost') }}</span>
<span class="text-gray-600 dark:text-gray-400">{{
t('apiKeys.dailyCost')
}}</span>
<span class="font-semibold text-green-600"
>${{ (key.dailyCost || 0).toFixed(4) }}</span
>
</div>
<div class="flex items-center justify-between text-sm">
<span class="text-gray-600 dark:text-gray-400">{{ t('apiKeys.totalCost') }}</span>
<span class="text-gray-600 dark:text-gray-400">{{
t('apiKeys.totalCost')
}}</span>
<span class="font-semibold text-blue-600"
>${{ (key.totalCost || 0).toFixed(4) }}</span
>
</div>
<div class="flex items-center justify-between text-sm">
<span class="text-gray-600 dark:text-gray-400">{{ t('apiKeys.lastUsed') }}</span>
<span class="text-gray-600 dark:text-gray-400">{{
t('apiKeys.lastUsed')
}}</span>
<span class="font-medium text-gray-700 dark:text-gray-300">{{
formatLastUsed(key.lastUsedAt)
}}</span>
@@ -499,7 +521,9 @@
<!-- 每日费用限制进度条 -->
<div v-if="key.dailyCostLimit > 0" class="space-y-1">
<div class="flex items-center justify-between text-xs">
<span class="text-gray-500 dark:text-gray-400">{{ t('apiKeys.dailyLimit') }}</span>
<span class="text-gray-500 dark:text-gray-400">{{
t('apiKeys.dailyLimit')
}}</span>
<span class="text-gray-700 dark:text-gray-300">
${{ (key.dailyCost || 0).toFixed(2) }} / ${{
key.dailyCostLimit.toFixed(2)
@@ -518,7 +542,9 @@
<!-- Opus 周费用限制进度条 -->
<div v-if="key.weeklyOpusCostLimit > 0" class="space-y-1">
<div class="flex items-center justify-between text-xs">
<span class="text-gray-500 dark:text-gray-400">{{ t('apiKeys.weeklyOpusLimit') }}</span>
<span class="text-gray-500 dark:text-gray-400">{{
t('apiKeys.weeklyOpusLimit')
}}</span>
<span class="text-gray-700 dark:text-gray-300">
${{ (key.weeklyOpusCost || 0).toFixed(2) }} / ${{
key.weeklyOpusCostLimit.toFixed(2)
@@ -703,7 +729,9 @@
<td class="bg-gray-50 px-3 py-4 dark:bg-gray-700" colspan="8">
<div v-if="!apiKeyModelStats[key.id]" class="py-4 text-center">
<div class="loading-spinner mx-auto" />
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">{{ t('apiKeys.loadingModelStats') }}</p>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
{{ t('apiKeys.loadingModelStats') }}
</p>
</div>
<div class="space-y-4">
<!-- 通用的标题和时间筛选器,无论是否有数据都显示 -->
@@ -719,7 +747,11 @@
v-if="apiKeyModelStats[key.id] && apiKeyModelStats[key.id].length > 0"
class="rounded-full bg-gray-100 px-2 py-1 text-xs text-gray-500 dark:bg-gray-700 dark:text-gray-400"
>
{{ t('apiKeys.modelStatsCount', { count: apiKeyModelStats[key.id].length }) }}
{{
t('apiKeys.modelStatsCount', {
count: apiKeyModelStats[key.id].length
})
}}
</span>
<!-- API Keys日期筛选器 -->
@@ -773,7 +805,9 @@
>
<div class="mb-3 flex items-center justify-center gap-2">
<i class="fas fa-chart-line text-lg text-gray-400" />
<p class="text-sm text-gray-500 dark:text-gray-400">{{ t('apiKeys.noModelData') }}</p>
<p class="text-sm text-gray-500 dark:text-gray-400">
{{ t('apiKeys.noModelData') }}
</p>
<button
class="ml-2 flex items-center gap-1 text-sm text-blue-500 transition-colors hover:text-blue-700"
:title="t('apiKeys.resetFilter')"
@@ -1078,7 +1112,9 @@
<!-- 今日使用 -->
<div class="rounded-lg bg-gray-50 p-3 dark:bg-gray-700">
<div class="mb-2 flex items-center justify-between">
<span class="text-xs text-gray-600 dark:text-gray-400">{{ t('apiKeys.dailyUsage') }}</span>
<span class="text-xs text-gray-600 dark:text-gray-400">{{
t('apiKeys.dailyUsage')
}}</span>
<button
class="text-xs text-blue-600 hover:text-blue-800"
@click="showUsageDetails(key)"
@@ -1089,19 +1125,26 @@
<div class="grid grid-cols-2 gap-3">
<div>
<p class="text-sm font-semibold text-gray-900 dark:text-gray-100">
{{ formatNumber(key.usage?.daily?.requests || 0) }} {{ t('apiKeys.requests') }}
{{ formatNumber(key.usage?.daily?.requests || 0) }}
{{ t('apiKeys.requests') }}
</p>
<p class="text-xs text-gray-500 dark:text-gray-400">
{{ t('apiKeys.requests') }}
</p>
<p class="text-xs text-gray-500 dark:text-gray-400">{{ t('apiKeys.requests') }}</p>
</div>
<div>
<p class="text-sm font-semibold text-green-600">
${{ (key.dailyCost || 0).toFixed(4) }}
</p>
<p class="text-xs text-gray-500 dark:text-gray-400">{{ t('apiKeys.totalCost') }}</p>
<p class="text-xs text-gray-500 dark:text-gray-400">
{{ t('apiKeys.totalCost') }}
</p>
</div>
</div>
<div class="mt-2 flex items-center justify-between">
<span class="text-xs text-gray-600 dark:text-gray-400">{{ t('apiKeys.lastUsed') }}</span>
<span class="text-xs text-gray-600 dark:text-gray-400">{{
t('apiKeys.lastUsed')
}}</span>
<span class="text-xs font-medium text-gray-700 dark:text-gray-300">{{
formatLastUsed(key.lastUsedAt)
}}</span>
@@ -1111,7 +1154,9 @@
<!-- 限制进度 -->
<div v-if="key.dailyCostLimit > 0" class="space-y-1">
<div class="flex items-center justify-between text-xs">
<span class="text-gray-500 dark:text-gray-400">{{ t('apiKeys.dailyLimit') }}</span>
<span class="text-gray-500 dark:text-gray-400">{{
t('apiKeys.dailyLimit')
}}</span>
<span class="text-gray-700 dark:text-gray-300">
${{ (key.dailyCost || 0).toFixed(2) }} / ${{ key.dailyCostLimit.toFixed(2) }}
</span>
@@ -1250,7 +1295,9 @@
{{ t('apiKeys.totalRecords', { count: sortedApiKeys.length }) }}
</span>
<div class="flex items-center gap-2">
<span class="text-xs text-gray-600 dark:text-gray-400 sm:text-sm">{{ t('apiKeys.pageSize') }}</span>
<span class="text-xs text-gray-600 dark:text-gray-400 sm:text-sm">{{
t('apiKeys.pageSize')
}}</span>
<select
v-model="pageSize"
class="rounded-md border border-gray-200 bg-white px-2 py-1 text-xs text-gray-700 transition-colors hover:border-gray-300 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:border-gray-500 sm:text-sm"
@@ -1260,7 +1307,9 @@
{{ size }}
</option>
</select>
<span class="text-xs text-gray-600 dark:text-gray-400 sm:text-sm">{{ t('apiKeys.records') }}</span>
<span class="text-xs text-gray-600 dark:text-gray-400 sm:text-sm">{{
t('apiKeys.records')
}}</span>
</div>
</div>
@@ -1475,19 +1524,26 @@
<td class="px-3 py-4">
<div class="text-sm">
<div class="flex items-center justify-between">
<span class="text-gray-600 dark:text-gray-400">{{ t('apiKeys.requests') }}</span>
<span class="text-gray-600 dark:text-gray-400">{{
t('apiKeys.requests')
}}</span>
<span class="font-semibold text-gray-900 dark:text-gray-100">
{{ formatNumber(key.usage?.total?.requests || 0) }} {{ t('apiKeys.requests') }}
{{ formatNumber(key.usage?.total?.requests || 0) }}
{{ t('apiKeys.requests') }}
</span>
</div>
<div class="flex items-center justify-between">
<span class="text-gray-600 dark:text-gray-400">{{ t('apiKeys.totalCost') }}</span>
<span class="text-gray-600 dark:text-gray-400">{{
t('apiKeys.totalCost')
}}</span>
<span class="font-semibold text-green-600">
${{ (key.usage?.total?.cost || 0).toFixed(4) }}
</span>
</div>
<div v-if="key.lastUsedAt" class="flex items-center justify-between">
<span class="text-gray-600 dark:text-gray-400">{{ t('apiKeys.lastUsed') }}</span>
<span class="text-gray-600 dark:text-gray-400">{{
t('apiKeys.lastUsed')
}}</span>
<span class="font-medium text-gray-700 dark:text-gray-300">
{{ formatLastUsed(key.lastUsedAt) }}
</span>
@@ -2350,9 +2406,7 @@ const toggleApiKeyStatus = async (key) => {
)
} else {
// 降级方案
confirmed = confirm(
t('apiKeys.confirmDisable', { name: key.name })
)
confirmed = confirm(t('apiKeys.confirmDisable', { name: key.name }))
}
}
@@ -2539,7 +2593,12 @@ const batchDeleteApiKeys = async () => {
const message = t('apiKeys.confirmBatchDelete', { count: selectedCount })
if (window.showConfirm) {
confirmed = await window.showConfirm(t('apiKeys.confirmBatchDelete').split(' ')[0], message, t('common.confirm'), t('common.cancel'))
confirmed = await window.showConfirm(
t('apiKeys.confirmBatchDelete').split(' ')[0],
message,
t('common.confirm'),
t('common.cancel')
)
} else {
confirmed = confirm(message)
}
@@ -2562,7 +2621,10 @@ const batchDeleteApiKeys = async () => {
// 如果有失败的,显示详细信息
if (failedCount > 0) {
const errorMessages = errors.map((e) => `${e.keyId}: ${e.error}`).join('\n')
showToast(t('apiKeys.batchPartialFail', { failed: failedCount }) + ':\n' + errorMessages, 'warning')
showToast(
t('apiKeys.batchPartialFail', { failed: failedCount }) + ':\n' + errorMessages,
'warning'
)
}
} else {
showToast(t('apiKeys.batchAllFailed'), 'error')

View File

@@ -33,7 +33,9 @@
to="/user-login"
>
<i class="fas fa-user text-sm md:text-base" />
<span class="text-xs font-semibold tracking-wide md:text-sm">{{ t('apiStats.userLogin') }}</span>
<span class="text-xs font-semibold tracking-wide md:text-sm">{{
t('apiStats.userLogin')
}}</span>
</router-link>
<!-- 管理后台按钮 -->
<router-link
@@ -42,7 +44,9 @@
to="/dashboard"
>
<i class="fas fa-shield-alt text-sm md:text-base" />
<span class="text-xs font-semibold tracking-wide md:text-sm">{{ t('apiStats.adminPanel') }}</span>
<span class="text-xs font-semibold tracking-wide md:text-sm">{{
t('apiStats.adminPanel')
}}</span>
</router-link>
</div>
</div>
@@ -97,9 +101,9 @@
>
<div class="flex items-center gap-2 md:gap-3">
<i class="fas fa-clock text-base text-blue-500 md:text-lg" />
<span class="text-base font-medium text-gray-700 dark:text-gray-200 md:text-lg"
>{{ t('apiStats.timeRange') }}</span
>
<span class="text-base font-medium text-gray-700 dark:text-gray-200 md:text-lg">{{
t('apiStats.timeRange')
}}</span>
</div>
<div class="flex w-full gap-2 md:w-auto">
<button
@@ -190,7 +194,7 @@ const currentTutorialComponent = computed(() => {
const components = {
'zh-cn': TutorialViewZhCn,
'zh-tw': TutorialViewZhTw,
'en': TutorialViewEn
en: TutorialViewEn
}
return components[locale] || TutorialViewZhCn
})

View File

@@ -42,7 +42,12 @@
dashboardData.accountsByPlatform.claude.total > 0
"
class="inline-flex items-center gap-0.5"
:title="t('dashboard.claudeAccount', { total: dashboardData.accountsByPlatform.claude.total, normal: dashboardData.accountsByPlatform.claude.normal })"
:title="
t('dashboard.claudeAccount', {
total: dashboardData.accountsByPlatform.claude.total,
normal: dashboardData.accountsByPlatform.claude.normal
})
"
>
<i class="fas fa-brain text-xs text-indigo-600" />
<span class="text-xs font-medium text-gray-700 dark:text-gray-300">{{
@@ -56,7 +61,12 @@
dashboardData.accountsByPlatform['claude-console'].total > 0
"
class="inline-flex items-center gap-0.5"
:title="t('dashboard.consoleAccount', { total: dashboardData.accountsByPlatform['claude-console'].total, normal: dashboardData.accountsByPlatform['claude-console'].normal })"
:title="
t('dashboard.consoleAccount', {
total: dashboardData.accountsByPlatform['claude-console'].total,
normal: dashboardData.accountsByPlatform['claude-console'].normal
})
"
>
<i class="fas fa-terminal text-xs text-purple-600" />
<span class="text-xs font-medium text-gray-700 dark:text-gray-300">{{
@@ -70,7 +80,12 @@
dashboardData.accountsByPlatform.gemini.total > 0
"
class="inline-flex items-center gap-0.5"
:title="t('dashboard.geminiAccount', { total: dashboardData.accountsByPlatform.gemini.total, normal: dashboardData.accountsByPlatform.gemini.normal })"
:title="
t('dashboard.geminiAccount', {
total: dashboardData.accountsByPlatform.gemini.total,
normal: dashboardData.accountsByPlatform.gemini.normal
})
"
>
<i class="fas fa-robot text-xs text-yellow-600" />
<span class="text-xs font-medium text-gray-700 dark:text-gray-300">{{
@@ -84,7 +99,12 @@
dashboardData.accountsByPlatform.bedrock.total > 0
"
class="inline-flex items-center gap-0.5"
:title="t('dashboard.bedrockAccount', { total: dashboardData.accountsByPlatform.bedrock.total, normal: dashboardData.accountsByPlatform.bedrock.normal })"
:title="
t('dashboard.bedrockAccount', {
total: dashboardData.accountsByPlatform.bedrock.total,
normal: dashboardData.accountsByPlatform.bedrock.normal
})
"
>
<i class="fab fa-aws text-xs text-orange-600" />
<span class="text-xs font-medium text-gray-700 dark:text-gray-300">{{
@@ -98,7 +118,12 @@
dashboardData.accountsByPlatform.openai.total > 0
"
class="inline-flex items-center gap-0.5"
:title="t('dashboard.openaiAccount', { total: dashboardData.accountsByPlatform.openai.total, normal: dashboardData.accountsByPlatform.openai.normal })"
:title="
t('dashboard.openaiAccount', {
total: dashboardData.accountsByPlatform.openai.total,
normal: dashboardData.accountsByPlatform.openai.normal
})
"
>
<i class="fas fa-openai text-xs text-gray-100" />
<span class="text-xs font-medium text-gray-700 dark:text-gray-300">{{
@@ -112,7 +137,12 @@
dashboardData.accountsByPlatform.azure_openai.total > 0
"
class="inline-flex items-center gap-0.5"
:title="t('dashboard.azureOpenaiAccount', { total: dashboardData.accountsByPlatform.azure_openai.total, normal: dashboardData.accountsByPlatform.azure_openai.normal })"
:title="
t('dashboard.azureOpenaiAccount', {
total: dashboardData.accountsByPlatform.azure_openai.total,
normal: dashboardData.accountsByPlatform.azure_openai.normal
})
"
>
<i class="fab fa-microsoft text-xs text-blue-600" />
<span class="text-xs font-medium text-gray-700 dark:text-gray-300">{{
@@ -153,7 +183,8 @@
{{ dashboardData.todayRequests }}
</p>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
{{ t('dashboard.totalRequests') }}: {{ formatNumber(dashboardData.totalRequests || 0) }}
{{ t('dashboard.totalRequests') }}:
{{ formatNumber(dashboardData.totalRequests || 0) }}
</p>
</div>
<div class="stat-icon flex-shrink-0 bg-gradient-to-br from-purple-500 to-purple-600">
@@ -303,7 +334,9 @@
<div>
<p class="mb-1 text-xs font-semibold text-gray-600 dark:text-gray-400 sm:text-sm">
{{ t('dashboard.realtimeRPM') }}
<span class="text-xs text-gray-400">({{ dashboardData.metricsWindow }}{{ t('dashboard.minutes') }})</span>
<span class="text-xs text-gray-400"
>({{ dashboardData.metricsWindow }}{{ t('dashboard.minutes') }})</span
>
</p>
<p class="text-2xl font-bold text-orange-600 sm:text-3xl">
{{ dashboardData.realtimeRPM || 0 }}
@@ -326,7 +359,9 @@
<div>
<p class="mb-1 text-xs font-semibold text-gray-600 dark:text-gray-400 sm:text-sm">
{{ t('dashboard.realtimeTPM') }}
<span class="text-xs text-gray-400">({{ dashboardData.metricsWindow }}{{ t('dashboard.minutes') }})</span>
<span class="text-xs text-gray-400"
>({{ dashboardData.metricsWindow }}{{ t('dashboard.minutes') }})</span
>
</p>
<p class="text-2xl font-bold text-rose-600 sm:text-3xl">
{{ formatNumber(dashboardData.realtimeTPM || 0) }}
@@ -453,7 +488,9 @@
@click="refreshAllData()"
>
<i :class="['fas fa-sync-alt text-xs', { 'animate-spin': isRefreshing }]" />
<span class="hidden sm:inline">{{ isRefreshing ? t('dashboard.refreshing') : t('dashboard.refresh') }}</span>
<span class="hidden sm:inline">{{
isRefreshing ? t('dashboard.refreshing') : t('dashboard.refresh')
}}</span>
</button>
</div>
</div>
@@ -579,7 +616,9 @@
]"
@click="((apiKeysTrendMetric = 'requests'), updateApiKeysUsageTrendChart())"
>
<i class="fas fa-exchange-alt mr-1" /><span class="hidden sm:inline">{{ t('dashboard.requestsCount') }}</span
<i class="fas fa-exchange-alt mr-1" /><span class="hidden sm:inline">{{
t('dashboard.requestsCount')
}}</span
><span class="sm:hidden">{{ t('dashboard.requestsCount').split(' ')[0] }}</span>
</button>
<button
@@ -591,7 +630,9 @@
]"
@click="((apiKeysTrendMetric = 'tokens'), updateApiKeysUsageTrendChart())"
>
<i class="fas fa-coins mr-1" /><span class="hidden sm:inline">{{ t('dashboard.tokenCount') }}</span
<i class="fas fa-coins mr-1" /><span class="hidden sm:inline">{{
t('dashboard.tokenCount')
}}</span
><span class="sm:hidden">Token</span>
</button>
</div>
@@ -600,7 +641,9 @@
<span v-if="apiKeysTrendData.totalApiKeys > 10">
{{ t('dashboard.showingTop10', { count: apiKeysTrendData.totalApiKeys }) }}
</span>
<span v-else>{{ t('dashboard.totalApiKeysCount', { count: apiKeysTrendData.totalApiKeys }) }}</span>
<span v-else>{{
t('dashboard.totalApiKeysCount', { count: apiKeysTrendData.totalApiKeys })
}}</span>
</div>
<div class="sm:h-[350px]" style="height: 300px">
<canvas ref="apiKeysUsageTrendChart" />
@@ -1164,7 +1207,10 @@ function createApiKeysUsageTrendChart() {
beginAtZero: true,
title: {
display: true,
text: apiKeysTrendMetric.value === 'tokens' ? t('dashboard.tokenQuantity') : t('dashboard.requestsQuantity'),
text:
apiKeysTrendMetric.value === 'tokens'
? t('dashboard.tokenQuantity')
: t('dashboard.requestsQuantity'),
color: chartColors.value.text
},
ticks: {

View File

@@ -42,7 +42,8 @@
<form class="space-y-4 sm:space-y-6" @submit.prevent="handleLogin">
<div>
<label class="mb-2 block text-sm font-semibold text-gray-900 dark:text-gray-100 sm:mb-3"
<label
class="mb-2 block text-sm font-semibold text-gray-900 dark:text-gray-100 sm:mb-3"
>{{ t('login.username') }}</label
>
<input
@@ -55,7 +56,8 @@
</div>
<div>
<label class="mb-2 block text-sm font-semibold text-gray-900 dark:text-gray-100 sm:mb-3"
<label
class="mb-2 block text-sm font-semibold text-gray-900 dark:text-gray-100 sm:mb-3"
>{{ t('login.password') }}</label
>
<input

View File

@@ -6,7 +6,9 @@
<h3 class="mb-1 text-lg font-bold text-gray-900 dark:text-gray-100 sm:mb-2 sm:text-xl">
{{ t('settings.title') }}
</h3>
<p class="text-sm text-gray-600 dark:text-gray-400 sm:text-base">{{ t('settings.description') }}</p>
<p class="text-sm text-gray-600 dark:text-gray-400 sm:text-base">
{{ t('settings.description') }}
</p>
</div>
<!-- 设置分类导航 -->
@@ -66,7 +68,9 @@
<div class="text-sm font-semibold text-gray-900 dark:text-gray-100">
{{ t('settings.siteName') }}
</div>
<div class="text-xs text-gray-500 dark:text-gray-400">{{ t('settings.siteNameDescription') }}</div>
<div class="text-xs text-gray-500 dark:text-gray-400">
{{ t('settings.siteNameDescription') }}
</div>
</div>
</div>
</td>
@@ -75,7 +79,7 @@
v-model="oemSettings.siteName"
class="form-input w-full max-w-md dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200"
maxlength="100"
:placeholder="t('settings.siteNamePlaceholder')"
:placeholder="t('settings.siteNamePlaceholder')"
type="text"
/>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
@@ -97,7 +101,9 @@
<div class="text-sm font-semibold text-gray-900 dark:text-gray-100">
{{ t('settings.siteIcon') }}
</div>
<div class="text-xs text-gray-500 dark:text-gray-400">{{ t('settings.siteIconDescription') }}</div>
<div class="text-xs text-gray-500 dark:text-gray-400">
{{ t('settings.siteIconDescription') }}
</div>
</div>
</div>
</td>
@@ -109,12 +115,14 @@
class="inline-flex items-center gap-3 rounded-lg bg-gray-50 p-3 dark:bg-gray-700"
>
<img
:alt="t('settings.iconPreview')"
:alt="t('settings.iconPreview')"
class="h-8 w-8"
:src="oemSettings.siteIconData || oemSettings.siteIcon"
@error="handleIconError"
/>
<span class="text-sm text-gray-600 dark:text-gray-400">{{ t('settings.currentIcon') }}</span>
<span class="text-sm text-gray-600 dark:text-gray-400">{{
t('settings.currentIcon')
}}</span>
<button
class="rounded-lg px-3 py-1 font-medium text-red-600 transition-colors hover:bg-red-50 hover:text-red-900"
@click="removeIcon"
@@ -139,9 +147,9 @@
<i class="fas fa-upload mr-2" />
{{ t('settings.uploadIcon') }}
</button>
<span class="ml-3 text-xs text-gray-500 dark:text-gray-400"
>{{ t('settings.iconFormats') }}</span
>
<span class="ml-3 text-xs text-gray-500 dark:text-gray-400">{{
t('settings.iconFormats')
}}</span>
</div>
</div>
</td>
@@ -160,7 +168,9 @@
<div class="text-sm font-semibold text-gray-900 dark:text-gray-100">
{{ t('settings.adminEntry') }}
</div>
<div class="text-xs text-gray-500 dark:text-gray-400">{{ t('settings.adminEntryDescription') }}</div>
<div class="text-xs text-gray-500 dark:text-gray-400">
{{ t('settings.adminEntryDescription') }}
</div>
</div>
</div>
</td>
@@ -172,7 +182,9 @@
class="peer relative h-6 w-11 rounded-full bg-gray-200 after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-all after:content-[''] peer-checked:bg-blue-600 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:border-gray-600 dark:bg-gray-700 dark:peer-focus:ring-blue-800"
></div>
<span class="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300">{{
hideAdminButton ? t('settings.hideLoginButton') : t('settings.showLoginButton')
hideAdminButton
? t('settings.hideLoginButton')
: t('settings.showLoginButton')
}}</span>
</label>
</div>
@@ -213,7 +225,9 @@
class="text-sm text-gray-500 dark:text-gray-400"
>
<i class="fas fa-clock mr-1" />
{{ t('settings.lastUpdated', { time: formatDateTime(oemSettings.updatedAt) }) }}
{{
t('settings.lastUpdated', { time: formatDateTime(oemSettings.updatedAt) })
}}
</div>
</div>
</td>
@@ -233,15 +247,19 @@
<i class="fas fa-tag"></i>
</div>
<div>
<h3 class="text-base font-semibold text-gray-900 dark:text-gray-100">{{ t('settings.siteNameCard') }}</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">{{ t('settings.siteNameCardDesc') }}</p>
<h3 class="text-base font-semibold text-gray-900 dark:text-gray-100">
{{ t('settings.siteNameCard') }}
</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">
{{ t('settings.siteNameCardDesc') }}
</p>
</div>
</div>
<input
v-model="oemSettings.siteName"
class="form-input w-full dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200"
maxlength="100"
:placeholder="t('settings.siteNamePlaceholder')"
:placeholder="t('settings.siteNamePlaceholder')"
type="text"
/>
</div>
@@ -255,7 +273,9 @@
<i class="fas fa-image"></i>
</div>
<div>
<h3 class="text-base font-semibold text-gray-900 dark:text-gray-100">{{ t('settings.siteIconCard') }}</h3>
<h3 class="text-base font-semibold text-gray-900 dark:text-gray-100">
{{ t('settings.siteIconCard') }}
</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">
{{ t('settings.siteIconCardDesc') }}
</p>
@@ -268,12 +288,14 @@
class="inline-flex items-center gap-3 rounded-lg bg-gray-50 p-3 dark:bg-gray-700"
>
<img
:alt="t('settings.iconPreview')"
:alt="t('settings.iconPreview')"
class="h-8 w-8"
:src="oemSettings.siteIconData || oemSettings.siteIcon"
@error="handleIconError"
/>
<span class="text-sm text-gray-600 dark:text-gray-400">{{ t('settings.currentIcon') }}</span>
<span class="text-sm text-gray-600 dark:text-gray-400">{{
t('settings.currentIcon')
}}</span>
<button
class="rounded-lg px-3 py-1 font-medium text-red-600 transition-colors hover:bg-red-50 hover:text-red-900"
@click="removeIcon"
@@ -314,8 +336,12 @@
<i class="fas fa-eye-slash"></i>
</div>
<div>
<h3 class="text-base font-semibold text-gray-900 dark:text-gray-100">{{ t('settings.adminEntryCard') }}</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">{{ t('settings.adminEntryCardDesc') }}</p>
<h3 class="text-base font-semibold text-gray-900 dark:text-gray-100">
{{ t('settings.adminEntryCard') }}
</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">
{{ t('settings.adminEntryCardDesc') }}
</p>
</div>
</div>
<div class="space-y-2">
@@ -362,7 +388,9 @@
class="text-center text-sm text-gray-500 dark:text-gray-400"
>
<i class="fas fa-clock mr-1" />
{{ t('settings.lastUpdatedMobile', { time: formatDateTime(oemSettings.updatedAt) }) }}
{{
t('settings.lastUpdatedMobile', { time: formatDateTime(oemSettings.updatedAt) })
}}
</div>
</div>
</div>
@@ -402,7 +430,9 @@
<div
class="mb-6 rounded-lg bg-white/80 p-6 shadow-lg backdrop-blur-sm dark:bg-gray-800/80"
>
<h2 class="mb-4 text-lg font-semibold text-gray-800 dark:text-gray-200">{{ t('settings.notificationTypes') }}</h2>
<h2 class="mb-4 text-lg font-semibold text-gray-800 dark:text-gray-200">
{{ t('settings.notificationTypes') }}
</h2>
<div class="space-y-3">
<div
v-for="(enabled, type) in webhookConfig.notificationTypes"
@@ -1207,7 +1237,10 @@ const savePlatform = async () => {
}
if (response.success && isMounted.value) {
showToast(editingPlatform.value ? t('settings.platformUpdated') : t('settings.platformAdded'), 'success')
showToast(
editingPlatform.value ? t('settings.platformUpdated') : t('settings.platformAdded'),
'success'
)
await loadWebhookConfig()
closePlatformModal()
}

View File

@@ -17,8 +17,8 @@ const currentTutorialComponent = computed(() => {
const components = {
'zh-cn': TutorialViewZhCn,
'zh-tw': TutorialViewZhTw,
'en': TutorialViewEn
en: TutorialViewEn
}
return components[locale] || TutorialViewZhCn
})
</script>
</script>

View File

@@ -72,7 +72,8 @@
</div>
<div class="flex items-center space-x-4">
<div class="text-sm text-gray-700 dark:text-gray-300">
{{ t('user.dashboard.welcome') }}, <span class="font-medium">{{ userStore.userName }}</span>
{{ t('user.dashboard.welcome') }},
<span class="font-medium">{{ userStore.userName }}</span>
</div>
<!-- 主题切换按钮 -->
@@ -94,7 +95,9 @@
<!-- Overview Tab -->
<div v-if="activeTab === 'overview'" class="space-y-6">
<div>
<h1 class="text-2xl font-semibold text-gray-900 dark:text-white">{{ t('user.dashboard.title') }}</h1>
<h1 class="text-2xl font-semibold text-gray-900 dark:text-white">
{{ t('user.dashboard.title') }}
</h1>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
{{ t('user.dashboard.welcomeMessage') }}
</p>
@@ -272,25 +275,33 @@
<div class="mt-5 border-t border-gray-200 dark:border-gray-700">
<dl class="divide-y divide-gray-200 dark:divide-gray-700">
<div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5">
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">{{ t('user.dashboard.username') }}</dt>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">
{{ t('user.dashboard.username') }}
</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-white sm:col-span-2 sm:mt-0">
{{ userProfile?.username }}
</dd>
</div>
<div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5">
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">{{ t('user.dashboard.displayName') }}</dt>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">
{{ t('user.dashboard.displayName') }}
</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-white sm:col-span-2 sm:mt-0">
{{ userProfile?.displayName || t('user.dashboard.notAvailable') }}
</dd>
</div>
<div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5">
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">{{ t('user.dashboard.email') }}</dt>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">
{{ t('user.dashboard.email') }}
</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-white sm:col-span-2 sm:mt-0">
{{ userProfile?.email || t('user.dashboard.notAvailable') }}
</dd>
</div>
<div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5">
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">{{ t('user.dashboard.role') }}</dt>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">
{{ t('user.dashboard.role') }}
</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-white sm:col-span-2 sm:mt-0">
<span
class="inline-flex items-center rounded-full bg-blue-100 px-2.5 py-0.5 text-xs font-medium text-blue-800 dark:bg-blue-900 dark:text-blue-200"
@@ -300,13 +311,17 @@
</dd>
</div>
<div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5">
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">{{ t('user.dashboard.memberSince') }}</dt>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">
{{ t('user.dashboard.memberSince') }}
</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-white sm:col-span-2 sm:mt-0">
{{ formatDate(userProfile?.createdAt) }}
</dd>
</div>
<div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5">
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">{{ t('user.dashboard.lastLogin') }}</dt>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">
{{ t('user.dashboard.lastLogin') }}
</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-white sm:col-span-2 sm:mt-0">
{{ formatDate(userProfile?.lastLoginAt) || t('user.dashboard.notAvailable') }}
</dd>

View File

@@ -3,7 +3,9 @@
<!-- Header -->
<div class="sm:flex sm:items-center">
<div class="sm:flex-auto">
<h1 class="text-2xl font-semibold text-gray-900 dark:text-white">{{ t('user.management.title') }}</h1>
<h1 class="text-2xl font-semibold text-gray-900 dark:text-white">
{{ t('user.management.title') }}
</h1>
<p class="mt-2 text-sm text-gray-700 dark:text-gray-300">
{{ t('user.management.description') }}
</p>
@@ -254,7 +256,9 @@
fill="currentColor"
></path>
</svg>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">{{ t('user.management.loadingUsers') }}</p>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
{{ t('user.management.loadingUsers') }}
</p>
</div>
<!-- Users List -->
@@ -299,7 +303,9 @@
: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'
]"
>
{{ user.isActive ? t('user.management.active') : t('user.management.disabled') }}
{{
user.isActive ? t('user.management.active') : t('user.management.disabled')
}}
</span>
<span
:class="[
@@ -328,8 +334,14 @@
v-if="user.totalUsage"
class="mt-1 flex items-center space-x-4 text-xs text-gray-400 dark:text-gray-500"
>
<span>{{ formatNumber(user.totalUsage.requests || 0) }} {{ t('user.management.requests') }}</span>
<span>${{ (user.totalUsage.totalCost || 0).toFixed(4) }} {{ t('user.management.totalCostLabel') }}</span>
<span
>{{ formatNumber(user.totalUsage.requests || 0) }}
{{ t('user.management.requests') }}</span
>
<span
>${{ (user.totalUsage.totalCost || 0).toFixed(4) }}
{{ t('user.management.totalCostLabel') }}</span
>
</div>
</div>
</div>
@@ -337,7 +349,7 @@
<!-- View Usage Stats -->
<button
class="inline-flex items-center rounded border border-transparent p-1 text-gray-400 hover:text-blue-600"
:title="t('user.management.viewUsageStats')"
:title="t('user.management.viewUsageStats')"
@click="viewUserStats(user)"
>
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -354,7 +366,7 @@
<button
class="inline-flex items-center rounded border border-transparent p-1 text-gray-400 hover:text-red-600 disabled:cursor-not-allowed disabled:opacity-50"
:disabled="user.apiKeyCount === 0"
:title="t('user.management.disableAllApiKeys')"
:title="t('user.management.disableAllApiKeys')"
@click="disableUserApiKeys(user)"
>
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -375,7 +387,9 @@
? 'text-gray-400 hover:text-red-600'
: 'text-gray-400 hover:text-green-600'
]"
:title="user.isActive ? t('user.management.disableUser') : t('user.management.enableUser')"
:title="
user.isActive ? t('user.management.disableUser') : t('user.management.enableUser')
"
@click="toggleUserStatus(user)"
>
<svg
@@ -405,7 +419,7 @@
<!-- Change Role -->
<button
class="inline-flex items-center rounded border border-transparent p-1 text-gray-400 hover:text-purple-600"
:title="t('user.management.changeRole')"
:title="t('user.management.changeRole')"
@click="changeUserRole(user)"
>
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -437,7 +451,9 @@
stroke-width="2"
/>
</svg>
<h3 class="mt-2 text-sm font-medium text-gray-900 dark:text-white">{{ t('user.management.noUsersFound') }}</h3>
<h3 class="mt-2 text-sm font-medium text-gray-900 dark:text-white">
{{ t('user.management.noUsersFound') }}
</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{{
searchQuery ? t('user.management.noUsersMatch') : t('user.management.noUsersCreated')
@@ -597,7 +613,9 @@ const viewUserStats = (user) => {
const toggleUserStatus = (user) => {
selectedUser.value = user
confirmAction.value = {
title: user.isActive ? t('user.management.disableUserTitle') : t('user.management.enableUserTitle'),
title: user.isActive
? t('user.management.disableUserTitle')
: t('user.management.enableUserTitle'),
message: user.isActive
? t('user.management.disableUserMessage', { username: user.username })
: t('user.management.enableUserMessage', { username: user.username }),
@@ -614,7 +632,10 @@ const disableUserApiKeys = (user) => {
selectedUser.value = user
confirmAction.value = {
title: t('user.management.disableAllKeysTitle'),
message: t('user.management.disableAllKeysMessage', { count: user.apiKeyCount, username: user.username }),
message: t('user.management.disableAllKeysMessage', {
count: user.apiKeyCount,
username: user.username
}),
confirmText: t('user.management.disableKeys'),
confirmClass: 'bg-red-600 hover:bg-red-700',
action: 'disableKeys'
@@ -642,13 +663,21 @@ const handleConfirmAction = async () => {
if (userIndex !== -1) {
users.value[userIndex].isActive = !user.isActive
}
showToast(user.isActive ? t('user.management.userDisabledSuccess') : t('user.management.userEnabledSuccess'), 'success')
showToast(
user.isActive
? t('user.management.userDisabledSuccess')
: t('user.management.userEnabledSuccess'),
'success'
)
}
} else if (action === 'disableKeys') {
const response = await apiClient.post(`/users/${user.id}/disable-keys`)
if (response.success) {
showToast(t('user.management.keysDisabledSuccess', { count: response.disabledCount }), 'success')
showToast(
t('user.management.keysDisabledSuccess', { count: response.disabledCount }),
'success'
)
await loadUsers() // Refresh to get updated counts
}
}

View File

@@ -81,7 +81,9 @@
>
file
</li>
<li>Follow the installation wizard to complete installation, keep default settings</li>
<li>
Follow the installation wizard to complete installation, keep default settings
</li>
</ol>
</div>
<div class="mb-3 sm:mb-4">
@@ -105,14 +107,18 @@
<ul class="space-y-1 text-xs text-blue-700 sm:text-sm sm:text-xs">
<li> Recommend using PowerShell instead of CMD</li>
<li> If you encounter permission issues, try running as administrator</li>
<li> Some antivirus software may flag as false positive, need to add to whitelist</li>
<li>
Some antivirus software may flag as false positive, need to add to whitelist
</li>
</ul>
</div>
</div>
<!-- Verify Installation -->
<div class="rounded-lg border border-green-200 bg-green-50 p-3 sm:p-4">
<h6 class="mb-2 text-sm font-medium text-green-800 sm:text-base">Verify Installation Success</h6>
<h6 class="mb-2 text-sm font-medium text-green-800 sm:text-base">
Verify Installation Success
</h6>
<p class="mb-2 text-xs text-green-700 sm:mb-3 sm:text-sm">
After installation completes, open PowerShell or CMD and enter the following commands:
</p>
@@ -122,7 +128,9 @@
<div class="whitespace-nowrap text-gray-300">node --version</div>
<div class="whitespace-nowrap text-gray-300">npm --version</div>
</div>
<p class="mt-2 text-xs text-green-700 sm:text-sm">If version numbers are displayed, installation was successful!</p>
<p class="mt-2 text-xs text-green-700 sm:text-sm">
If version numbers are displayed, installation was successful!
</p>
</div>
</div>
@@ -159,7 +167,8 @@
</div>
</div>
<p class="text-sm text-gray-600 dark:text-gray-400">
This command will download and install the latest version of Claude Code from the official npm repository.
This command will download and install the latest version of Claude Code from the
official npm repository.
</p>
<div class="mt-4 rounded-lg border border-blue-200 bg-blue-50 p-3 sm:p-4">
@@ -173,15 +182,21 @@
<!-- Verify Installation -->
<div class="rounded-lg border border-green-200 bg-green-50 p-3 sm:p-4">
<h6 class="mb-2 font-medium text-green-800 dark:text-green-300">Verify Claude Code Installation</h6>
<p class="mb-3 text-sm text-green-700">After installation completes, enter the following command to check if installation was successful:</p>
<h6 class="mb-2 font-medium text-green-800 dark:text-green-300">
Verify Claude Code Installation
</h6>
<p class="mb-3 text-sm text-green-700">
After installation completes, enter the following command to check if installation was
successful:
</p>
<div
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
>
<div class="whitespace-nowrap text-gray-300">claude --version</div>
</div>
<p class="mt-2 text-sm text-green-700">
If version number is displayed, congratulations! Claude Code has been successfully installed.
If version number is displayed, congratulations! Claude Code has been successfully
installed.
</p>
</div>
</div>
@@ -230,7 +245,8 @@
</div>
</div>
<p class="mt-2 text-xs text-yellow-700">
💡 Remember to replace "your-api-key" with the actual key created in the "API Keys" tab above.
💡 Remember to replace "your-api-key" with the actual key created in the "API Keys"
tab above.
</p>
</div>
@@ -280,9 +296,12 @@
<!-- Verify Environment Variable Setup -->
<div class="mt-6 rounded-lg border border-blue-200 bg-blue-50 p-3 sm:p-4">
<h6 class="mb-2 font-medium text-blue-800 dark:text-blue-300">Verify Environment Variable Setup</h6>
<h6 class="mb-2 font-medium text-blue-800 dark:text-blue-300">
Verify Environment Variable Setup
</h6>
<p class="mb-3 text-sm text-blue-700">
After setting environment variables, you can verify if they were set successfully with the following commands:
After setting environment variables, you can verify if they were set successfully with
the following commands:
</p>
<div class="space-y-4">
@@ -320,7 +339,8 @@
<div>cr_xxxxxxxxxxxxxxxxxx</div>
</div>
<p class="text-xs text-blue-700">
💡 If output is empty or shows variable name itself, environment variable setup failed, please reconfigure.
💡 If output is empty or shows variable name itself, environment variable setup
failed, please reconfigure.
</p>
</div>
</div>
@@ -416,7 +436,8 @@
Configure Codex Environment Variables
</h5>
<p class="mb-3 text-sm text-gray-700 dark:text-gray-300 sm:mb-4 sm:text-base">
If you use tools that support OpenAI API (such as Codex), you need to set the following environment variables:
If you use tools that support OpenAI API (such as Codex), you need to set the following
environment variables:
</p>
<div class="space-y-4">
@@ -528,8 +549,8 @@
<ul class="list-inside list-disc space-y-1 text-sm">
<li>Run PowerShell as Administrator</li>
<li>
Or configure npm to use user directory: <code
class="rounded bg-gray-200 px-1 text-xs sm:text-sm"
Or configure npm to use user directory:
<code class="rounded bg-gray-200 px-1 text-xs sm:text-sm"
>npm config set prefix %APPDATA%\npm</code
>
</li>
@@ -567,7 +588,8 @@
<li>Restart PowerShell or CMD</li>
<li>Or log out and log back into Windows</li>
<li>
Verify settings: <code class="rounded bg-gray-200 px-1 text-xs sm:text-sm"
Verify settings:
<code class="rounded bg-gray-200 px-1 text-xs sm:text-sm"
>echo $env:ANTHROPIC_BASE_URL</code
>
</li>
@@ -605,7 +627,8 @@
<div class="mb-4">
<p class="mb-3 text-gray-700">Method 1: Using Homebrew (Recommended)</p>
<p class="mb-2 text-xs text-gray-600 dark:text-gray-400 sm:text-sm">
If you already have Homebrew installed, using it to install Node.js will be more convenient:
If you already have Homebrew installed, using it to install Node.js will be more
convenient:
</p>
<div
class="overflow-x-auto rounded-lg bg-gray-900 p-3 font-mono text-xs text-green-400 sm:p-4 sm:text-sm"
@@ -655,15 +678,21 @@
<!-- Verify Installation -->
<div class="rounded-lg border border-green-200 bg-green-50 p-3 sm:p-4">
<h6 class="mb-2 font-medium text-green-800 dark:text-green-300">Verify Installation Success</h6>
<p class="mb-3 text-sm text-green-700">After installation, open Terminal and enter the following commands:</p>
<h6 class="mb-2 font-medium text-green-800 dark:text-green-300">
Verify Installation Success
</h6>
<p class="mb-3 text-sm text-green-700">
After installation, open Terminal and enter the following commands:
</p>
<div
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
>
<div class="whitespace-nowrap text-gray-300">node --version</div>
<div class="whitespace-nowrap text-gray-300">npm --version</div>
</div>
<p class="mt-2 text-sm text-green-700">If version numbers are displayed, the installation was successful!</p>
<p class="mt-2 text-sm text-green-700">
If version numbers are displayed, the installation was successful!
</p>
</div>
</div>
@@ -699,7 +728,9 @@
npm install -g @anthropic-ai/claude-code
</div>
</div>
<p class="mb-2 text-sm text-gray-600">If you encounter permission issues, you can use sudo:</p>
<p class="mb-2 text-sm text-gray-600">
If you encounter permission issues, you can use sudo:
</p>
<div
class="overflow-x-auto rounded-lg bg-gray-900 p-3 font-mono text-xs text-green-400 sm:p-4 sm:text-sm"
>
@@ -711,15 +742,21 @@
<!-- Verify Installation -->
<div class="rounded-lg border border-green-200 bg-green-50 p-3 sm:p-4">
<h6 class="mb-2 font-medium text-green-800 dark:text-green-300">Verify Claude Code Installation</h6>
<p class="mb-3 text-sm text-green-700">After installation completes, enter the following command to check if installation was successful:</p>
<h6 class="mb-2 font-medium text-green-800 dark:text-green-300">
Verify Claude Code Installation
</h6>
<p class="mb-3 text-sm text-green-700">
After installation completes, enter the following command to check if installation was
successful:
</p>
<div
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
>
<div class="whitespace-nowrap text-gray-300">claude --version</div>
</div>
<p class="mt-2 text-sm text-green-700">
If version number is displayed, congratulations! Claude Code has been successfully installed.
If version number is displayed, congratulations! Claude Code has been successfully
installed.
</p>
</div>
</div>
@@ -766,7 +803,8 @@
</div>
</div>
<p class="mt-2 text-xs text-yellow-700">
💡 Remember to replace "your-api-key" with the actual key created in the "API Keys" tab above.
💡 Remember to replace "your-api-key" with the actual key created in the "API Keys"
tab above.
</p>
</div>
@@ -903,7 +941,8 @@
Configure Codex Environment Variables
</h5>
<p class="mb-3 text-sm text-gray-700 dark:text-gray-300 sm:mb-4 sm:text-base">
If you use tools that support OpenAI API (such as Codex), you need to set the following environment variables:
If you use tools that support OpenAI API (such as Codex), you need to set the following
environment variables:
</p>
<div class="space-y-4">
@@ -1014,13 +1053,14 @@
<p class="mb-2">Try the following solutions:</p>
<ul class="list-inside list-disc space-y-1 text-sm">
<li>
Install with sudo: <code class="rounded bg-gray-200 px-1 text-xs sm:text-sm"
Install with sudo:
<code class="rounded bg-gray-200 px-1 text-xs sm:text-sm"
>sudo npm install -g @anthropic-ai/claude-code</code
>
</li>
<li>
Or configure npm to use user directory: <code
class="rounded bg-gray-200 px-1 text-xs sm:text-sm"
Or configure npm to use user directory:
<code class="rounded bg-gray-200 px-1 text-xs sm:text-sm"
>npm config set prefix ~/.npm-global</code
>
</li>
@@ -1040,7 +1080,8 @@
<li>Open "System Preferences" "Security & Privacy"</li>
<li>Click "Allow Anyway" or "Open Anyway"</li>
<li>
Or run in Terminal: <code class="rounded bg-gray-200 px-1 text-xs sm:text-sm"
Or run in Terminal:
<code class="rounded bg-gray-200 px-1 text-xs sm:text-sm"
>sudo spctl --master-disable</code
>
</li>
@@ -1057,10 +1098,13 @@
<div class="px-3 pb-3 text-gray-600 sm:px-4 sm:pb-4">
<p class="mb-2">Check the following points:</p>
<ul class="list-inside list-disc space-y-1 text-sm">
<li>Confirm you modified the correct configuration file (.zshrc or .bash_profile)</li>
<li>
Confirm you modified the correct configuration file (.zshrc or .bash_profile)
</li>
<li>Restart Terminal</li>
<li>
Verify settings: <code class="rounded bg-gray-200 px-1 text-xs sm:text-sm"
Verify settings:
<code class="rounded bg-gray-200 px-1 text-xs sm:text-sm"
>echo $ANTHROPIC_BASE_URL</code
>
</li>
@@ -1127,7 +1171,10 @@
<h6 class="mb-2 text-sm font-medium text-orange-800 sm:text-base">Linux Notes</h6>
<ul class="space-y-1 text-xs text-orange-700 sm:text-sm">
<li>• Some distributions may require additional dependencies</li>
<li>• If you encounter permission issues, use <code class="rounded bg-orange-200 px-1">sudo</code></li>
<li>
• If you encounter permission issues, use
<code class="rounded bg-orange-200 px-1">sudo</code>
</li>
<li>• Ensure your user has write permissions to npm's global directory</li>
</ul>
</div>
@@ -1135,15 +1182,21 @@
<!-- Verify Installation -->
<div class="rounded-lg border border-green-200 bg-green-50 p-3 sm:p-4">
<h6 class="mb-2 font-medium text-green-800 dark:text-green-300">Verify Installation Success</h6>
<p class="mb-3 text-sm text-green-700">After installation, open terminal and enter the following commands:</p>
<h6 class="mb-2 font-medium text-green-800 dark:text-green-300">
Verify Installation Success
</h6>
<p class="mb-3 text-sm text-green-700">
After installation, open terminal and enter the following commands:
</p>
<div
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
>
<div class="whitespace-nowrap text-gray-300">node --version</div>
<div class="whitespace-nowrap text-gray-300">npm --version</div>
</div>
<p class="mt-2 text-sm text-green-700">If version numbers are displayed, the installation was successful!</p>
<p class="mt-2 text-sm text-green-700">
If version numbers are displayed, the installation was successful!
</p>
</div>
</div>
@@ -1179,7 +1232,9 @@
npm install -g @anthropic-ai/claude-code
</div>
</div>
<p class="mb-2 text-sm text-gray-600">If you encounter permission issues, you can use sudo:</p>
<p class="mb-2 text-sm text-gray-600">
If you encounter permission issues, you can use sudo:
</p>
<div
class="overflow-x-auto rounded-lg bg-gray-900 p-3 font-mono text-xs text-green-400 sm:p-4 sm:text-sm"
>
@@ -1191,15 +1246,21 @@
<!-- Verify Installation -->
<div class="rounded-lg border border-green-200 bg-green-50 p-3 sm:p-4">
<h6 class="mb-2 font-medium text-green-800 dark:text-green-300">Verify Claude Code Installation</h6>
<p class="mb-3 text-sm text-green-700">After installation completes, enter the following command to check if installation was successful:</p>
<h6 class="mb-2 font-medium text-green-800 dark:text-green-300">
Verify Claude Code Installation
</h6>
<p class="mb-3 text-sm text-green-700">
After installation completes, enter the following command to check if installation was
successful:
</p>
<div
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
>
<div class="whitespace-nowrap text-gray-300">claude --version</div>
</div>
<p class="mt-2 text-sm text-green-700">
If version number is displayed, congratulations! Claude Code has been successfully installed.
If version number is displayed, congratulations! Claude Code has been successfully
installed.
</p>
</div>
</div>
@@ -1246,7 +1307,8 @@
</div>
</div>
<p class="mt-2 text-xs text-yellow-700">
💡 Remember to replace "your-api-key" with the actual key created in the "API Keys" tab above.
💡 Remember to replace "your-api-key" with the actual key created in the "API Keys"
tab above.
</p>
</div>
@@ -1381,7 +1443,8 @@
Configure Codex Environment Variables
</h5>
<p class="mb-3 text-sm text-gray-700 dark:text-gray-300 sm:mb-4 sm:text-base">
If you use tools that support OpenAI API (such as Codex), you need to set the following environment variables:
If you use tools that support OpenAI API (such as Codex), you need to set the following
environment variables:
</p>
<div class="space-y-4">
@@ -1492,18 +1555,20 @@
<p class="mb-2">Try the following solutions:</p>
<ul class="list-inside list-disc space-y-1 text-sm">
<li>
Install with sudo: <code class="rounded bg-gray-200 px-1 text-xs sm:text-sm"
Install with sudo:
<code class="rounded bg-gray-200 px-1 text-xs sm:text-sm"
>sudo npm install -g @anthropic-ai/claude-code</code
>
</li>
<li>
Or configure npm to use user directory: <code
class="rounded bg-gray-200 px-1 text-xs sm:text-sm"
Or configure npm to use user directory:
<code class="rounded bg-gray-200 px-1 text-xs sm:text-sm"
>npm config set prefix ~/.npm-global</code
>
</li>
<li>
Then add to PATH: <code class="rounded bg-gray-200 px-1 text-xs sm:text-sm"
Then add to PATH:
<code class="rounded bg-gray-200 px-1 text-xs sm:text-sm"
>export PATH=~/.npm-global/bin:$PATH</code
>
</li>
@@ -1547,7 +1612,8 @@
<code class="rounded bg-gray-200 px-1 text-xs sm:text-sm">source ~/.bashrc</code>
</li>
<li>
Verify settings: <code class="rounded bg-gray-200 px-1 text-xs sm:text-sm"
Verify settings:
<code class="rounded bg-gray-200 px-1 text-xs sm:text-sm"
>echo $ANTHROPIC_BASE_URL</code
>
</li>
@@ -1564,10 +1630,12 @@
>
<h5 class="mb-2 text-lg font-semibold sm:text-xl">🎉 Congratulations!</h5>
<p class="mb-3 text-sm text-blue-100 sm:mb-4 sm:text-base">
You have successfully installed and configured Claude Code. Now you can start enjoying the convenience brought by AI programming assistant.
You have successfully installed and configured Claude Code. Now you can start enjoying the
convenience brought by AI programming assistant.
</p>
<p class="text-xs text-blue-200 sm:text-sm">
If you encounter any issues during use, you can check the official documentation or community discussions for help.
If you encounter any issues during use, you can check the official documentation or
community discussions for help.
</p>
</div>
</div>