style(frontend): format code with prettier

格式化前端业务代码,符合代码规范
- 统一代码风格
- 修复 ESLint 警告
This commit is contained in:
ianshaw
2025-12-24 18:07:58 -08:00
parent 372a01290b
commit 938ffb002e
4 changed files with 443 additions and 270 deletions

View File

@@ -3,7 +3,7 @@
* Handles AI platform account management for administrators * Handles AI platform account management for administrators
*/ */
import { apiClient } from '../client'; import { apiClient } from '../client'
import type { import type {
Account, Account,
CreateAccountRequest, CreateAccountRequest,
@@ -13,7 +13,7 @@ import type {
WindowStats, WindowStats,
ClaudeModel, ClaudeModel,
AccountUsageStatsResponse, AccountUsageStatsResponse,
} from '@/types'; } from '@/types'
/** /**
* List all accounts with pagination * List all accounts with pagination
@@ -26,10 +26,10 @@ export async function list(
page: number = 1, page: number = 1,
pageSize: number = 20, pageSize: number = 20,
filters?: { filters?: {
platform?: string; platform?: string
type?: string; type?: string
status?: string; status?: string
search?: string; search?: string
} }
): Promise<PaginatedResponse<Account>> { ): Promise<PaginatedResponse<Account>> {
const { data } = await apiClient.get<PaginatedResponse<Account>>('/admin/accounts', { const { data } = await apiClient.get<PaginatedResponse<Account>>('/admin/accounts', {
@@ -38,8 +38,8 @@ export async function list(
page_size: pageSize, page_size: pageSize,
...filters, ...filters,
}, },
}); })
return data; return data
} }
/** /**
@@ -48,8 +48,8 @@ export async function list(
* @returns Account details * @returns Account details
*/ */
export async function getById(id: number): Promise<Account> { export async function getById(id: number): Promise<Account> {
const { data } = await apiClient.get<Account>(`/admin/accounts/${id}`); const { data } = await apiClient.get<Account>(`/admin/accounts/${id}`)
return data; return data
} }
/** /**
@@ -58,8 +58,8 @@ export async function getById(id: number): Promise<Account> {
* @returns Created account * @returns Created account
*/ */
export async function create(accountData: CreateAccountRequest): Promise<Account> { export async function create(accountData: CreateAccountRequest): Promise<Account> {
const { data } = await apiClient.post<Account>('/admin/accounts', accountData); const { data } = await apiClient.post<Account>('/admin/accounts', accountData)
return data; return data
} }
/** /**
@@ -69,8 +69,8 @@ export async function create(accountData: CreateAccountRequest): Promise<Account
* @returns Updated account * @returns Updated account
*/ */
export async function update(id: number, updates: UpdateAccountRequest): Promise<Account> { export async function update(id: number, updates: UpdateAccountRequest): Promise<Account> {
const { data } = await apiClient.put<Account>(`/admin/accounts/${id}`, updates); const { data } = await apiClient.put<Account>(`/admin/accounts/${id}`, updates)
return data; return data
} }
/** /**
@@ -79,8 +79,8 @@ export async function update(id: number, updates: UpdateAccountRequest): Promise
* @returns Success confirmation * @returns Success confirmation
*/ */
export async function deleteAccount(id: number): Promise<{ message: string }> { export async function deleteAccount(id: number): Promise<{ message: string }> {
const { data } = await apiClient.delete<{ message: string }>(`/admin/accounts/${id}`); const { data } = await apiClient.delete<{ message: string }>(`/admin/accounts/${id}`)
return data; return data
} }
/** /**
@@ -89,11 +89,8 @@ export async function deleteAccount(id: number): Promise<{ message: string }> {
* @param status - New status * @param status - New status
* @returns Updated account * @returns Updated account
*/ */
export async function toggleStatus( export async function toggleStatus(id: number, status: 'active' | 'inactive'): Promise<Account> {
id: number, return update(id, { status })
status: 'active' | 'inactive'
): Promise<Account> {
return update(id, { status });
} }
/** /**
@@ -102,16 +99,16 @@ export async function toggleStatus(
* @returns Test result * @returns Test result
*/ */
export async function testAccount(id: number): Promise<{ export async function testAccount(id: number): Promise<{
success: boolean; success: boolean
message: string; message: string
latency_ms?: number; latency_ms?: number
}> { }> {
const { data } = await apiClient.post<{ const { data } = await apiClient.post<{
success: boolean; success: boolean
message: string; message: string
latency_ms?: number; latency_ms?: number
}>(`/admin/accounts/${id}/test`); }>(`/admin/accounts/${id}/test`)
return data; return data
} }
/** /**
@@ -120,8 +117,8 @@ export async function testAccount(id: number): Promise<{
* @returns Updated account * @returns Updated account
*/ */
export async function refreshCredentials(id: number): Promise<Account> { export async function refreshCredentials(id: number): Promise<Account> {
const { data } = await apiClient.post<Account>(`/admin/accounts/${id}/refresh`); const { data } = await apiClient.post<Account>(`/admin/accounts/${id}/refresh`)
return data; return data
} }
/** /**
@@ -133,8 +130,8 @@ export async function refreshCredentials(id: number): Promise<Account> {
export async function getStats(id: number, days: number = 30): Promise<AccountUsageStatsResponse> { export async function getStats(id: number, days: number = 30): Promise<AccountUsageStatsResponse> {
const { data } = await apiClient.get<AccountUsageStatsResponse>(`/admin/accounts/${id}/stats`, { const { data } = await apiClient.get<AccountUsageStatsResponse>(`/admin/accounts/${id}/stats`, {
params: { days }, params: { days },
}); })
return data; return data
} }
/** /**
@@ -143,8 +140,8 @@ export async function getStats(id: number, days: number = 30): Promise<AccountUs
* @returns Updated account * @returns Updated account
*/ */
export async function clearError(id: number): Promise<Account> { export async function clearError(id: number): Promise<Account> {
const { data } = await apiClient.post<Account>(`/admin/accounts/${id}/clear-error`); const { data } = await apiClient.post<Account>(`/admin/accounts/${id}/clear-error`)
return data; return data
} }
/** /**
@@ -153,8 +150,8 @@ export async function clearError(id: number): Promise<Account> {
* @returns Account usage info * @returns Account usage info
*/ */
export async function getUsage(id: number): Promise<AccountUsageInfo> { export async function getUsage(id: number): Promise<AccountUsageInfo> {
const { data } = await apiClient.get<AccountUsageInfo>(`/admin/accounts/${id}/usage`); const { data } = await apiClient.get<AccountUsageInfo>(`/admin/accounts/${id}/usage`)
return data; return data
} }
/** /**
@@ -163,8 +160,10 @@ export async function getUsage(id: number): Promise<AccountUsageInfo> {
* @returns Success confirmation * @returns Success confirmation
*/ */
export async function clearRateLimit(id: number): Promise<{ message: string }> { export async function clearRateLimit(id: number): Promise<{ message: string }> {
const { data } = await apiClient.post<{ message: string }>(`/admin/accounts/${id}/clear-rate-limit`); const { data } = await apiClient.post<{ message: string }>(
return data; `/admin/accounts/${id}/clear-rate-limit`
)
return data
} }
/** /**
@@ -177,8 +176,8 @@ export async function generateAuthUrl(
endpoint: string, endpoint: string,
config: { proxy_id?: number } config: { proxy_id?: number }
): Promise<{ auth_url: string; session_id: string }> { ): Promise<{ auth_url: string; session_id: string }> {
const { data } = await apiClient.post<{ auth_url: string; session_id: string }>(endpoint, config); const { data } = await apiClient.post<{ auth_url: string; session_id: string }>(endpoint, config)
return data; return data
} }
/** /**
@@ -191,8 +190,8 @@ export async function exchangeCode(
endpoint: string, endpoint: string,
exchangeData: { session_id: string; code: string; proxy_id?: number } exchangeData: { session_id: string; code: string; proxy_id?: number }
): Promise<Record<string, unknown>> { ): Promise<Record<string, unknown>> {
const { data } = await apiClient.post<Record<string, unknown>>(endpoint, exchangeData); const { data } = await apiClient.post<Record<string, unknown>>(endpoint, exchangeData)
return data; return data
} }
/** /**
@@ -201,16 +200,16 @@ export async function exchangeCode(
* @returns Results of batch creation * @returns Results of batch creation
*/ */
export async function batchCreate(accounts: CreateAccountRequest[]): Promise<{ export async function batchCreate(accounts: CreateAccountRequest[]): Promise<{
success: number; success: number
failed: number; failed: number
results: Array<{ success: boolean; account?: Account; error?: string }>; results: Array<{ success: boolean; account?: Account; error?: string }>
}> { }> {
const { data } = await apiClient.post<{ const { data } = await apiClient.post<{
success: number; success: number
failed: number; failed: number
results: Array<{ success: boolean; account?: Account; error?: string }>; results: Array<{ success: boolean; account?: Account; error?: string }>
}>('/admin/accounts/batch', { accounts }); }>('/admin/accounts/batch', { accounts })
return data; return data
} }
/** /**
@@ -219,20 +218,20 @@ export async function batchCreate(accounts: CreateAccountRequest[]): Promise<{
* @returns Results of batch update * @returns Results of batch update
*/ */
export async function batchUpdateCredentials(request: { export async function batchUpdateCredentials(request: {
account_ids: number[]; account_ids: number[]
field: string; field: string
value: any; value: any
}): Promise<{ }): Promise<{
success: number; success: number
failed: number; failed: number
results: Array<{ account_id: number; success: boolean; error?: string }>; results: Array<{ account_id: number; success: boolean; error?: string }>
}> { }> {
const { data} = await apiClient.post<{ const { data } = await apiClient.post<{
success: number; success: number
failed: number; failed: number
results: Array<{ account_id: number; success: boolean; error?: string }>; results: Array<{ account_id: number; success: boolean; error?: string }>
}>('/admin/accounts/batch-update-credentials', request); }>('/admin/accounts/batch-update-credentials', request)
return data; return data
} }
/** /**
@@ -245,19 +244,19 @@ export async function bulkUpdate(
accountIds: number[], accountIds: number[],
updates: Record<string, unknown> updates: Record<string, unknown>
): Promise<{ ): Promise<{
success: number; success: number
failed: number; failed: number
results: Array<{ account_id: number; success: boolean; error?: string }>; results: Array<{ account_id: number; success: boolean; error?: string }>
}> { }> {
const { data } = await apiClient.post<{ const { data } = await apiClient.post<{
success: number; success: number
failed: number; failed: number
results: Array<{ account_id: number; success: boolean; error?: string }>; results: Array<{ account_id: number; success: boolean; error?: string }>
}>('/admin/accounts/bulk-update', { }>('/admin/accounts/bulk-update', {
account_ids: accountIds, account_ids: accountIds,
...updates ...updates,
}); })
return data; return data
} }
/** /**
@@ -266,8 +265,8 @@ export async function bulkUpdate(
* @returns Today's stats (requests, tokens, cost) * @returns Today's stats (requests, tokens, cost)
*/ */
export async function getTodayStats(id: number): Promise<WindowStats> { export async function getTodayStats(id: number): Promise<WindowStats> {
const { data } = await apiClient.get<WindowStats>(`/admin/accounts/${id}/today-stats`); const { data } = await apiClient.get<WindowStats>(`/admin/accounts/${id}/today-stats`)
return data; return data
} }
/** /**
@@ -277,8 +276,10 @@ export async function getTodayStats(id: number): Promise<WindowStats> {
* @returns Updated account * @returns Updated account
*/ */
export async function setSchedulable(id: number, schedulable: boolean): Promise<Account> { export async function setSchedulable(id: number, schedulable: boolean): Promise<Account> {
const { data } = await apiClient.post<Account>(`/admin/accounts/${id}/schedulable`, { schedulable }); const { data } = await apiClient.post<Account>(`/admin/accounts/${id}/schedulable`, {
return data; schedulable,
})
return data
} }
/** /**
@@ -287,30 +288,30 @@ export async function setSchedulable(id: number, schedulable: boolean): Promise<
* @returns List of available models for this account * @returns List of available models for this account
*/ */
export async function getAvailableModels(id: number): Promise<ClaudeModel[]> { export async function getAvailableModels(id: number): Promise<ClaudeModel[]> {
const { data } = await apiClient.get<ClaudeModel[]>(`/admin/accounts/${id}/models`); const { data } = await apiClient.get<ClaudeModel[]>(`/admin/accounts/${id}/models`)
return data; return data
} }
export async function syncFromCrs(params: { export async function syncFromCrs(params: {
base_url: string; base_url: string
username: string; username: string
password: string; password: string
sync_proxies?: boolean; sync_proxies?: boolean
}): Promise<{ }): Promise<{
created: number; created: number
updated: number; updated: number
skipped: number; skipped: number
failed: number; failed: number
items: Array<{ items: Array<{
crs_account_id: string; crs_account_id: string
kind: string; kind: string
name: string; name: string
action: string; action: string
error?: string; error?: string
}>; }>
}> { }> {
const { data } = await apiClient.post('/admin/accounts/sync/crs', params); const { data } = await apiClient.post('/admin/accounts/sync/crs', params)
return data; return data
} }
export const accountsAPI = { export const accountsAPI = {
@@ -335,6 +336,6 @@ export const accountsAPI = {
batchUpdateCredentials, batchUpdateCredentials,
bulkUpdate, bulkUpdate,
syncFromCrs, syncFromCrs,
}; }
export default accountsAPI; export default accountsAPI

View File

@@ -1,16 +1,16 @@
<template> <template>
<Modal <Modal :show="show" :title="t('admin.accounts.bulkEdit.title')" size="lg" @close="handleClose">
:show="show" <form class="space-y-5" @submit.prevent="handleSubmit">
:title="t('admin.accounts.bulkEdit.title')"
size="lg"
@close="handleClose"
>
<form @submit.prevent="handleSubmit" class="space-y-5">
<!-- Info --> <!-- Info -->
<div class="rounded-lg bg-blue-50 dark:bg-blue-900/20 p-4"> <div class="rounded-lg bg-blue-50 dark:bg-blue-900/20 p-4">
<p class="text-sm text-blue-700 dark:text-blue-400"> <p class="text-sm text-blue-700 dark:text-blue-400">
<svg class="w-5 h-5 inline mr-1.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg class="w-5 h-5 inline mr-1.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> <path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg> </svg>
{{ t('admin.accounts.bulkEdit.selectionInfo', { count: accountIds.length }) }} {{ t('admin.accounts.bulkEdit.selectionInfo', { count: accountIds.length }) }}
</p> </p>
@@ -21,8 +21,8 @@
<div class="flex items-center justify-between mb-3"> <div class="flex items-center justify-between mb-3">
<label class="input-label mb-0">{{ t('admin.accounts.baseUrl') }}</label> <label class="input-label mb-0">{{ t('admin.accounts.baseUrl') }}</label>
<input <input
type="checkbox"
v-model="enableBaseUrl" v-model="enableBaseUrl"
type="checkbox"
class="rounded border-gray-300 text-primary-600 focus:ring-primary-500" class="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
/> />
</div> </div>
@@ -34,7 +34,9 @@
:class="!enableBaseUrl && 'opacity-50 cursor-not-allowed'" :class="!enableBaseUrl && 'opacity-50 cursor-not-allowed'"
:placeholder="t('admin.accounts.bulkEdit.baseUrlPlaceholder')" :placeholder="t('admin.accounts.bulkEdit.baseUrlPlaceholder')"
/> />
<p class="input-hint">{{ t('admin.accounts.bulkEdit.baseUrlNotice') }}</p> <p class="input-hint">
{{ t('admin.accounts.bulkEdit.baseUrlNotice') }}
</p>
</div> </div>
<!-- Model restriction --> <!-- Model restriction -->
@@ -42,8 +44,8 @@
<div class="flex items-center justify-between mb-3"> <div class="flex items-center justify-between mb-3">
<label class="input-label mb-0">{{ t('admin.accounts.modelRestriction') }}</label> <label class="input-label mb-0">{{ t('admin.accounts.modelRestriction') }}</label>
<input <input
type="checkbox"
v-model="enableModelRestriction" v-model="enableModelRestriction"
type="checkbox"
class="rounded border-gray-300 text-primary-600 focus:ring-primary-500" class="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
/> />
</div> </div>
@@ -53,31 +55,51 @@
<div class="flex gap-2 mb-4"> <div class="flex gap-2 mb-4">
<button <button
type="button" type="button"
@click="modelRestrictionMode = 'whitelist'"
:class="[ :class="[
'flex-1 rounded-lg px-4 py-2 text-sm font-medium transition-all', 'flex-1 rounded-lg px-4 py-2 text-sm font-medium transition-all',
modelRestrictionMode === 'whitelist' modelRestrictionMode === 'whitelist'
? 'bg-primary-100 text-primary-700 dark:bg-primary-900/30 dark:text-primary-400' ? 'bg-primary-100 text-primary-700 dark:bg-primary-900/30 dark:text-primary-400'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200 dark:bg-dark-600 dark:text-gray-400 dark:hover:bg-dark-500' : 'bg-gray-100 text-gray-600 hover:bg-gray-200 dark:bg-dark-600 dark:text-gray-400 dark:hover:bg-dark-500',
]" ]"
@click="modelRestrictionMode = 'whitelist'"
> >
<svg class="w-4 h-4 inline mr-1.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /> class="w-4 h-4 inline mr-1.5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg> </svg>
{{ t('admin.accounts.modelWhitelist') }} {{ t('admin.accounts.modelWhitelist') }}
</button> </button>
<button <button
type="button" type="button"
@click="modelRestrictionMode = 'mapping'"
:class="[ :class="[
'flex-1 rounded-lg px-4 py-2 text-sm font-medium transition-all', 'flex-1 rounded-lg px-4 py-2 text-sm font-medium transition-all',
modelRestrictionMode === 'mapping' modelRestrictionMode === 'mapping'
? 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400' ? 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200 dark:bg-dark-600 dark:text-gray-400 dark:hover:bg-dark-500' : 'bg-gray-100 text-gray-600 hover:bg-gray-200 dark:bg-dark-600 dark:text-gray-400 dark:hover:bg-dark-500',
]" ]"
@click="modelRestrictionMode = 'mapping'"
> >
<svg class="w-4 h-4 inline mr-1.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" /> class="w-4 h-4 inline mr-1.5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4"
/>
</svg> </svg>
{{ t('admin.accounts.modelMapping') }} {{ t('admin.accounts.modelMapping') }}
</button> </button>
@@ -87,8 +109,18 @@
<div v-if="modelRestrictionMode === 'whitelist'"> <div v-if="modelRestrictionMode === 'whitelist'">
<div class="mb-3 rounded-lg bg-blue-50 dark:bg-blue-900/20 p-3"> <div class="mb-3 rounded-lg bg-blue-50 dark:bg-blue-900/20 p-3">
<p class="text-xs text-blue-700 dark:text-blue-400"> <p class="text-xs text-blue-700 dark:text-blue-400">
<svg class="w-4 h-4 inline mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> class="w-4 h-4 inline mr-1"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg> </svg>
{{ t('admin.accounts.selectAllowedModels') }} {{ t('admin.accounts.selectAllowedModels') }}
</p> </p>
@@ -100,12 +132,16 @@
v-for="model in allModels" v-for="model in allModels"
:key="model.value" :key="model.value"
class="flex cursor-pointer items-center rounded-lg border p-3 transition-all hover:bg-gray-50 dark:border-dark-600 dark:hover:bg-dark-700" class="flex cursor-pointer items-center rounded-lg border p-3 transition-all hover:bg-gray-50 dark:border-dark-600 dark:hover:bg-dark-700"
:class="allowedModels.includes(model.value) ? 'border-primary-500 bg-primary-50 dark:bg-primary-900/20' : 'border-gray-200'" :class="
allowedModels.includes(model.value)
? 'border-primary-500 bg-primary-50 dark:bg-primary-900/20'
: 'border-gray-200'
"
> >
<input <input
v-model="allowedModels"
type="checkbox" type="checkbox"
:value="model.value" :value="model.value"
v-model="allowedModels"
class="mr-2 rounded border-gray-300 text-primary-600 focus:ring-primary-500" class="mr-2 rounded border-gray-300 text-primary-600 focus:ring-primary-500"
/> />
<span class="text-sm text-gray-700 dark:text-gray-300">{{ model.label }}</span> <span class="text-sm text-gray-700 dark:text-gray-300">{{ model.label }}</span>
@@ -114,7 +150,9 @@
<p class="text-xs text-gray-500 dark:text-gray-400"> <p class="text-xs text-gray-500 dark:text-gray-400">
{{ t('admin.accounts.selectedModels', { count: allowedModels.length }) }} {{ t('admin.accounts.selectedModels', { count: allowedModels.length }) }}
<span v-if="allowedModels.length === 0">{{ t('admin.accounts.supportsAllModels') }}</span> <span v-if="allowedModels.length === 0">{{
t('admin.accounts.supportsAllModels')
}}</span>
</p> </p>
</div> </div>
@@ -122,8 +160,18 @@
<div v-else> <div v-else>
<div class="mb-3 rounded-lg bg-purple-50 dark:bg-purple-900/20 p-3"> <div class="mb-3 rounded-lg bg-purple-50 dark:bg-purple-900/20 p-3">
<p class="text-xs text-purple-700 dark:text-purple-400"> <p class="text-xs text-purple-700 dark:text-purple-400">
<svg class="w-4 h-4 inline mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> class="w-4 h-4 inline mr-1"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg> </svg>
{{ t('admin.accounts.mapRequestModels') }} {{ t('admin.accounts.mapRequestModels') }}
</p> </p>
@@ -142,8 +190,18 @@
class="input flex-1" class="input flex-1"
:placeholder="t('admin.accounts.requestModel')" :placeholder="t('admin.accounts.requestModel')"
/> />
<svg class="w-4 h-4 text-gray-400 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3" /> class="w-4 h-4 text-gray-400 flex-shrink-0"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M14 5l7 7m0 0l-7 7m7-7H3"
/>
</svg> </svg>
<input <input
v-model="mapping.to" v-model="mapping.to"
@@ -153,11 +211,16 @@
/> />
<button <button
type="button" type="button"
@click="removeModelMapping(index)"
class="p-2 text-red-500 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-lg transition-colors" class="p-2 text-red-500 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-lg transition-colors"
@click="removeModelMapping(index)"
> >
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /> <path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/>
</svg> </svg>
</button> </button>
</div> </div>
@@ -165,11 +228,21 @@
<button <button
type="button" type="button"
@click="addModelMapping"
class="w-full rounded-lg border-2 border-dashed border-gray-300 dark:border-dark-500 px-4 py-2 text-gray-600 dark:text-gray-400 transition-colors hover:border-gray-400 hover:text-gray-700 dark:hover:border-dark-400 dark:hover:text-gray-300 mb-3" class="w-full rounded-lg border-2 border-dashed border-gray-300 dark:border-dark-500 px-4 py-2 text-gray-600 dark:text-gray-400 transition-colors hover:border-gray-400 hover:text-gray-700 dark:hover:border-dark-400 dark:hover:text-gray-300 mb-3"
@click="addModelMapping"
> >
<svg class="w-4 h-4 inline mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" /> class="w-4 h-4 inline mr-1"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 4v16m8-8H4"
/>
</svg> </svg>
{{ t('admin.accounts.addMapping') }} {{ t('admin.accounts.addMapping') }}
</button> </button>
@@ -180,11 +253,8 @@
v-for="preset in presetMappings" v-for="preset in presetMappings"
:key="preset.label" :key="preset.label"
type="button" type="button"
:class="['rounded-lg px-3 py-1 text-xs transition-colors', preset.color]"
@click="addPresetMapping(preset.from, preset.to)" @click="addPresetMapping(preset.from, preset.to)"
:class="[
'rounded-lg px-3 py-1 text-xs transition-colors',
preset.color
]"
> >
+ {{ preset.label }} + {{ preset.label }}
</button> </button>
@@ -198,11 +268,13 @@
<div class="flex items-center justify-between mb-3"> <div class="flex items-center justify-between mb-3">
<div> <div>
<label class="input-label mb-0">{{ t('admin.accounts.customErrorCodes') }}</label> <label class="input-label mb-0">{{ t('admin.accounts.customErrorCodes') }}</label>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">{{ t('admin.accounts.customErrorCodesHint') }}</p> <p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
{{ t('admin.accounts.customErrorCodesHint') }}
</p>
</div> </div>
<input <input
type="checkbox"
v-model="enableCustomErrorCodes" v-model="enableCustomErrorCodes"
type="checkbox"
class="rounded border-gray-300 text-primary-600 focus:ring-primary-500" class="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
/> />
</div> </div>
@@ -210,8 +282,18 @@
<div v-if="enableCustomErrorCodes" class="space-y-3"> <div v-if="enableCustomErrorCodes" class="space-y-3">
<div class="rounded-lg bg-amber-50 dark:bg-amber-900/20 p-3"> <div class="rounded-lg bg-amber-50 dark:bg-amber-900/20 p-3">
<p class="text-xs text-amber-700 dark:text-amber-400"> <p class="text-xs text-amber-700 dark:text-amber-400">
<svg class="w-4 h-4 inline mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /> class="w-4 h-4 inline mr-1"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg> </svg>
{{ t('admin.accounts.customErrorCodesWarning') }} {{ t('admin.accounts.customErrorCodesWarning') }}
</p> </p>
@@ -223,13 +305,13 @@
v-for="code in commonErrorCodes" v-for="code in commonErrorCodes"
:key="code.value" :key="code.value"
type="button" type="button"
@click="toggleErrorCode(code.value)"
:class="[ :class="[
'rounded-lg px-3 py-1.5 text-sm font-medium transition-colors', 'rounded-lg px-3 py-1.5 text-sm font-medium transition-colors',
selectedErrorCodes.includes(code.value) selectedErrorCodes.includes(code.value)
? 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400 ring-1 ring-red-500' ? 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400 ring-1 ring-red-500'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200 dark:bg-dark-600 dark:text-gray-400 dark:hover:bg-dark-500' : 'bg-gray-100 text-gray-600 hover:bg-gray-200 dark:bg-dark-600 dark:text-gray-400 dark:hover:bg-dark-500',
]" ]"
@click="toggleErrorCode(code.value)"
> >
{{ code.value }} {{ code.label }} {{ code.value }} {{ code.label }}
</button> </button>
@@ -246,13 +328,14 @@
:placeholder="t('admin.accounts.enterErrorCode')" :placeholder="t('admin.accounts.enterErrorCode')"
@keyup.enter="addCustomErrorCode" @keyup.enter="addCustomErrorCode"
/> />
<button <button type="button" class="btn btn-secondary px-3" @click="addCustomErrorCode">
type="button"
@click="addCustomErrorCode"
class="btn btn-secondary px-3"
>
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" /> <path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 4v16m8-8H4"
/>
</svg> </svg>
</button> </button>
</div> </div>
@@ -267,11 +350,16 @@
{{ code }} {{ code }}
<button <button
type="button" type="button"
@click="removeErrorCode(code)"
class="hover:text-red-900 dark:hover:text-red-300" class="hover:text-red-900 dark:hover:text-red-300"
@click="removeErrorCode(code)"
> >
<svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /> <path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg> </svg>
</button> </button>
</span> </span>
@@ -286,28 +374,32 @@
<div class="border-t border-gray-200 dark:border-dark-600 pt-4"> <div class="border-t border-gray-200 dark:border-dark-600 pt-4">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="flex-1 pr-4"> <div class="flex-1 pr-4">
<label class="input-label mb-0">{{ t('admin.accounts.interceptWarmupRequests') }}</label> <label class="input-label mb-0">{{
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">{{ t('admin.accounts.interceptWarmupRequestsDesc') }}</p> t('admin.accounts.interceptWarmupRequests')
}}</label>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
{{ t('admin.accounts.interceptWarmupRequestsDesc') }}
</p>
</div> </div>
<input <input
type="checkbox"
v-model="enableInterceptWarmup" v-model="enableInterceptWarmup"
type="checkbox"
class="rounded border-gray-300 text-primary-600 focus:ring-primary-500" class="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
/> />
</div> </div>
<div v-if="enableInterceptWarmup" class="mt-3"> <div v-if="enableInterceptWarmup" class="mt-3">
<button <button
type="button" type="button"
@click="interceptWarmupRequests = !interceptWarmupRequests"
:class="[ :class="[
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2', 'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2',
interceptWarmupRequests ? 'bg-primary-600' : 'bg-gray-200 dark:bg-dark-600' interceptWarmupRequests ? 'bg-primary-600' : 'bg-gray-200 dark:bg-dark-600',
]" ]"
@click="interceptWarmupRequests = !interceptWarmupRequests"
> >
<span <span
:class="[ :class="[
'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out', 'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out',
interceptWarmupRequests ? 'translate-x-5' : 'translate-x-0' interceptWarmupRequests ? 'translate-x-5' : 'translate-x-0',
]" ]"
/> />
</button> </button>
@@ -319,16 +411,13 @@
<div class="flex items-center justify-between mb-3"> <div class="flex items-center justify-between mb-3">
<label class="input-label mb-0">{{ t('admin.accounts.proxy') }}</label> <label class="input-label mb-0">{{ t('admin.accounts.proxy') }}</label>
<input <input
type="checkbox"
v-model="enableProxy" v-model="enableProxy"
type="checkbox"
class="rounded border-gray-300 text-primary-600 focus:ring-primary-500" class="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
/> />
</div> </div>
<div :class="!enableProxy && 'opacity-50 pointer-events-none'"> <div :class="!enableProxy && 'opacity-50 pointer-events-none'">
<ProxySelector <ProxySelector v-model="proxyId" :proxies="proxies" />
v-model="proxyId"
:proxies="proxies"
/>
</div> </div>
</div> </div>
@@ -338,8 +427,8 @@
<div class="flex items-center justify-between mb-3"> <div class="flex items-center justify-between mb-3">
<label class="input-label mb-0">{{ t('admin.accounts.concurrency') }}</label> <label class="input-label mb-0">{{ t('admin.accounts.concurrency') }}</label>
<input <input
type="checkbox"
v-model="enableConcurrency" v-model="enableConcurrency"
type="checkbox"
class="rounded border-gray-300 text-primary-600 focus:ring-primary-500" class="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
/> />
</div> </div>
@@ -356,8 +445,8 @@
<div class="flex items-center justify-between mb-3"> <div class="flex items-center justify-between mb-3">
<label class="input-label mb-0">{{ t('admin.accounts.priority') }}</label> <label class="input-label mb-0">{{ t('admin.accounts.priority') }}</label>
<input <input
type="checkbox"
v-model="enablePriority" v-model="enablePriority"
type="checkbox"
class="rounded border-gray-300 text-primary-600 focus:ring-primary-500" class="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
/> />
</div> </div>
@@ -377,16 +466,13 @@
<div class="flex items-center justify-between mb-3"> <div class="flex items-center justify-between mb-3">
<label class="input-label mb-0">{{ t('common.status') }}</label> <label class="input-label mb-0">{{ t('common.status') }}</label>
<input <input
type="checkbox"
v-model="enableStatus" v-model="enableStatus"
type="checkbox"
class="rounded border-gray-300 text-primary-600 focus:ring-primary-500" class="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
/> />
</div> </div>
<div :class="!enableStatus && 'opacity-50 pointer-events-none'"> <div :class="!enableStatus && 'opacity-50 pointer-events-none'">
<Select <Select v-model="status" :options="statusOptions" />
v-model="status"
:options="statusOptions"
/>
</div> </div>
</div> </div>
@@ -395,43 +481,45 @@
<div class="flex items-center justify-between mb-3"> <div class="flex items-center justify-between mb-3">
<label class="input-label mb-0">{{ t('nav.groups') }}</label> <label class="input-label mb-0">{{ t('nav.groups') }}</label>
<input <input
type="checkbox"
v-model="enableGroups" v-model="enableGroups"
type="checkbox"
class="rounded border-gray-300 text-primary-600 focus:ring-primary-500" class="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
/> />
</div> </div>
<div :class="!enableGroups && 'opacity-50 pointer-events-none'"> <div :class="!enableGroups && 'opacity-50 pointer-events-none'">
<GroupSelector <GroupSelector v-model="groupIds" :groups="groups" />
v-model="groupIds"
:groups="groups"
/>
</div> </div>
</div> </div>
<!-- Action buttons --> <!-- Action buttons -->
<div class="flex justify-end gap-3 pt-4"> <div class="flex justify-end gap-3 pt-4">
<button <button type="button" class="btn btn-secondary" @click="handleClose">
@click="handleClose"
type="button"
class="btn btn-secondary"
>
{{ t('common.cancel') }} {{ t('common.cancel') }}
</button> </button>
<button <button type="submit" :disabled="submitting" class="btn btn-primary">
type="submit"
:disabled="submitting"
class="btn btn-primary"
>
<svg <svg
v-if="submitting" v-if="submitting"
class="animate-spin -ml-1 mr-2 h-4 w-4" class="animate-spin -ml-1 mr-2 h-4 w-4"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
> >
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> <circle
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
/>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg> </svg>
{{ submitting ? t('admin.accounts.bulkEdit.updating') : t('admin.accounts.bulkEdit.submit') }} {{
submitting ? t('admin.accounts.bulkEdit.updating') : t('admin.accounts.bulkEdit.submit')
}}
</button> </button>
</div> </div>
</form> </form>
@@ -513,18 +601,57 @@ const allModels = [
{ value: 'gpt-5.1-codex', label: 'GPT-5.1 Codex' }, { value: 'gpt-5.1-codex', label: 'GPT-5.1 Codex' },
{ value: 'gpt-5.1-2025-11-13', label: 'GPT-5.1' }, { value: 'gpt-5.1-2025-11-13', label: 'GPT-5.1' },
{ value: 'gpt-5.1-codex-mini', label: 'GPT-5.1 Codex Mini' }, { value: 'gpt-5.1-codex-mini', label: 'GPT-5.1 Codex Mini' },
{ value: 'gpt-5-2025-08-07', label: 'GPT-5' } { value: 'gpt-5-2025-08-07', label: 'GPT-5' },
] ]
// Preset mappings (combined Anthropic + OpenAI) // Preset mappings (combined Anthropic + OpenAI)
const presetMappings = [ const presetMappings = [
{ label: 'Sonnet 4', from: 'claude-sonnet-4-20250514', to: 'claude-sonnet-4-20250514', color: 'bg-blue-100 text-blue-700 hover:bg-blue-200 dark:bg-blue-900/30 dark:text-blue-400' }, {
{ label: 'Sonnet 4.5', from: 'claude-sonnet-4-5-20250929', to: 'claude-sonnet-4-5-20250929', color: 'bg-indigo-100 text-indigo-700 hover:bg-indigo-200 dark:bg-indigo-900/30 dark:text-indigo-400' }, label: 'Sonnet 4',
{ label: 'Opus 4.5', from: 'claude-opus-4-5-20251101', to: 'claude-opus-4-5-20251101', color: 'bg-purple-100 text-purple-700 hover:bg-purple-200 dark:bg-purple-900/30 dark:text-purple-400' }, from: 'claude-sonnet-4-20250514',
{ label: 'Opus->Sonnet', from: 'claude-opus-4-5-20251101', to: 'claude-sonnet-4-5-20250929', color: 'bg-amber-100 text-amber-700 hover:bg-amber-200 dark:bg-amber-900/30 dark:text-amber-400' }, to: 'claude-sonnet-4-20250514',
{ label: 'GPT-5.2', from: 'gpt-5.2-2025-12-11', to: 'gpt-5.2-2025-12-11', color: 'bg-green-100 text-green-700 hover:bg-green-200 dark:bg-green-900/30 dark:text-green-400' }, color: 'bg-blue-100 text-blue-700 hover:bg-blue-200 dark:bg-blue-900/30 dark:text-blue-400',
{ label: 'GPT-5.2 Codex', from: 'gpt-5.2-codex', to: 'gpt-5.2-codex', color: 'bg-blue-100 text-blue-700 hover:bg-blue-200 dark:bg-blue-900/30 dark:text-blue-400' }, },
{ label: 'Max->Codex', from: 'gpt-5.1-codex-max', to: 'gpt-5.1-codex', color: 'bg-pink-100 text-pink-700 hover:bg-pink-200 dark:bg-pink-900/30 dark:text-pink-400' } {
label: 'Sonnet 4.5',
from: 'claude-sonnet-4-5-20250929',
to: 'claude-sonnet-4-5-20250929',
color:
'bg-indigo-100 text-indigo-700 hover:bg-indigo-200 dark:bg-indigo-900/30 dark:text-indigo-400',
},
{
label: 'Opus 4.5',
from: 'claude-opus-4-5-20251101',
to: 'claude-opus-4-5-20251101',
color:
'bg-purple-100 text-purple-700 hover:bg-purple-200 dark:bg-purple-900/30 dark:text-purple-400',
},
{
label: 'Opus->Sonnet',
from: 'claude-opus-4-5-20251101',
to: 'claude-sonnet-4-5-20250929',
color:
'bg-amber-100 text-amber-700 hover:bg-amber-200 dark:bg-amber-900/30 dark:text-amber-400',
},
{
label: 'GPT-5.2',
from: 'gpt-5.2-2025-12-11',
to: 'gpt-5.2-2025-12-11',
color:
'bg-green-100 text-green-700 hover:bg-green-200 dark:bg-green-900/30 dark:text-green-400',
},
{
label: 'GPT-5.2 Codex',
from: 'gpt-5.2-codex',
to: 'gpt-5.2-codex',
color: 'bg-blue-100 text-blue-700 hover:bg-blue-200 dark:bg-blue-900/30 dark:text-blue-400',
},
{
label: 'Max->Codex',
from: 'gpt-5.1-codex-max',
to: 'gpt-5.1-codex',
color: 'bg-pink-100 text-pink-700 hover:bg-pink-200 dark:bg-pink-900/30 dark:text-pink-400',
},
] ]
// Common HTTP error codes // Common HTTP error codes
@@ -535,12 +662,12 @@ const commonErrorCodes = [
{ value: 500, label: 'Server Error' }, { value: 500, label: 'Server Error' },
{ value: 502, label: 'Bad Gateway' }, { value: 502, label: 'Bad Gateway' },
{ value: 503, label: 'Unavailable' }, { value: 503, label: 'Unavailable' },
{ value: 529, label: 'Overloaded' } { value: 529, label: 'Overloaded' },
] ]
const statusOptions = computed(() => [ const statusOptions = computed(() => [
{ value: 'active', label: t('common.active') }, { value: 'active', label: t('common.active') },
{ value: 'inactive', label: t('common.inactive') } { value: 'inactive', label: t('common.inactive') },
]) ])
// Model mapping helpers // Model mapping helpers
@@ -553,7 +680,7 @@ const removeModelMapping = (index: number) => {
} }
const addPresetMapping = (from: string, to: string) => { const addPresetMapping = (from: string, to: string) => {
const exists = modelMappings.value.some(m => m.from === from) const exists = modelMappings.value.some((m) => m.from === from)
if (exists) { if (exists) {
appStore.showInfo(t('admin.accounts.mappingExists', { model: from })) appStore.showInfo(t('admin.accounts.mappingExists', { model: from }))
return return
@@ -681,15 +808,16 @@ const handleSubmit = async () => {
return return
} }
const hasAnyFieldEnabled = enableBaseUrl.value const hasAnyFieldEnabled =
|| enableModelRestriction.value enableBaseUrl.value ||
|| enableCustomErrorCodes.value enableModelRestriction.value ||
|| enableInterceptWarmup.value enableCustomErrorCodes.value ||
|| enableProxy.value enableInterceptWarmup.value ||
|| enableConcurrency.value enableProxy.value ||
|| enablePriority.value enableConcurrency.value ||
|| enableStatus.value enablePriority.value ||
|| enableGroups.value enableStatus.value ||
enableGroups.value
if (!hasAnyFieldEnabled) { if (!hasAnyFieldEnabled) {
appStore.showError(t('admin.accounts.bulkEdit.noFieldsSelected')) appStore.showError(t('admin.accounts.bulkEdit.noFieldsSelected'))
@@ -730,32 +858,35 @@ const handleSubmit = async () => {
} }
// Reset form when modal closes // Reset form when modal closes
watch(() => props.show, (newShow) => { watch(
if (!newShow) { () => props.show,
// Reset all enable flags (newShow) => {
enableBaseUrl.value = false if (!newShow) {
enableModelRestriction.value = false // Reset all enable flags
enableCustomErrorCodes.value = false enableBaseUrl.value = false
enableInterceptWarmup.value = false enableModelRestriction.value = false
enableProxy.value = false enableCustomErrorCodes.value = false
enableConcurrency.value = false enableInterceptWarmup.value = false
enablePriority.value = false enableProxy.value = false
enableStatus.value = false enableConcurrency.value = false
enableGroups.value = false enablePriority.value = false
enableStatus.value = false
enableGroups.value = false
// Reset all values // Reset all values
baseUrl.value = '' baseUrl.value = ''
modelRestrictionMode.value = 'whitelist' modelRestrictionMode.value = 'whitelist'
allowedModels.value = [] allowedModels.value = []
modelMappings.value = [] modelMappings.value = []
selectedErrorCodes.value = [] selectedErrorCodes.value = []
customErrorCodeInput.value = null customErrorCodeInput.value = null
interceptWarmupRequests.value = false interceptWarmupRequests.value = false
proxyId.value = null proxyId.value = null
concurrency.value = 1 concurrency.value = 1
priority.value = 1 priority.value = 1
status.value = 'active' status.value = 'active'
groupIds.value = [] groupIds.value = []
}
} }
}) )
</script> </script>

View File

@@ -17,11 +17,14 @@ export default {
}, },
features: { features: {
unifiedGateway: 'Unified API Gateway', unifiedGateway: 'Unified API Gateway',
unifiedGatewayDesc: 'Convert Claude subscriptions to API endpoints. Access AI capabilities through standard /v1/messages interface.', unifiedGatewayDesc:
'Convert Claude subscriptions to API endpoints. Access AI capabilities through standard /v1/messages interface.',
multiAccount: 'Multi-Account Pool', multiAccount: 'Multi-Account Pool',
multiAccountDesc: 'Manage multiple upstream accounts with smart load balancing. Support OAuth and API Key authentication.', multiAccountDesc:
'Manage multiple upstream accounts with smart load balancing. Support OAuth and API Key authentication.',
balanceQuota: 'Balance & Quota', balanceQuota: 'Balance & Quota',
balanceQuotaDesc: 'Token-based billing with precise usage tracking. Manage quotas and recharge with redeem codes.', balanceQuotaDesc:
'Token-based billing with precise usage tracking. Manage quotas and recharge with redeem codes.',
}, },
providers: { providers: {
title: 'Supported Providers', title: 'Supported Providers',
@@ -235,7 +238,8 @@ export default {
useKey: 'Use Key', useKey: 'Use Key',
useKeyModal: { useKeyModal: {
title: 'Use API Key', title: 'Use API Key',
description: 'Add the following environment variables to your terminal profile or run directly in terminal to configure API access.', description:
'Add the following environment variables to your terminal profile or run directly in terminal to configure API access.',
copy: 'Copy', copy: 'Copy',
copied: 'Copied', copied: 'Copied',
note: 'These environment variables will be active in the current terminal session. For permanent configuration, add them to ~/.bashrc, ~/.zshrc, or the appropriate configuration file.', note: 'These environment variables will be active in the current terminal session. For permanent configuration, add them to ~/.bashrc, ~/.zshrc, or the appropriate configuration file.',
@@ -517,7 +521,8 @@ export default {
failedToLoadApiKeys: 'Failed to load user API keys', failedToLoadApiKeys: 'Failed to load user API keys',
deleteConfirm: "Are you sure you want to delete '{email}'? This action cannot be undone.", deleteConfirm: "Are you sure you want to delete '{email}'? This action cannot be undone.",
setAllowedGroups: 'Set Allowed Groups', setAllowedGroups: 'Set Allowed Groups',
allowedGroupsHint: 'Select which standard groups this user can use. Subscription groups are managed separately.', allowedGroupsHint:
'Select which standard groups this user can use. Subscription groups are managed separately.',
noStandardGroups: 'No standard groups available', noStandardGroups: 'No standard groups available',
allowAllGroups: 'Allow All Groups', allowAllGroups: 'Allow All Groups',
allowAllGroupsHint: 'User can use any non-exclusive group', allowAllGroupsHint: 'User can use any non-exclusive group',
@@ -529,8 +534,10 @@ export default {
depositAmount: 'Deposit Amount', depositAmount: 'Deposit Amount',
withdrawAmount: 'Withdraw Amount', withdrawAmount: 'Withdraw Amount',
currentBalance: 'Current Balance', currentBalance: 'Current Balance',
depositNotesPlaceholder: 'e.g., New user registration bonus, promotional credit, compensation, etc.', depositNotesPlaceholder:
withdrawNotesPlaceholder: 'e.g., Service issue refund, incorrect charge reversal, account closure refund, etc.', 'e.g., New user registration bonus, promotional credit, compensation, etc.',
withdrawNotesPlaceholder:
'e.g., Service issue refund, incorrect charge reversal, account closure refund, etc.',
notesOptional: 'Notes are optional but helpful for record keeping', notesOptional: 'Notes are optional but helpful for record keeping',
amountHint: 'Please enter a positive amount', amountHint: 'Please enter a positive amount',
newBalance: 'New Balance', newBalance: 'New Balance',
@@ -597,12 +604,15 @@ export default {
failedToCreate: 'Failed to create group', failedToCreate: 'Failed to create group',
failedToUpdate: 'Failed to update group', failedToUpdate: 'Failed to update group',
failedToDelete: 'Failed to delete group', failedToDelete: 'Failed to delete group',
deleteConfirm: "Are you sure you want to delete '{name}'? All associated API keys will no longer belong to any group.", deleteConfirm:
deleteConfirmSubscription: "Are you sure you want to delete subscription group '{name}'? This will invalidate all API keys bound to this subscription and delete all related subscription records. This action cannot be undone.", "Are you sure you want to delete '{name}'? All associated API keys will no longer belong to any group.",
deleteConfirmSubscription:
"Are you sure you want to delete subscription group '{name}'? This will invalidate all API keys bound to this subscription and delete all related subscription records. This action cannot be undone.",
subscription: { subscription: {
title: 'Subscription Settings', title: 'Subscription Settings',
type: 'Billing Type', type: 'Billing Type',
typeHint: 'Standard billing deducts from user balance. Subscription mode uses quota limits instead.', typeHint:
'Standard billing deducts from user balance. Subscription mode uses quota limits instead.',
typeNotEditable: 'Billing type cannot be changed after group creation.', typeNotEditable: 'Billing type cannot be changed after group creation.',
standard: 'Standard (Balance)', standard: 'Standard (Balance)',
subscription: 'Subscription (Quota)', subscription: 'Subscription (Quota)',
@@ -674,7 +684,8 @@ export default {
failedToAssign: 'Failed to assign subscription', failedToAssign: 'Failed to assign subscription',
failedToExtend: 'Failed to extend subscription', failedToExtend: 'Failed to extend subscription',
failedToRevoke: 'Failed to revoke subscription', failedToRevoke: 'Failed to revoke subscription',
revokeConfirm: "Are you sure you want to revoke the subscription for '{user}'? This action cannot be undone.", revokeConfirm:
"Are you sure you want to revoke the subscription for '{user}'? This action cannot be undone.",
}, },
// Accounts // Accounts
@@ -754,7 +765,8 @@ export default {
}, },
bulkEdit: { bulkEdit: {
title: 'Bulk Edit Accounts', title: 'Bulk Edit Accounts',
selectionInfo: '{count} account(s) selected. Only checked or filled fields will be updated; others stay unchanged.', selectionInfo:
'{count} account(s) selected. Only checked or filled fields will be updated; others stay unchanged.',
baseUrlPlaceholder: 'https://api.anthropic.com or https://api.openai.com', baseUrlPlaceholder: 'https://api.anthropic.com or https://api.openai.com',
baseUrlNotice: 'Applies to API Key accounts only; leave empty to keep existing value', baseUrlNotice: 'Applies to API Key accounts only; leave empty to keep existing value',
submit: 'Update Accounts', submit: 'Update Accounts',
@@ -797,7 +809,8 @@ export default {
modelWhitelist: 'Model Whitelist', modelWhitelist: 'Model Whitelist',
modelMapping: 'Model Mapping', modelMapping: 'Model Mapping',
selectAllowedModels: 'Select allowed models. Leave empty to support all models.', selectAllowedModels: 'Select allowed models. Leave empty to support all models.',
mapRequestModels: 'Map request models to actual models. Left is the requested model, right is the actual model sent to API.', mapRequestModels:
'Map request models to actual models. Left is the requested model, right is the actual model sent to API.',
selectedModels: 'Selected {count} model(s)', selectedModels: 'Selected {count} model(s)',
supportsAllModels: '(supports all models)', supportsAllModels: '(supports all models)',
requestModel: 'Request model', requestModel: 'Request model',
@@ -806,14 +819,16 @@ export default {
mappingExists: 'Mapping for {model} already exists', mappingExists: 'Mapping for {model} already exists',
customErrorCodes: 'Custom Error Codes', customErrorCodes: 'Custom Error Codes',
customErrorCodesHint: 'Only stop scheduling for selected error codes', customErrorCodesHint: 'Only stop scheduling for selected error codes',
customErrorCodesWarning: 'Only selected error codes will stop scheduling. Other errors will return 500.', customErrorCodesWarning:
'Only selected error codes will stop scheduling. Other errors will return 500.',
selectedErrorCodes: 'Selected', selectedErrorCodes: 'Selected',
noneSelectedUsesDefault: 'None selected (uses default policy)', noneSelectedUsesDefault: 'None selected (uses default policy)',
enterErrorCode: 'Enter error code (100-599)', enterErrorCode: 'Enter error code (100-599)',
invalidErrorCode: 'Please enter a valid HTTP error code (100-599)', invalidErrorCode: 'Please enter a valid HTTP error code (100-599)',
errorCodeExists: 'This error code is already selected', errorCodeExists: 'This error code is already selected',
interceptWarmupRequests: 'Intercept Warmup Requests', interceptWarmupRequests: 'Intercept Warmup Requests',
interceptWarmupRequestsDesc: 'When enabled, warmup requests like title generation will return mock responses without consuming upstream tokens', interceptWarmupRequestsDesc:
'When enabled, warmup requests like title generation will return mock responses without consuming upstream tokens',
proxy: 'Proxy', proxy: 'Proxy',
noProxy: 'No Proxy', noProxy: 'No Proxy',
concurrency: 'Concurrency', concurrency: 'Concurrency',
@@ -836,11 +851,13 @@ export default {
authMethod: 'Authorization Method', authMethod: 'Authorization Method',
manualAuth: 'Manual Authorization', manualAuth: 'Manual Authorization',
cookieAutoAuth: 'Cookie Auto-Auth', cookieAutoAuth: 'Cookie Auto-Auth',
cookieAutoAuthDesc: 'Use claude.ai sessionKey to automatically complete OAuth authorization without manually opening browser.', cookieAutoAuthDesc:
'Use claude.ai sessionKey to automatically complete OAuth authorization without manually opening browser.',
sessionKey: 'sessionKey', sessionKey: 'sessionKey',
keysCount: '{count} keys', keysCount: '{count} keys',
batchCreateAccounts: 'Will batch create {count} accounts', batchCreateAccounts: 'Will batch create {count} accounts',
sessionKeyPlaceholder: 'One sessionKey per line, e.g.:\nsk-ant-sid01-xxxxx...\nsk-ant-sid01-yyyyy...', sessionKeyPlaceholder:
'One sessionKey per line, e.g.:\nsk-ant-sid01-xxxxx...\nsk-ant-sid01-yyyyy...',
sessionKeyPlaceholderSingle: 'sk-ant-sid01-xxxxx...', sessionKeyPlaceholderSingle: 'sk-ant-sid01-xxxxx...',
howToGetSessionKey: 'How to get sessionKey', howToGetSessionKey: 'How to get sessionKey',
step1: 'Login to <strong>claude.ai</strong> in your browser', step1: 'Login to <strong>claude.ai</strong> in your browser',
@@ -858,10 +875,13 @@ export default {
generating: 'Generating...', generating: 'Generating...',
regenerate: 'Regenerate', regenerate: 'Regenerate',
step2OpenUrl: 'Open the URL in your browser and complete authorization', step2OpenUrl: 'Open the URL in your browser and complete authorization',
openUrlDesc: 'Open the authorization URL in a new tab, log in to your Claude account and authorize.', openUrlDesc:
proxyWarning: '<strong>Note:</strong> If you configured a proxy, make sure your browser uses the same proxy to access the authorization page.', 'Open the authorization URL in a new tab, log in to your Claude account and authorize.',
proxyWarning:
'<strong>Note:</strong> If you configured a proxy, make sure your browser uses the same proxy to access the authorization page.',
step3EnterCode: 'Enter the Authorization Code', step3EnterCode: 'Enter the Authorization Code',
authCodeDesc: 'After authorization is complete, the page will display an <strong>Authorization Code</strong>. Copy and paste it below:', authCodeDesc:
'After authorization is complete, the page will display an <strong>Authorization Code</strong>. Copy and paste it below:',
authCode: 'Authorization Code', authCode: 'Authorization Code',
authCodePlaceholder: 'Paste the Authorization Code from Claude page...', authCodePlaceholder: 'Paste the Authorization Code from Claude page...',
authCodeHint: 'Paste the Authorization Code copied from the Claude page', authCodeHint: 'Paste the Authorization Code copied from the Claude page',
@@ -879,13 +899,18 @@ export default {
step1GenerateUrl: 'Click the button below to generate the authorization URL', step1GenerateUrl: 'Click the button below to generate the authorization URL',
generateAuthUrl: 'Generate Auth URL', generateAuthUrl: 'Generate Auth URL',
step2OpenUrl: 'Open the URL in your browser and complete authorization', step2OpenUrl: 'Open the URL in your browser and complete authorization',
openUrlDesc: 'Open the authorization URL in a new tab, log in to your OpenAI account and authorize.', openUrlDesc:
importantNotice: '<strong>Important:</strong> The page may take a while to load after authorization. Please wait patiently. When the browser address bar changes to <code>http://localhost...</code>, the authorization is complete.', 'Open the authorization URL in a new tab, log in to your OpenAI account and authorize.',
importantNotice:
'<strong>Important:</strong> The page may take a while to load after authorization. Please wait patiently. When the browser address bar changes to <code>http://localhost...</code>, the authorization is complete.',
step3EnterCode: 'Enter Authorization URL or Code', step3EnterCode: 'Enter Authorization URL or Code',
authCodeDesc: 'After authorization is complete, when the page URL becomes <code>http://localhost:xxx/auth/callback?code=...</code>:', authCodeDesc:
'After authorization is complete, when the page URL becomes <code>http://localhost:xxx/auth/callback?code=...</code>:',
authCode: 'Authorization URL or Code', authCode: 'Authorization URL or Code',
authCodePlaceholder: 'Option 1: Copy the complete URL\n(http://localhost:xxx/auth/callback?code=...)\nOption 2: Copy only the code parameter value', authCodePlaceholder:
authCodeHint: 'You can copy the entire URL or just the code parameter value, the system will auto-detect', 'Option 1: Copy the complete URL\n(http://localhost:xxx/auth/callback?code=...)\nOption 2: Copy only the code parameter value',
authCodeHint:
'You can copy the entire URL or just the code parameter value, the system will auto-detect',
}, },
}, },
// Re-Auth Modal // Re-Auth Modal
@@ -985,8 +1010,10 @@ export default {
standardAdd: 'Standard Add', standardAdd: 'Standard Add',
batchAdd: 'Quick Add', batchAdd: 'Quick Add',
batchInput: 'Proxy List', batchInput: 'Proxy List',
batchInputPlaceholder: "Enter one proxy per line in the following formats:\nsocks5://user:pass{'@'}192.168.1.1:1080\nhttp://192.168.1.1:8080\nhttps://user:pass{'@'}proxy.example.com:443", batchInputPlaceholder:
batchInputHint: "Supports http, https, socks5 protocols. Format: protocol://[user:pass{'@'}]host:port", "Enter one proxy per line in the following formats:\nsocks5://user:pass{'@'}192.168.1.1:1080\nhttp://192.168.1.1:8080\nhttps://user:pass{'@'}proxy.example.com:443",
batchInputHint:
"Supports http, https, socks5 protocols. Format: protocol://[user:pass{'@'}]host:port",
parsedCount: '{count} valid', parsedCount: '{count} valid',
invalidCount: '{count} invalid', invalidCount: '{count} invalid',
duplicateCount: '{count} duplicate', duplicateCount: '{count} duplicate',
@@ -1009,7 +1036,8 @@ export default {
failedToUpdate: 'Failed to update proxy', failedToUpdate: 'Failed to update proxy',
failedToDelete: 'Failed to delete proxy', failedToDelete: 'Failed to delete proxy',
failedToTest: 'Failed to test proxy', failedToTest: 'Failed to test proxy',
deleteConfirm: "Are you sure you want to delete '{name}'? Accounts using this proxy will have their proxy removed.", deleteConfirm:
"Are you sure you want to delete '{name}'? Accounts using this proxy will have their proxy removed.",
}, },
// Redeem Codes // Redeem Codes
@@ -1038,8 +1066,10 @@ export default {
exportCsv: 'Export CSV', exportCsv: 'Export CSV',
deleteAllUnused: 'Delete All Unused Codes', deleteAllUnused: 'Delete All Unused Codes',
deleteCode: 'Delete Redeem Code', deleteCode: 'Delete Redeem Code',
deleteCodeConfirm: 'Are you sure you want to delete this redeem code? This action cannot be undone.', deleteCodeConfirm:
deleteAllUnusedConfirm: 'Are you sure you want to delete all unused (active) redeem codes? This action cannot be undone.', 'Are you sure you want to delete this redeem code? This action cannot be undone.',
deleteAllUnusedConfirm:
'Are you sure you want to delete all unused (active) redeem codes? This action cannot be undone.',
deleteAll: 'Delete All', deleteAll: 'Delete All',
generateCodesTitle: 'Generate Redeem Codes', generateCodesTitle: 'Generate Redeem Codes',
generatedSuccessfully: 'Generated Successfully', generatedSuccessfully: 'Generated Successfully',
@@ -1119,7 +1149,8 @@ export default {
siteSubtitle: 'Site Subtitle', siteSubtitle: 'Site Subtitle',
siteSubtitleHint: 'Displayed on login and register pages', siteSubtitleHint: 'Displayed on login and register pages',
apiBaseUrl: 'API Base URL', apiBaseUrl: 'API Base URL',
apiBaseUrlHint: 'Used for "Use Key" and "Import to CC Switch" features. Leave empty to use current site URL.', apiBaseUrlHint:
'Used for "Use Key" and "Import to CC Switch" features. Leave empty to use current site URL.',
contactInfo: 'Contact Info', contactInfo: 'Contact Info',
contactInfoPlaceholder: 'e.g., QQ: 123456789', contactInfoPlaceholder: 'e.g., QQ: 123456789',
contactInfoHint: 'Customer support contact info, displayed on redeem page, profile, etc.', contactInfoHint: 'Customer support contact info, displayed on redeem page, profile, etc.',
@@ -1169,7 +1200,8 @@ export default {
create: 'Create Key', create: 'Create Key',
creating: 'Creating...', creating: 'Creating...',
regenerateConfirm: 'Are you sure? The current key will be immediately invalidated.', regenerateConfirm: 'Are you sure? The current key will be immediately invalidated.',
deleteConfirm: 'Are you sure you want to delete the admin API key? External integrations will stop working.', deleteConfirm:
'Are you sure you want to delete the admin API key? External integrations will stop working.',
keyGenerated: 'New admin API key generated', keyGenerated: 'New admin API key generated',
keyDeleted: 'Admin API key deleted', keyDeleted: 'Admin API key deleted',
copyKey: 'Copy Key', copyKey: 'Copy Key',
@@ -1235,7 +1267,8 @@ export default {
title: 'My Subscriptions', title: 'My Subscriptions',
description: 'View your subscription plans and usage', description: 'View your subscription plans and usage',
noActiveSubscriptions: 'No Active Subscriptions', noActiveSubscriptions: 'No Active Subscriptions',
noActiveSubscriptionsDesc: 'You don\'t have any active subscriptions. Contact administrator to get one.', noActiveSubscriptionsDesc:
"You don't have any active subscriptions. Contact administrator to get one.",
status: { status: {
active: 'Active', active: 'Active',
expired: 'Expired', expired: 'Expired',

View File

@@ -620,7 +620,8 @@ export default {
editGroup: '编辑分组', editGroup: '编辑分组',
deleteGroup: '删除分组', deleteGroup: '删除分组',
deleteConfirm: "确定要删除分组 '{name}' 吗?所有关联的 API 密钥将不再属于任何分组。", deleteConfirm: "确定要删除分组 '{name}' 吗?所有关联的 API 密钥将不再属于任何分组。",
deleteConfirmSubscription: "确定要删除订阅分组 '{name}' 吗?此操作会让所有绑定此订阅的用户的 API Key 失效,并删除所有相关的订阅记录。此操作无法撤销。", deleteConfirmSubscription:
"确定要删除订阅分组 '{name}' 吗?此操作会让所有绑定此订阅的用户的 API Key 失效,并删除所有相关的订阅记录。此操作无法撤销。",
columns: { columns: {
name: '名称', name: '名称',
platform: '平台', platform: '平台',
@@ -782,7 +783,8 @@ export default {
createAccount: '添加账号', createAccount: '添加账号',
syncFromCrs: '从 CRS 同步', syncFromCrs: '从 CRS 同步',
syncFromCrsTitle: '从 CRS 同步账号', syncFromCrsTitle: '从 CRS 同步账号',
syncFromCrsDesc: '将 claude-relay-serviceCRS中的账号同步到当前系统不会在浏览器侧直接请求 CRS。', syncFromCrsDesc:
'将 claude-relay-serviceCRS中的账号同步到当前系统不会在浏览器侧直接请求 CRS。',
crsBaseUrl: 'CRS 服务地址', crsBaseUrl: 'CRS 服务地址',
crsBaseUrlPlaceholder: '例如http://127.0.0.1:3000', crsBaseUrlPlaceholder: '例如http://127.0.0.1:3000',
crsUsername: '用户名', crsUsername: '用户名',
@@ -973,7 +975,8 @@ export default {
sessionKey: 'sessionKey', sessionKey: 'sessionKey',
keysCount: '{count} 个密钥', keysCount: '{count} 个密钥',
batchCreateAccounts: '将批量创建 {count} 个账号', batchCreateAccounts: '将批量创建 {count} 个账号',
sessionKeyPlaceholder: '每行一个 sessionKey例如\nsk-ant-sid01-xxxxx...\nsk-ant-sid01-yyyyy...', sessionKeyPlaceholder:
'每行一个 sessionKey例如\nsk-ant-sid01-xxxxx...\nsk-ant-sid01-yyyyy...',
sessionKeyPlaceholderSingle: 'sk-ant-sid01-xxxxx...', sessionKeyPlaceholderSingle: 'sk-ant-sid01-xxxxx...',
howToGetSessionKey: '如何获取 sessionKey', howToGetSessionKey: '如何获取 sessionKey',
step1: '在浏览器中登录 <strong>claude.ai</strong>', step1: '在浏览器中登录 <strong>claude.ai</strong>',
@@ -992,7 +995,8 @@ export default {
regenerate: '重新生成', regenerate: '重新生成',
step2OpenUrl: '在浏览器中打开 URL 并完成授权', step2OpenUrl: '在浏览器中打开 URL 并完成授权',
openUrlDesc: '在新标签页中打开授权 URL登录您的 Claude 账号并授权。', openUrlDesc: '在新标签页中打开授权 URL登录您的 Claude 账号并授权。',
proxyWarning: '<strong>注意:</strong>如果您配置了代理,请确保浏览器使用相同的代理访问授权页面。', proxyWarning:
'<strong>注意:</strong>如果您配置了代理,请确保浏览器使用相同的代理访问授权页面。',
step3EnterCode: '输入授权码', step3EnterCode: '输入授权码',
authCodeDesc: '授权完成后,页面会显示一个 <strong>授权码</strong>。复制并粘贴到下方:', authCodeDesc: '授权完成后,页面会显示一个 <strong>授权码</strong>。复制并粘贴到下方:',
authCode: '授权码', authCode: '授权码',
@@ -1013,11 +1017,14 @@ export default {
generateAuthUrl: '生成授权链接', generateAuthUrl: '生成授权链接',
step2OpenUrl: '在浏览器中打开链接并完成授权', step2OpenUrl: '在浏览器中打开链接并完成授权',
openUrlDesc: '请在新标签页中打开授权链接,登录您的 OpenAI 账户并授权。', openUrlDesc: '请在新标签页中打开授权链接,登录您的 OpenAI 账户并授权。',
importantNotice: '<strong>重要提示:</strong>授权后页面可能会加载较长时间,请耐心等待。当浏览器地址栏变为 <code>http://localhost...</code> 开头时,表示授权已完成。', importantNotice:
'<strong>重要提示:</strong>授权后页面可能会加载较长时间,请耐心等待。当浏览器地址栏变为 <code>http://localhost...</code> 开头时,表示授权已完成。',
step3EnterCode: '输入授权链接或 Code', step3EnterCode: '输入授权链接或 Code',
authCodeDesc: '授权完成后,当页面地址变为 <code>http://localhost:xxx/auth/callback?code=...</code> 时:', authCodeDesc:
'授权完成后,当页面地址变为 <code>http://localhost:xxx/auth/callback?code=...</code> 时:',
authCode: '授权链接或 Code', authCode: '授权链接或 Code',
authCodePlaceholder: '方式1复制完整的链接\n(http://localhost:xxx/auth/callback?code=...)\n方式2仅复制 code 参数的值', authCodePlaceholder:
'方式1复制完整的链接\n(http://localhost:xxx/auth/callback?code=...)\n方式2仅复制 code 参数的值',
authCodeHint: '您可以直接复制整个链接或仅复制 code 参数值,系统会自动识别', authCodeHint: '您可以直接复制整个链接或仅复制 code 参数值,系统会自动识别',
}, },
}, },
@@ -1153,7 +1160,8 @@ export default {
standardAdd: '标准添加', standardAdd: '标准添加',
batchAdd: '快捷添加', batchAdd: '快捷添加',
batchInput: '代理列表', batchInput: '代理列表',
batchInputPlaceholder: "每行输入一个代理,支持以下格式:\nsocks5://user:pass{'@'}192.168.1.1:1080\nhttp://192.168.1.1:8080\nhttps://user:pass{'@'}proxy.example.com:443", batchInputPlaceholder:
"每行输入一个代理,支持以下格式:\nsocks5://user:pass{'@'}192.168.1.1:1080\nhttp://192.168.1.1:8080\nhttps://user:pass{'@'}proxy.example.com:443",
batchInputHint: "支持 http、https、socks5 协议,格式:协议://[用户名:密码{'@'}]主机:端口", batchInputHint: "支持 http、https、socks5 协议,格式:协议://[用户名:密码{'@'}]主机:端口",
parsedCount: '有效 {count} 个', parsedCount: '有效 {count} 个',
invalidCount: '无效 {count} 个', invalidCount: '无效 {count} 个',