feat(frontend): 增强用户界面和使用教程
主要改进: - 扩展 UseKeyModal 支持 Antigravity/Gemini 平台教程 - 添加 CCS (Claude Code Settings) 导入说明 - 添加混合渠道风险警告提示 - 优化登录/注册页面样式 - 更新 Antigravity 混合调度选项文案 - 完善中英文国际化文案
This commit is contained in:
@@ -8,7 +8,7 @@ import type {
|
|||||||
DashboardStats,
|
DashboardStats,
|
||||||
TrendDataPoint,
|
TrendDataPoint,
|
||||||
ModelStat,
|
ModelStat,
|
||||||
APIKeyUsageTrendPoint,
|
ApiKeyUsageTrendPoint,
|
||||||
UserUsageTrendPoint
|
UserUsageTrendPoint
|
||||||
} from '@/types'
|
} from '@/types'
|
||||||
|
|
||||||
@@ -93,7 +93,7 @@ export interface ApiKeyTrendParams extends TrendParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiKeyTrendResponse {
|
export interface ApiKeyTrendResponse {
|
||||||
trend: APIKeyUsageTrendPoint[]
|
trend: ApiKeyUsageTrendPoint[]
|
||||||
start_date: string
|
start_date: string
|
||||||
end_date: string
|
end_date: string
|
||||||
granularity: string
|
granularity: string
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import subscriptionsAPI from './subscriptions'
|
|||||||
import usageAPI from './usage'
|
import usageAPI from './usage'
|
||||||
import geminiAPI from './gemini'
|
import geminiAPI from './gemini'
|
||||||
import antigravityAPI from './antigravity'
|
import antigravityAPI from './antigravity'
|
||||||
import opsAPI from './ops'
|
|
||||||
import userAttributesAPI from './userAttributes'
|
import userAttributesAPI from './userAttributes'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -34,7 +33,6 @@ export const adminAPI = {
|
|||||||
usage: usageAPI,
|
usage: usageAPI,
|
||||||
gemini: geminiAPI,
|
gemini: geminiAPI,
|
||||||
antigravity: antigravityAPI,
|
antigravity: antigravityAPI,
|
||||||
ops: opsAPI,
|
|
||||||
userAttributes: userAttributesAPI
|
userAttributes: userAttributesAPI
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,7 +49,6 @@ export {
|
|||||||
usageAPI,
|
usageAPI,
|
||||||
geminiAPI,
|
geminiAPI,
|
||||||
antigravityAPI,
|
antigravityAPI,
|
||||||
opsAPI,
|
|
||||||
userAttributesAPI
|
userAttributesAPI
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,29 @@
|
|||||||
{{ platformDescription }}
|
{{ platformDescription }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- OS Tabs -->
|
<!-- Client Tabs (only for Antigravity platform) -->
|
||||||
|
<div v-if="platform === 'antigravity'" class="border-b border-gray-200 dark:border-dark-700">
|
||||||
|
<nav class="-mb-px flex space-x-6" aria-label="Client">
|
||||||
|
<button
|
||||||
|
v-for="tab in clientTabs"
|
||||||
|
:key="tab.id"
|
||||||
|
@click="activeClientTab = tab.id"
|
||||||
|
:class="[
|
||||||
|
'whitespace-nowrap py-2.5 px-1 border-b-2 font-medium text-sm transition-colors',
|
||||||
|
activeClientTab === tab.id
|
||||||
|
? 'border-primary-500 text-primary-600 dark:text-primary-400'
|
||||||
|
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<span class="flex items-center gap-2">
|
||||||
|
<component :is="tab.icon" class="w-4 h-4" />
|
||||||
|
{{ tab.label }}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- OS/Shell Tabs -->
|
||||||
<div class="border-b border-gray-200 dark:border-dark-700">
|
<div class="border-b border-gray-200 dark:border-dark-700">
|
||||||
<nav class="-mb-px flex space-x-4" aria-label="Tabs">
|
<nav class="-mb-px flex space-x-4" aria-label="Tabs">
|
||||||
<button
|
<button
|
||||||
@@ -154,16 +176,21 @@ const { copyToClipboard: clipboardCopy } = useClipboard()
|
|||||||
|
|
||||||
const copiedIndex = ref<number | null>(null)
|
const copiedIndex = ref<number | null>(null)
|
||||||
const activeTab = ref<string>('unix')
|
const activeTab = ref<string>('unix')
|
||||||
|
const activeClientTab = ref<string>('claude') // Level 1 tab for antigravity platform
|
||||||
|
|
||||||
// Reset active tab when platform changes
|
// Reset tabs when platform changes
|
||||||
watch(() => props.platform, (newPlatform) => {
|
watch(() => props.platform, (newPlatform) => {
|
||||||
if (newPlatform === 'openai') {
|
activeTab.value = 'unix'
|
||||||
activeTab.value = 'unix'
|
if (newPlatform === 'antigravity') {
|
||||||
} else {
|
activeClientTab.value = 'claude'
|
||||||
activeTab.value = 'unix'
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Reset shell tab when client changes (for antigravity)
|
||||||
|
watch(activeClientTab, () => {
|
||||||
|
activeTab.value = 'unix'
|
||||||
|
})
|
||||||
|
|
||||||
// Icon components
|
// Icon components
|
||||||
const AppleIcon = {
|
const AppleIcon = {
|
||||||
render() {
|
render() {
|
||||||
@@ -189,8 +216,52 @@ const WindowsIcon = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Anthropic tabs (3 shell types)
|
// Terminal icon for Claude Code
|
||||||
const anthropicTabs: TabConfig[] = [
|
const TerminalIcon = {
|
||||||
|
render() {
|
||||||
|
return h('svg', {
|
||||||
|
fill: 'none',
|
||||||
|
stroke: 'currentColor',
|
||||||
|
viewBox: '0 0 24 24',
|
||||||
|
'stroke-width': '1.5',
|
||||||
|
class: 'w-4 h-4'
|
||||||
|
}, [
|
||||||
|
h('path', {
|
||||||
|
'stroke-linecap': 'round',
|
||||||
|
'stroke-linejoin': 'round',
|
||||||
|
d: 'm6.75 7.5 3 2.25-3 2.25m4.5 0h3m-9 8.25h13.5A2.25 2.25 0 0 0 21 17.25V6.75A2.25 2.25 0 0 0 18.75 4.5H5.25A2.25 2.25 0 0 0 3 6.75v10.5A2.25 2.25 0 0 0 5.25 20.25Z'
|
||||||
|
})
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sparkle icon for Gemini
|
||||||
|
const SparkleIcon = {
|
||||||
|
render() {
|
||||||
|
return h('svg', {
|
||||||
|
fill: 'none',
|
||||||
|
stroke: 'currentColor',
|
||||||
|
viewBox: '0 0 24 24',
|
||||||
|
'stroke-width': '1.5',
|
||||||
|
class: 'w-4 h-4'
|
||||||
|
}, [
|
||||||
|
h('path', {
|
||||||
|
'stroke-linecap': 'round',
|
||||||
|
'stroke-linejoin': 'round',
|
||||||
|
d: 'M9.813 15.904 9 18.75l-.813-2.846a4.5 4.5 0 0 0-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 0 0 3.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 0 0 3.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 0 0-3.09 3.09ZM18.259 8.715 18 9.75l-.259-1.035a3.375 3.375 0 0 0-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 0 0 2.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 0 0 2.456 2.456L21.75 6l-1.035.259a3.375 3.375 0 0 0-2.456 2.456ZM16.894 20.567 16.5 21.75l-.394-1.183a2.25 2.25 0 0 0-1.423-1.423L13.5 18.75l1.183-.394a2.25 2.25 0 0 0 1.423-1.423l.394-1.183.394 1.183a2.25 2.25 0 0 0 1.423 1.423l1.183.394-1.183.394a2.25 2.25 0 0 0-1.423 1.423Z'
|
||||||
|
})
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client tabs for Antigravity platform (Level 1)
|
||||||
|
const clientTabs = computed((): TabConfig[] => [
|
||||||
|
{ id: 'claude', label: t('keys.useKeyModal.antigravity.claudeCode'), icon: TerminalIcon },
|
||||||
|
{ id: 'gemini', label: t('keys.useKeyModal.antigravity.geminiCli'), icon: SparkleIcon }
|
||||||
|
])
|
||||||
|
|
||||||
|
// Shell tabs (3 types for environment variable based configs)
|
||||||
|
const shellTabs: TabConfig[] = [
|
||||||
{ id: 'unix', label: 'macOS / Linux', icon: AppleIcon },
|
{ id: 'unix', label: 'macOS / Linux', icon: AppleIcon },
|
||||||
{ id: 'cmd', label: 'Windows CMD', icon: WindowsIcon },
|
{ id: 'cmd', label: 'Windows CMD', icon: WindowsIcon },
|
||||||
{ id: 'powershell', label: 'PowerShell', icon: WindowsIcon }
|
{ id: 'powershell', label: 'PowerShell', icon: WindowsIcon }
|
||||||
@@ -204,26 +275,40 @@ const openaiTabs: TabConfig[] = [
|
|||||||
|
|
||||||
const currentTabs = computed(() => {
|
const currentTabs = computed(() => {
|
||||||
if (props.platform === 'openai') {
|
if (props.platform === 'openai') {
|
||||||
return openaiTabs
|
return openaiTabs // 2 tabs: unix, windows
|
||||||
}
|
}
|
||||||
return anthropicTabs
|
// All other platforms (anthropic, gemini, antigravity) use shell tabs
|
||||||
|
return shellTabs
|
||||||
})
|
})
|
||||||
|
|
||||||
const platformDescription = computed(() => {
|
const platformDescription = computed(() => {
|
||||||
if (props.platform === 'openai') {
|
switch (props.platform) {
|
||||||
return t('keys.useKeyModal.openai.description')
|
case 'openai':
|
||||||
|
return t('keys.useKeyModal.openai.description')
|
||||||
|
case 'gemini':
|
||||||
|
return t('keys.useKeyModal.gemini.description')
|
||||||
|
case 'antigravity':
|
||||||
|
return t('keys.useKeyModal.antigravity.description')
|
||||||
|
default:
|
||||||
|
return t('keys.useKeyModal.description')
|
||||||
}
|
}
|
||||||
return t('keys.useKeyModal.description')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const platformNote = computed(() => {
|
const platformNote = computed(() => {
|
||||||
if (props.platform === 'openai') {
|
switch (props.platform) {
|
||||||
if (activeTab.value === 'windows') {
|
case 'openai':
|
||||||
return t('keys.useKeyModal.openai.noteWindows')
|
return activeTab.value === 'windows'
|
||||||
}
|
? t('keys.useKeyModal.openai.noteWindows')
|
||||||
return t('keys.useKeyModal.openai.note')
|
: t('keys.useKeyModal.openai.note')
|
||||||
|
case 'gemini':
|
||||||
|
return t('keys.useKeyModal.gemini.note')
|
||||||
|
case 'antigravity':
|
||||||
|
return activeClientTab.value === 'claude'
|
||||||
|
? t('keys.useKeyModal.antigravity.claudeNote')
|
||||||
|
: t('keys.useKeyModal.antigravity.geminiNote')
|
||||||
|
default:
|
||||||
|
return t('keys.useKeyModal.note')
|
||||||
}
|
}
|
||||||
return t('keys.useKeyModal.note')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Syntax highlighting helpers
|
// Syntax highlighting helpers
|
||||||
@@ -239,11 +324,20 @@ const currentFiles = computed((): FileConfig[] => {
|
|||||||
const baseUrl = props.baseUrl || window.location.origin
|
const baseUrl = props.baseUrl || window.location.origin
|
||||||
const apiKey = props.apiKey
|
const apiKey = props.apiKey
|
||||||
|
|
||||||
if (props.platform === 'openai') {
|
switch (props.platform) {
|
||||||
return generateOpenAIFiles(baseUrl, apiKey)
|
case 'openai':
|
||||||
|
return generateOpenAIFiles(baseUrl, apiKey)
|
||||||
|
case 'gemini':
|
||||||
|
return [generateGeminiCliContent(baseUrl, apiKey)]
|
||||||
|
case 'antigravity':
|
||||||
|
// Both Claude Code and Gemini CLI need /antigravity suffix for antigravity platform
|
||||||
|
if (activeClientTab.value === 'claude') {
|
||||||
|
return generateAnthropicFiles(`${baseUrl}/antigravity`, apiKey)
|
||||||
|
}
|
||||||
|
return [generateGeminiCliContent(`${baseUrl}/antigravity`, apiKey)]
|
||||||
|
default: // anthropic
|
||||||
|
return generateAnthropicFiles(baseUrl, apiKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
return generateAnthropicFiles(baseUrl, apiKey)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
function generateAnthropicFiles(baseUrl: string, apiKey: string): FileConfig[] {
|
function generateAnthropicFiles(baseUrl: string, apiKey: string): FileConfig[] {
|
||||||
@@ -282,6 +376,51 @@ ${keyword('$env:')}${variable('ANTHROPIC_AUTH_TOKEN')}${operator('=')}${string(`
|
|||||||
return [{ path, content, highlighted }]
|
return [{ path, content, highlighted }]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function generateGeminiCliContent(baseUrl: string, apiKey: string): FileConfig {
|
||||||
|
const model = 'gemini-2.5-pro'
|
||||||
|
const modelComment = t('keys.useKeyModal.gemini.modelComment')
|
||||||
|
let path: string
|
||||||
|
let content: string
|
||||||
|
let highlighted: string
|
||||||
|
|
||||||
|
switch (activeTab.value) {
|
||||||
|
case 'unix':
|
||||||
|
path = 'Terminal'
|
||||||
|
content = `export GOOGLE_GEMINI_BASE_URL="${baseUrl}"
|
||||||
|
export GEMINI_API_KEY="${apiKey}"
|
||||||
|
export GEMINI_MODEL="${model}" # ${modelComment}`
|
||||||
|
highlighted = `${keyword('export')} ${variable('GOOGLE_GEMINI_BASE_URL')}${operator('=')}${string(`"${baseUrl}"`)}
|
||||||
|
${keyword('export')} ${variable('GEMINI_API_KEY')}${operator('=')}${string(`"${apiKey}"`)}
|
||||||
|
${keyword('export')} ${variable('GEMINI_MODEL')}${operator('=')}${string(`"${model}"`)} ${comment(`# ${modelComment}`)}`
|
||||||
|
break
|
||||||
|
case 'cmd':
|
||||||
|
path = 'Command Prompt'
|
||||||
|
content = `set GOOGLE_GEMINI_BASE_URL=${baseUrl}
|
||||||
|
set GEMINI_API_KEY=${apiKey}
|
||||||
|
set GEMINI_MODEL=${model}`
|
||||||
|
highlighted = `${keyword('set')} ${variable('GOOGLE_GEMINI_BASE_URL')}${operator('=')}${baseUrl}
|
||||||
|
${keyword('set')} ${variable('GEMINI_API_KEY')}${operator('=')}${apiKey}
|
||||||
|
${keyword('set')} ${variable('GEMINI_MODEL')}${operator('=')}${model}
|
||||||
|
${comment(`REM ${modelComment}`)}`
|
||||||
|
break
|
||||||
|
case 'powershell':
|
||||||
|
path = 'PowerShell'
|
||||||
|
content = `$env:GOOGLE_GEMINI_BASE_URL="${baseUrl}"
|
||||||
|
$env:GEMINI_API_KEY="${apiKey}"
|
||||||
|
$env:GEMINI_MODEL="${model}" # ${modelComment}`
|
||||||
|
highlighted = `${keyword('$env:')}${variable('GOOGLE_GEMINI_BASE_URL')}${operator('=')}${string(`"${baseUrl}"`)}
|
||||||
|
${keyword('$env:')}${variable('GEMINI_API_KEY')}${operator('=')}${string(`"${apiKey}"`)}
|
||||||
|
${keyword('$env:')}${variable('GEMINI_MODEL')}${operator('=')}${string(`"${model}"`)} ${comment(`# ${modelComment}`)}`
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
path = 'Terminal'
|
||||||
|
content = ''
|
||||||
|
highlighted = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
return { path, content, highlighted }
|
||||||
|
}
|
||||||
|
|
||||||
function generateOpenAIFiles(baseUrl: string, apiKey: string): FileConfig[] {
|
function generateOpenAIFiles(baseUrl: string, apiKey: string): FileConfig[] {
|
||||||
const isWindows = activeTab.value === 'windows'
|
const isWindows = activeTab.value === 'windows'
|
||||||
const configDir = isWindows ? '%userprofile%\\.codex' : '~/.codex'
|
const configDir = isWindows ? '%userprofile%\\.codex' : '~/.codex'
|
||||||
|
|||||||
@@ -183,21 +183,6 @@ const DashboardIcon = {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const ActivityIcon = {
|
|
||||||
render: () =>
|
|
||||||
h(
|
|
||||||
'svg',
|
|
||||||
{ fill: 'none', viewBox: '0 0 24 24', stroke: 'currentColor', 'stroke-width': '1.5' },
|
|
||||||
[
|
|
||||||
h('path', {
|
|
||||||
'stroke-linecap': 'round',
|
|
||||||
'stroke-linejoin': 'round',
|
|
||||||
d: 'M3 12h4l3 6 4-12 3 6h4'
|
|
||||||
})
|
|
||||||
]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const KeyIcon = {
|
const KeyIcon = {
|
||||||
render: () =>
|
render: () =>
|
||||||
h(
|
h(
|
||||||
@@ -457,7 +442,6 @@ const personalNavItems = computed(() => {
|
|||||||
const adminNavItems = computed(() => {
|
const adminNavItems = computed(() => {
|
||||||
const baseItems = [
|
const baseItems = [
|
||||||
{ path: '/admin/dashboard', label: t('nav.dashboard'), icon: DashboardIcon },
|
{ path: '/admin/dashboard', label: t('nav.dashboard'), icon: DashboardIcon },
|
||||||
{ path: '/admin/ops', label: t('nav.ops'), icon: ActivityIcon },
|
|
||||||
{ path: '/admin/users', label: t('nav.users'), icon: UsersIcon, hideInSimpleMode: true },
|
{ path: '/admin/users', label: t('nav.users'), icon: UsersIcon, hideInSimpleMode: true },
|
||||||
{ path: '/admin/groups', label: t('nav.groups'), icon: FolderIcon, hideInSimpleMode: true },
|
{ path: '/admin/groups', label: t('nav.groups'), icon: FolderIcon, hideInSimpleMode: true },
|
||||||
{ path: '/admin/subscriptions', label: t('nav.subscriptions'), icon: CreditCardIcon, hideInSimpleMode: true },
|
{ path: '/admin/subscriptions', label: t('nav.subscriptions'), icon: CreditCardIcon, hideInSimpleMode: true },
|
||||||
|
|||||||
@@ -127,8 +127,6 @@ export default {
|
|||||||
total: 'Total',
|
total: 'Total',
|
||||||
balance: 'Balance',
|
balance: 'Balance',
|
||||||
available: 'Available',
|
available: 'Available',
|
||||||
copy: 'Copy',
|
|
||||||
details: 'Details',
|
|
||||||
copiedToClipboard: 'Copied to clipboard',
|
copiedToClipboard: 'Copied to clipboard',
|
||||||
copyFailed: 'Failed to copy',
|
copyFailed: 'Failed to copy',
|
||||||
contactSupport: 'Contact Support',
|
contactSupport: 'Contact Support',
|
||||||
@@ -137,6 +135,9 @@ export default {
|
|||||||
noOptionsFound: 'No options found',
|
noOptionsFound: 'No options found',
|
||||||
saving: 'Saving...',
|
saving: 'Saving...',
|
||||||
refresh: 'Refresh',
|
refresh: 'Refresh',
|
||||||
|
notAvailable: 'N/A',
|
||||||
|
now: 'Now',
|
||||||
|
unknown: 'Unknown',
|
||||||
time: {
|
time: {
|
||||||
never: 'Never',
|
never: 'Never',
|
||||||
justNow: 'Just now',
|
justNow: 'Just now',
|
||||||
@@ -149,7 +150,6 @@ export default {
|
|||||||
// Navigation
|
// Navigation
|
||||||
nav: {
|
nav: {
|
||||||
dashboard: 'Dashboard',
|
dashboard: 'Dashboard',
|
||||||
ops: 'Ops Center',
|
|
||||||
apiKeys: 'API Keys',
|
apiKeys: 'API Keys',
|
||||||
usage: 'Usage',
|
usage: 'Usage',
|
||||||
redeem: 'Redeem',
|
redeem: 'Redeem',
|
||||||
@@ -324,6 +324,18 @@ export default {
|
|||||||
note: 'Make sure the config directory exists. macOS/Linux users can run mkdir -p ~/.codex to create it.',
|
note: 'Make sure the config directory exists. macOS/Linux users can run mkdir -p ~/.codex to create it.',
|
||||||
noteWindows: 'Press Win+R and enter %userprofile%\\.codex to open the config directory. Create it manually if it does not exist.',
|
noteWindows: 'Press Win+R and enter %userprofile%\\.codex to open the config directory. Create it manually if it does not exist.',
|
||||||
},
|
},
|
||||||
|
antigravity: {
|
||||||
|
description: 'Configure API access for Antigravity group. Select the configuration method based on your client.',
|
||||||
|
claudeCode: 'Claude Code',
|
||||||
|
geminiCli: 'Gemini CLI',
|
||||||
|
claudeNote: 'These environment variables will be active in the current terminal session. For permanent configuration, add them to ~/.bashrc, ~/.zshrc, or the appropriate configuration file.',
|
||||||
|
geminiNote: 'These environment variables will be active in the current terminal session. For permanent configuration, add them to ~/.bashrc, ~/.zshrc, or the appropriate configuration file.',
|
||||||
|
},
|
||||||
|
gemini: {
|
||||||
|
description: 'Add the following environment variables to your terminal profile or run directly in terminal to configure Gemini CLI access.',
|
||||||
|
modelComment: 'If you have Gemini 3 access, you can use: gemini-3-pro-preview',
|
||||||
|
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.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
customKeyLabel: 'Custom Key',
|
customKeyLabel: 'Custom Key',
|
||||||
customKeyPlaceholder: 'Enter your custom key (min 16 chars)',
|
customKeyPlaceholder: 'Enter your custom key (min 16 chars)',
|
||||||
@@ -331,7 +343,15 @@ export default {
|
|||||||
customKeyTooShort: 'Custom key must be at least 16 characters',
|
customKeyTooShort: 'Custom key must be at least 16 characters',
|
||||||
customKeyInvalidChars: 'Custom key can only contain letters, numbers, underscores, and hyphens',
|
customKeyInvalidChars: 'Custom key can only contain letters, numbers, underscores, and hyphens',
|
||||||
customKeyRequired: 'Please enter a custom key',
|
customKeyRequired: 'Please enter a custom key',
|
||||||
ccSwitchNotInstalled: 'CC-Switch is not installed or the protocol handler is not registered. Please install CC-Switch first or manually copy the API key.'
|
ccSwitchNotInstalled: 'CC-Switch is not installed or the protocol handler is not registered. Please install CC-Switch first or manually copy the API key.',
|
||||||
|
ccsClientSelect: {
|
||||||
|
title: 'Select Client',
|
||||||
|
description: 'Please select the client type to import to CC-Switch:',
|
||||||
|
claudeCode: 'Claude Code',
|
||||||
|
claudeCodeDesc: 'Import as Claude Code configuration',
|
||||||
|
geminiCli: 'Gemini CLI',
|
||||||
|
geminiCliDesc: 'Import as Gemini CLI configuration',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// Usage
|
// Usage
|
||||||
@@ -549,123 +569,6 @@ export default {
|
|||||||
recentUsage: 'Recent Usage',
|
recentUsage: 'Recent Usage',
|
||||||
failedToLoad: 'Failed to load dashboard statistics'
|
failedToLoad: 'Failed to load dashboard statistics'
|
||||||
},
|
},
|
||||||
ops: {
|
|
||||||
title: 'Ops Monitoring Center 2.0',
|
|
||||||
description: 'Stability metrics, error distribution, and system health',
|
|
||||||
status: {
|
|
||||||
title: 'System Health Snapshot',
|
|
||||||
subtitle: 'Real-time metrics and error visibility',
|
|
||||||
systemNormal: 'System Normal',
|
|
||||||
systemDegraded: 'System Degraded',
|
|
||||||
systemDown: 'System Down',
|
|
||||||
noData: 'No Data',
|
|
||||||
monitoring: 'Monitoring',
|
|
||||||
lastUpdated: 'Last Updated',
|
|
||||||
live: 'Live',
|
|
||||||
waiting: 'Waiting for data',
|
|
||||||
realtime: 'Connected',
|
|
||||||
disconnected: 'Disconnected'
|
|
||||||
},
|
|
||||||
charts: {
|
|
||||||
errorTrend: 'Error Trend',
|
|
||||||
errorDistribution: 'Error Distribution',
|
|
||||||
errorRate: 'Error Rate',
|
|
||||||
requestCount: 'Request Count',
|
|
||||||
rateLimits: 'Rate Limits (429)',
|
|
||||||
serverErrors: 'Server Errors (5xx)',
|
|
||||||
clientErrors: 'Client Errors (4xx)',
|
|
||||||
otherErrors: 'Other',
|
|
||||||
latencyDist: 'Latency Distribution',
|
|
||||||
providerSla: 'Upstream SLA Comparison',
|
|
||||||
errorDist: 'Error Type Distribution',
|
|
||||||
systemStatus: 'System Resources'
|
|
||||||
},
|
|
||||||
metrics: {
|
|
||||||
successRate: 'Success Rate',
|
|
||||||
errorRate: 'Error Rate',
|
|
||||||
p95: 'P95 Latency',
|
|
||||||
p99: 'P99 Latency',
|
|
||||||
http2Errors: 'HTTP/2 Errors',
|
|
||||||
activeAlerts: 'Active Alerts',
|
|
||||||
cpuUsage: 'CPU Usage',
|
|
||||||
queueDepth: 'Queue Depth',
|
|
||||||
healthScore: 'Health Score',
|
|
||||||
sla: 'Availability (SLA)',
|
|
||||||
qps: 'Real-time QPS',
|
|
||||||
tps: 'Real-time TPS',
|
|
||||||
errorCount: 'Error Count'
|
|
||||||
},
|
|
||||||
errors: {
|
|
||||||
title: 'Recent Errors',
|
|
||||||
subtitle: 'Inspect failures across platforms and phases',
|
|
||||||
count: '{n} errors'
|
|
||||||
},
|
|
||||||
filters: {
|
|
||||||
allSeverities: 'All severities',
|
|
||||||
allPlatforms: 'All platforms',
|
|
||||||
allPhases: 'All phases',
|
|
||||||
p0: 'P0 (Critical)',
|
|
||||||
p1: 'P1 (High)',
|
|
||||||
p2: 'P2 (Medium)',
|
|
||||||
p3: 'P3 (Low)'
|
|
||||||
},
|
|
||||||
searchPlaceholder: 'Search by request ID, model, or message',
|
|
||||||
range: {
|
|
||||||
'15m': 'Last 15 minutes',
|
|
||||||
'1h': 'Last 1 hour',
|
|
||||||
'24h': 'Last 24 hours',
|
|
||||||
'7d': 'Last 7 days'
|
|
||||||
},
|
|
||||||
platform: {
|
|
||||||
anthropic: 'Anthropic',
|
|
||||||
openai: 'OpenAI',
|
|
||||||
gemini: 'Gemini',
|
|
||||||
antigravity: 'Antigravity'
|
|
||||||
},
|
|
||||||
phase: {
|
|
||||||
auth: 'Auth',
|
|
||||||
concurrency: 'Concurrency',
|
|
||||||
billing: 'Billing',
|
|
||||||
scheduling: 'Scheduling',
|
|
||||||
network: 'Network',
|
|
||||||
upstream: 'Upstream',
|
|
||||||
response: 'Response',
|
|
||||||
internal: 'Internal'
|
|
||||||
},
|
|
||||||
severity: {
|
|
||||||
p0: 'P0',
|
|
||||||
p1: 'P1',
|
|
||||||
p2: 'P2',
|
|
||||||
p3: 'P3'
|
|
||||||
},
|
|
||||||
table: {
|
|
||||||
time: 'Time',
|
|
||||||
severity: 'Severity',
|
|
||||||
phase: 'Phase',
|
|
||||||
statusCode: 'Status',
|
|
||||||
platform: 'Platform',
|
|
||||||
model: 'Model',
|
|
||||||
latency: 'Latency',
|
|
||||||
requestId: 'Request ID',
|
|
||||||
message: 'Message'
|
|
||||||
},
|
|
||||||
details: {
|
|
||||||
title: 'Error Details',
|
|
||||||
requestId: 'Request ID',
|
|
||||||
errorMessage: 'Error Message',
|
|
||||||
requestPath: 'Request path',
|
|
||||||
clientIp: 'Client IP',
|
|
||||||
userId: 'User ID',
|
|
||||||
apiKeyId: 'API Key ID',
|
|
||||||
groupId: 'Group ID',
|
|
||||||
stream: 'Stream'
|
|
||||||
},
|
|
||||||
empty: {
|
|
||||||
title: 'No ops data yet',
|
|
||||||
subtitle: 'Enable error logging and metrics to populate this view'
|
|
||||||
},
|
|
||||||
failedToLoad: 'Failed to load ops data'
|
|
||||||
},
|
|
||||||
|
|
||||||
// Users
|
// Users
|
||||||
users: {
|
users: {
|
||||||
@@ -1030,6 +933,54 @@ export default {
|
|||||||
codeAssist: 'Code Assist',
|
codeAssist: 'Code Assist',
|
||||||
antigravityOauth: 'Antigravity OAuth'
|
antigravityOauth: 'Antigravity OAuth'
|
||||||
},
|
},
|
||||||
|
status: {
|
||||||
|
paused: 'Paused',
|
||||||
|
limited: 'Limited',
|
||||||
|
tempUnschedulable: 'Temp Unschedulable'
|
||||||
|
},
|
||||||
|
tempUnschedulable: {
|
||||||
|
title: 'Temp Unschedulable',
|
||||||
|
statusTitle: 'Temp Unschedulable Status',
|
||||||
|
hint: 'Disable accounts temporarily when error code and keyword both match.',
|
||||||
|
notice: 'Rules are evaluated in order and require both error code and keyword match.',
|
||||||
|
addRule: 'Add Rule',
|
||||||
|
ruleOrder: 'Rule Order',
|
||||||
|
ruleIndex: 'Rule #{index}',
|
||||||
|
errorCode: 'Error Code',
|
||||||
|
errorCodePlaceholder: 'e.g. 429',
|
||||||
|
durationMinutes: 'Duration (minutes)',
|
||||||
|
durationPlaceholder: 'e.g. 30',
|
||||||
|
keywords: 'Keywords',
|
||||||
|
keywordsPlaceholder: 'e.g. overloaded, too many requests',
|
||||||
|
keywordsHint: 'Separate keywords with commas; any keyword match will trigger.',
|
||||||
|
description: 'Description',
|
||||||
|
descriptionPlaceholder: 'Optional note for this rule',
|
||||||
|
rulesInvalid: 'Add at least one rule with error code, keywords, and duration.',
|
||||||
|
viewDetails: 'View temp unschedulable details',
|
||||||
|
accountName: 'Account',
|
||||||
|
triggeredAt: 'Triggered At',
|
||||||
|
until: 'Until',
|
||||||
|
remaining: 'Remaining',
|
||||||
|
matchedKeyword: 'Matched Keyword',
|
||||||
|
errorMessage: 'Error Details',
|
||||||
|
reset: 'Reset Status',
|
||||||
|
resetSuccess: 'Temp unschedulable status reset',
|
||||||
|
resetFailed: 'Failed to reset temp unschedulable status',
|
||||||
|
failedToLoad: 'Failed to load temp unschedulable status',
|
||||||
|
notActive: 'This account is not temporarily unschedulable.',
|
||||||
|
expired: 'Expired',
|
||||||
|
remainingMinutes: 'About {minutes} minutes',
|
||||||
|
remainingHours: 'About {hours} hours',
|
||||||
|
remainingHoursMinutes: 'About {hours} hours {minutes} minutes',
|
||||||
|
presets: {
|
||||||
|
overloadLabel: '529 Overloaded',
|
||||||
|
overloadDesc: 'Overloaded - pause 60 minutes',
|
||||||
|
rateLimitLabel: '429 Rate Limit',
|
||||||
|
rateLimitDesc: 'Rate limited - pause 10 minutes',
|
||||||
|
unavailableLabel: '503 Unavailable',
|
||||||
|
unavailableDesc: 'Unavailable - pause 30 minutes'
|
||||||
|
}
|
||||||
|
},
|
||||||
columns: {
|
columns: {
|
||||||
name: 'Name',
|
name: 'Name',
|
||||||
platformType: 'Platform/Type',
|
platformType: 'Platform/Type',
|
||||||
@@ -1147,10 +1098,10 @@ export default {
|
|||||||
priority: 'Priority',
|
priority: 'Priority',
|
||||||
priorityHint: 'Higher priority accounts are used first',
|
priorityHint: 'Higher priority accounts are used first',
|
||||||
higherPriorityFirst: 'Higher value means higher priority',
|
higherPriorityFirst: 'Higher value means higher priority',
|
||||||
mixedScheduling: 'Mixed Scheduling',
|
mixedScheduling: 'Use in /v1/messages',
|
||||||
mixedSchedulingHint: 'Enable to participate in Anthropic/Gemini group scheduling',
|
mixedSchedulingHint: 'Enable to participate in Anthropic/Gemini group scheduling',
|
||||||
mixedSchedulingTooltip:
|
mixedSchedulingTooltip:
|
||||||
'When enabled, this account can be scheduled by /v1/messages and /v1beta endpoints. Otherwise, it will only be scheduled by /antigravity. Note: Anthropic Claude and Antigravity Claude cannot be mixed in the same context. Please manage groups carefully when enabled.',
|
'!! WARNING !! Antigravity Claude and Anthropic Claude cannot be used in the same context. If you have both Anthropic and Antigravity accounts, enabling this option will cause frequent 400 errors. When enabled, please use the group feature to isolate Antigravity accounts from Anthropic accounts. Make sure you understand this before enabling!!',
|
||||||
creating: 'Creating...',
|
creating: 'Creating...',
|
||||||
updating: 'Updating...',
|
updating: 'Updating...',
|
||||||
accountCreated: 'Account created successfully',
|
accountCreated: 'Account created successfully',
|
||||||
@@ -1664,6 +1615,7 @@ export default {
|
|||||||
siteKey: 'Site Key',
|
siteKey: 'Site Key',
|
||||||
secretKey: 'Secret Key',
|
secretKey: 'Secret Key',
|
||||||
siteKeyHint: 'Get this from your Cloudflare Dashboard',
|
siteKeyHint: 'Get this from your Cloudflare Dashboard',
|
||||||
|
cloudflareDashboard: 'Cloudflare Dashboard',
|
||||||
secretKeyHint: 'Server-side verification key (keep this secret)'
|
secretKeyHint: 'Server-side verification key (keep this secret)'
|
||||||
},
|
},
|
||||||
defaults: {
|
defaults: {
|
||||||
@@ -1814,6 +1766,7 @@ export default {
|
|||||||
noActiveSubscriptions: 'No Active Subscriptions',
|
noActiveSubscriptions: 'No Active Subscriptions',
|
||||||
noActiveSubscriptionsDesc:
|
noActiveSubscriptionsDesc:
|
||||||
"You don't have any active subscriptions. Contact administrator to get one.",
|
"You don't have any active subscriptions. Contact administrator to get one.",
|
||||||
|
failedToLoad: 'Failed to load subscriptions',
|
||||||
status: {
|
status: {
|
||||||
active: 'Active',
|
active: 'Active',
|
||||||
expired: 'Expired',
|
expired: 'Expired',
|
||||||
|
|||||||
@@ -124,8 +124,6 @@ export default {
|
|||||||
total: '总计',
|
total: '总计',
|
||||||
balance: '余额',
|
balance: '余额',
|
||||||
available: '可用',
|
available: '可用',
|
||||||
copy: '复制',
|
|
||||||
details: '详情',
|
|
||||||
copiedToClipboard: '已复制到剪贴板',
|
copiedToClipboard: '已复制到剪贴板',
|
||||||
copyFailed: '复制失败',
|
copyFailed: '复制失败',
|
||||||
contactSupport: '联系客服',
|
contactSupport: '联系客服',
|
||||||
@@ -134,6 +132,9 @@ export default {
|
|||||||
noOptionsFound: '无匹配选项',
|
noOptionsFound: '无匹配选项',
|
||||||
saving: '保存中...',
|
saving: '保存中...',
|
||||||
refresh: '刷新',
|
refresh: '刷新',
|
||||||
|
notAvailable: '不可用',
|
||||||
|
now: '现在',
|
||||||
|
unknown: '未知',
|
||||||
time: {
|
time: {
|
||||||
never: '从未',
|
never: '从未',
|
||||||
justNow: '刚刚',
|
justNow: '刚刚',
|
||||||
@@ -146,7 +147,6 @@ export default {
|
|||||||
// Navigation
|
// Navigation
|
||||||
nav: {
|
nav: {
|
||||||
dashboard: '仪表盘',
|
dashboard: '仪表盘',
|
||||||
ops: '运维监控',
|
|
||||||
apiKeys: 'API 密钥',
|
apiKeys: 'API 密钥',
|
||||||
usage: '使用记录',
|
usage: '使用记录',
|
||||||
redeem: '兑换',
|
redeem: '兑换',
|
||||||
@@ -320,6 +320,18 @@ export default {
|
|||||||
note: '请确保配置目录存在。macOS/Linux 用户可运行 mkdir -p ~/.codex 创建目录。',
|
note: '请确保配置目录存在。macOS/Linux 用户可运行 mkdir -p ~/.codex 创建目录。',
|
||||||
noteWindows: '按 Win+R,输入 %userprofile%\\.codex 打开配置目录。如目录不存在,请先手动创建。',
|
noteWindows: '按 Win+R,输入 %userprofile%\\.codex 打开配置目录。如目录不存在,请先手动创建。',
|
||||||
},
|
},
|
||||||
|
antigravity: {
|
||||||
|
description: '为 Antigravity 分组配置 API 访问。请根据您使用的客户端选择对应的配置方式。',
|
||||||
|
claudeCode: 'Claude Code',
|
||||||
|
geminiCli: 'Gemini CLI',
|
||||||
|
claudeNote: '这些环境变量将在当前终端会话中生效。如需永久配置,请将其添加到 ~/.bashrc、~/.zshrc 或相应的配置文件中。',
|
||||||
|
geminiNote: '这些环境变量将在当前终端会话中生效。如需永久配置,请将其添加到 ~/.bashrc、~/.zshrc 或相应的配置文件中。',
|
||||||
|
},
|
||||||
|
gemini: {
|
||||||
|
description: '将以下环境变量添加到您的终端配置文件或直接在终端中运行,以配置 Gemini CLI 访问。',
|
||||||
|
modelComment: '如果你有 Gemini 3 权限可以填:gemini-3-pro-preview',
|
||||||
|
note: '这些环境变量将在当前终端会话中生效。如需永久配置,请将其添加到 ~/.bashrc、~/.zshrc 或相应的配置文件中。',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
customKeyLabel: '自定义密钥',
|
customKeyLabel: '自定义密钥',
|
||||||
customKeyPlaceholder: '输入自定义密钥(至少16个字符)',
|
customKeyPlaceholder: '输入自定义密钥(至少16个字符)',
|
||||||
@@ -327,7 +339,15 @@ export default {
|
|||||||
customKeyTooShort: '自定义密钥至少需要16个字符',
|
customKeyTooShort: '自定义密钥至少需要16个字符',
|
||||||
customKeyInvalidChars: '自定义密钥只能包含字母、数字、下划线和连字符',
|
customKeyInvalidChars: '自定义密钥只能包含字母、数字、下划线和连字符',
|
||||||
customKeyRequired: '请输入自定义密钥',
|
customKeyRequired: '请输入自定义密钥',
|
||||||
ccSwitchNotInstalled: 'CC-Switch 未安装或协议处理程序未注册。请先安装 CC-Switch 或手动复制 API 密钥。'
|
ccSwitchNotInstalled: 'CC-Switch 未安装或协议处理程序未注册。请先安装 CC-Switch 或手动复制 API 密钥。',
|
||||||
|
ccsClientSelect: {
|
||||||
|
title: '选择客户端',
|
||||||
|
description: '请选择您要导入到 CC-Switch 的客户端类型:',
|
||||||
|
claudeCode: 'Claude Code',
|
||||||
|
claudeCodeDesc: '导入为 Claude Code 配置',
|
||||||
|
geminiCli: 'Gemini CLI',
|
||||||
|
geminiCliDesc: '导入为 Gemini CLI 配置',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// Usage
|
// Usage
|
||||||
@@ -562,123 +582,6 @@ export default {
|
|||||||
configureSystem: '配置系统设置',
|
configureSystem: '配置系统设置',
|
||||||
failedToLoad: '加载仪表盘数据失败'
|
failedToLoad: '加载仪表盘数据失败'
|
||||||
},
|
},
|
||||||
ops: {
|
|
||||||
title: '运维监控中心 2.0',
|
|
||||||
description: '稳定性指标、错误分布与系统健康',
|
|
||||||
status: {
|
|
||||||
title: '系统健康快照',
|
|
||||||
subtitle: '实时指标与错误可见性',
|
|
||||||
systemNormal: '系统正常',
|
|
||||||
systemDegraded: '系统降级',
|
|
||||||
systemDown: '系统异常',
|
|
||||||
noData: '无数据',
|
|
||||||
monitoring: '监控中',
|
|
||||||
lastUpdated: '最后更新',
|
|
||||||
live: '实时',
|
|
||||||
waiting: '等待数据',
|
|
||||||
realtime: '实时连接中',
|
|
||||||
disconnected: '连接已断开'
|
|
||||||
},
|
|
||||||
charts: {
|
|
||||||
errorTrend: '错误趋势',
|
|
||||||
errorDistribution: '错误分布',
|
|
||||||
errorRate: '错误率',
|
|
||||||
requestCount: '请求数',
|
|
||||||
rateLimits: '限流 (429)',
|
|
||||||
serverErrors: '服务端错误 (5xx)',
|
|
||||||
clientErrors: '客户端错误 (4xx)',
|
|
||||||
otherErrors: '其他',
|
|
||||||
latencyDist: '请求延迟分布',
|
|
||||||
providerSla: '上游供应商健康度 (SLA)',
|
|
||||||
errorDist: '错误类型分布',
|
|
||||||
systemStatus: '系统运行状态'
|
|
||||||
},
|
|
||||||
metrics: {
|
|
||||||
successRate: '成功率',
|
|
||||||
errorRate: '错误率',
|
|
||||||
p95: 'P95 延迟',
|
|
||||||
p99: 'P99 延迟',
|
|
||||||
http2Errors: 'HTTP/2 错误',
|
|
||||||
activeAlerts: '活跃告警',
|
|
||||||
cpuUsage: 'CPU 使用率',
|
|
||||||
queueDepth: '排队深度',
|
|
||||||
healthScore: '健康评分',
|
|
||||||
sla: '服务可用率 (SLA)',
|
|
||||||
qps: '实时 QPS',
|
|
||||||
tps: '实时 TPS',
|
|
||||||
errorCount: '周期错误数'
|
|
||||||
},
|
|
||||||
errors: {
|
|
||||||
title: '最近错误',
|
|
||||||
subtitle: '按平台与阶段定位失败原因',
|
|
||||||
count: '{n} 条错误'
|
|
||||||
},
|
|
||||||
filters: {
|
|
||||||
allSeverities: '全部级别',
|
|
||||||
allPlatforms: '全部平台',
|
|
||||||
allPhases: '全部阶段',
|
|
||||||
p0: 'P0(致命)',
|
|
||||||
p1: 'P1(高)',
|
|
||||||
p2: 'P2(中)',
|
|
||||||
p3: 'P3(低)'
|
|
||||||
},
|
|
||||||
searchPlaceholder: '按请求ID、模型或错误信息搜索',
|
|
||||||
range: {
|
|
||||||
'15m': '近 15 分钟',
|
|
||||||
'1h': '近 1 小时',
|
|
||||||
'24h': '近 24 小时',
|
|
||||||
'7d': '近 7 天'
|
|
||||||
},
|
|
||||||
platform: {
|
|
||||||
anthropic: 'Anthropic',
|
|
||||||
openai: 'OpenAI',
|
|
||||||
gemini: 'Gemini',
|
|
||||||
antigravity: 'Antigravity'
|
|
||||||
},
|
|
||||||
phase: {
|
|
||||||
auth: '认证',
|
|
||||||
concurrency: '并发',
|
|
||||||
billing: '计费',
|
|
||||||
scheduling: '调度',
|
|
||||||
network: '网络',
|
|
||||||
upstream: '上游',
|
|
||||||
response: '响应',
|
|
||||||
internal: '内部'
|
|
||||||
},
|
|
||||||
severity: {
|
|
||||||
p0: 'P0',
|
|
||||||
p1: 'P1',
|
|
||||||
p2: 'P2',
|
|
||||||
p3: 'P3'
|
|
||||||
},
|
|
||||||
table: {
|
|
||||||
time: '时间',
|
|
||||||
severity: '级别',
|
|
||||||
phase: '阶段',
|
|
||||||
statusCode: '状态码',
|
|
||||||
platform: '平台',
|
|
||||||
model: '模型',
|
|
||||||
latency: '延迟',
|
|
||||||
requestId: '请求ID',
|
|
||||||
message: '错误信息'
|
|
||||||
},
|
|
||||||
details: {
|
|
||||||
title: '错误详情',
|
|
||||||
requestId: '请求ID',
|
|
||||||
errorMessage: '错误信息',
|
|
||||||
requestPath: '请求路径',
|
|
||||||
clientIp: '客户端IP',
|
|
||||||
userId: '用户ID',
|
|
||||||
apiKeyId: 'API Key ID',
|
|
||||||
groupId: '分组ID',
|
|
||||||
stream: '流式'
|
|
||||||
},
|
|
||||||
empty: {
|
|
||||||
title: '暂无运维数据',
|
|
||||||
subtitle: '启用错误日志与指标采集后将展示在此处'
|
|
||||||
},
|
|
||||||
failedToLoad: '加载运维数据失败'
|
|
||||||
},
|
|
||||||
|
|
||||||
// Users Management
|
// Users Management
|
||||||
users: {
|
users: {
|
||||||
@@ -1156,6 +1059,54 @@ export default {
|
|||||||
error: '错误',
|
error: '错误',
|
||||||
cooldown: '冷却中'
|
cooldown: '冷却中'
|
||||||
},
|
},
|
||||||
|
status: {
|
||||||
|
paused: '已暂停',
|
||||||
|
limited: '受限',
|
||||||
|
tempUnschedulable: '临时不可调度'
|
||||||
|
},
|
||||||
|
tempUnschedulable: {
|
||||||
|
title: '临时不可调度',
|
||||||
|
statusTitle: '临时不可调度状态',
|
||||||
|
hint: '当错误码与关键词同时匹配时,账号会在指定时间内被临时禁用。',
|
||||||
|
notice: '规则按顺序匹配,需同时满足错误码与关键词。',
|
||||||
|
addRule: '添加规则',
|
||||||
|
ruleOrder: '规则序号',
|
||||||
|
ruleIndex: '规则 #{index}',
|
||||||
|
errorCode: '错误码',
|
||||||
|
errorCodePlaceholder: '例如 429',
|
||||||
|
durationMinutes: '持续时间(分钟)',
|
||||||
|
durationPlaceholder: '例如 30',
|
||||||
|
keywords: '关键词',
|
||||||
|
keywordsPlaceholder: '例如 overloaded, too many requests',
|
||||||
|
keywordsHint: '多个关键词用逗号分隔,匹配时必须命中其中之一。',
|
||||||
|
description: '描述',
|
||||||
|
descriptionPlaceholder: '可选,便于记忆规则用途',
|
||||||
|
rulesInvalid: '请至少填写一条包含错误码、关键词和时长的规则。',
|
||||||
|
viewDetails: '查看临时不可调度详情',
|
||||||
|
accountName: '账号',
|
||||||
|
triggeredAt: '触发时间',
|
||||||
|
until: '解除时间',
|
||||||
|
remaining: '剩余时间',
|
||||||
|
matchedKeyword: '匹配关键词',
|
||||||
|
errorMessage: '错误详情',
|
||||||
|
reset: '重置状态',
|
||||||
|
resetSuccess: '临时不可调度已重置',
|
||||||
|
resetFailed: '重置临时不可调度失败',
|
||||||
|
failedToLoad: '加载临时不可调度状态失败',
|
||||||
|
notActive: '当前账号未处于临时不可调度状态。',
|
||||||
|
expired: '已到期',
|
||||||
|
remainingMinutes: '约 {minutes} 分钟',
|
||||||
|
remainingHours: '约 {hours} 小时',
|
||||||
|
remainingHoursMinutes: '约 {hours} 小时 {minutes} 分钟',
|
||||||
|
presets: {
|
||||||
|
overloadLabel: '529 过载',
|
||||||
|
overloadDesc: '服务过载 - 暂停 60 分钟',
|
||||||
|
rateLimitLabel: '429 限流',
|
||||||
|
rateLimitDesc: '触发限流 - 暂停 10 分钟',
|
||||||
|
unavailableLabel: '503 维护',
|
||||||
|
unavailableDesc: '服务不可用 - 暂停 30 分钟'
|
||||||
|
}
|
||||||
|
},
|
||||||
usageWindow: {
|
usageWindow: {
|
||||||
statsTitle: '5小时窗口用量统计',
|
statsTitle: '5小时窗口用量统计',
|
||||||
statsTitleDaily: '每日用量统计',
|
statsTitleDaily: '每日用量统计',
|
||||||
@@ -1300,10 +1251,10 @@ export default {
|
|||||||
priority: '优先级',
|
priority: '优先级',
|
||||||
priorityHint: '优先级越高的账号优先使用',
|
priorityHint: '优先级越高的账号优先使用',
|
||||||
higherPriorityFirst: '数值越高优先级越高',
|
higherPriorityFirst: '数值越高优先级越高',
|
||||||
mixedScheduling: '混合调度',
|
mixedScheduling: '在 /v1/messages 中使用',
|
||||||
mixedSchedulingHint: '启用后可参与 Anthropic/Gemini 分组的调度',
|
mixedSchedulingHint: '启用后可参与 Anthropic/Gemini 分组的调度',
|
||||||
mixedSchedulingTooltip:
|
mixedSchedulingTooltip:
|
||||||
'开启后,该账户可被 /v1/messages 及 /v1beta 端点调度,否则只被 /antigravity 调度。注意:Anthropic Claude 和 Antigravity Claude 无法在同个上下文中混合使用,开启后请自行做好分组管理。',
|
'!!注意!! Antigravity Claude 和 Anthropic Claude 无法在同个上下文中使用,如果你同时有 Anthropic 账号和 Antigravity 账号,开启此选项会导致经常 400 报错。开启后,请用分组功能做好 Antigravity 账号和 Anthropic 账号的隔离。一定要弄明白再开启!!',
|
||||||
creating: '创建中...',
|
creating: '创建中...',
|
||||||
updating: '更新中...',
|
updating: '更新中...',
|
||||||
accountCreated: '账号创建成功',
|
accountCreated: '账号创建成功',
|
||||||
@@ -1860,6 +1811,7 @@ export default {
|
|||||||
siteKey: '站点密钥',
|
siteKey: '站点密钥',
|
||||||
secretKey: '私密密钥',
|
secretKey: '私密密钥',
|
||||||
siteKeyHint: '从 Cloudflare Dashboard 获取',
|
siteKeyHint: '从 Cloudflare Dashboard 获取',
|
||||||
|
cloudflareDashboard: 'Cloudflare Dashboard',
|
||||||
secretKeyHint: '服务端验证密钥(请保密)'
|
secretKeyHint: '服务端验证密钥(请保密)'
|
||||||
},
|
},
|
||||||
defaults: {
|
defaults: {
|
||||||
@@ -2007,6 +1959,7 @@ export default {
|
|||||||
description: '查看您的订阅计划和用量',
|
description: '查看您的订阅计划和用量',
|
||||||
noActiveSubscriptions: '暂无有效订阅',
|
noActiveSubscriptions: '暂无有效订阅',
|
||||||
noActiveSubscriptionsDesc: '您没有任何有效订阅。请联系管理员获取订阅。',
|
noActiveSubscriptionsDesc: '您没有任何有效订阅。请联系管理员获取订阅。',
|
||||||
|
failedToLoad: '加载订阅失败',
|
||||||
status: {
|
status: {
|
||||||
active: '有效',
|
active: '有效',
|
||||||
expired: '已过期',
|
expired: '已过期',
|
||||||
|
|||||||
@@ -163,18 +163,6 @@ const routes: RouteRecordRaw[] = [
|
|||||||
descriptionKey: 'admin.dashboard.description'
|
descriptionKey: 'admin.dashboard.description'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/admin/ops',
|
|
||||||
name: 'AdminOps',
|
|
||||||
component: () => import('@/views/admin/ops/OpsDashboard.vue'),
|
|
||||||
meta: {
|
|
||||||
requiresAuth: true,
|
|
||||||
requiresAdmin: true,
|
|
||||||
title: 'Ops Dashboard',
|
|
||||||
titleKey: 'admin.ops.title',
|
|
||||||
descriptionKey: 'admin.ops.description'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: '/admin/users',
|
path: '/admin/users',
|
||||||
name: 'AdminUsers',
|
name: 'AdminUsers',
|
||||||
|
|||||||
@@ -240,7 +240,7 @@
|
|||||||
href="https://dash.cloudflare.com/"
|
href="https://dash.cloudflare.com/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="text-primary-600 hover:text-primary-500"
|
class="text-primary-600 hover:text-primary-500"
|
||||||
>Cloudflare Dashboard</a
|
>{{ t('admin.settings.turnstile.cloudflareDashboard') }}</a
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -295,12 +295,12 @@ function onTurnstileVerify(token: string): void {
|
|||||||
|
|
||||||
function onTurnstileExpire(): void {
|
function onTurnstileExpire(): void {
|
||||||
turnstileToken.value = ''
|
turnstileToken.value = ''
|
||||||
errors.turnstile = 'Verification expired, please try again'
|
errors.turnstile = t('auth.turnstileExpired')
|
||||||
}
|
}
|
||||||
|
|
||||||
function onTurnstileError(): void {
|
function onTurnstileError(): void {
|
||||||
turnstileToken.value = ''
|
turnstileToken.value = ''
|
||||||
errors.turnstile = 'Verification failed, please try again'
|
errors.turnstile = t('auth.turnstileFailed')
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== Validation ====================
|
// ==================== Validation ====================
|
||||||
@@ -315,25 +315,25 @@ function validateForm(): boolean {
|
|||||||
|
|
||||||
// Email validation
|
// Email validation
|
||||||
if (!formData.email.trim()) {
|
if (!formData.email.trim()) {
|
||||||
errors.email = 'Email is required'
|
errors.email = t('auth.emailRequired')
|
||||||
isValid = false
|
isValid = false
|
||||||
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
|
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
|
||||||
errors.email = 'Please enter a valid email address'
|
errors.email = t('auth.invalidEmail')
|
||||||
isValid = false
|
isValid = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Password validation
|
// Password validation
|
||||||
if (!formData.password) {
|
if (!formData.password) {
|
||||||
errors.password = 'Password is required'
|
errors.password = t('auth.passwordRequired')
|
||||||
isValid = false
|
isValid = false
|
||||||
} else if (formData.password.length < 6) {
|
} else if (formData.password.length < 6) {
|
||||||
errors.password = 'Password must be at least 6 characters'
|
errors.password = t('auth.passwordMinLength')
|
||||||
isValid = false
|
isValid = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Turnstile validation
|
// Turnstile validation
|
||||||
if (turnstileEnabled.value && !turnstileToken.value) {
|
if (turnstileEnabled.value && !turnstileToken.value) {
|
||||||
errors.turnstile = 'Please complete the verification'
|
errors.turnstile = t('auth.completeVerification')
|
||||||
isValid = false
|
isValid = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -362,7 +362,7 @@ async function handleLogin(): Promise<void> {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Show success toast
|
// Show success toast
|
||||||
appStore.showSuccess('Login successful! Welcome back.')
|
appStore.showSuccess(t('auth.loginSuccess'))
|
||||||
|
|
||||||
// Redirect to dashboard or intended route
|
// Redirect to dashboard or intended route
|
||||||
const redirectTo = (router.currentRoute.value.query.redirect as string) || '/dashboard'
|
const redirectTo = (router.currentRoute.value.query.redirect as string) || '/dashboard'
|
||||||
@@ -382,7 +382,7 @@ async function handleLogin(): Promise<void> {
|
|||||||
} else if (err.message) {
|
} else if (err.message) {
|
||||||
errorMessage.value = err.message
|
errorMessage.value = err.message
|
||||||
} else {
|
} else {
|
||||||
errorMessage.value = 'Login failed. Please check your credentials and try again.'
|
errorMessage.value = t('auth.loginFailed')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also show error toast
|
// Also show error toast
|
||||||
|
|||||||
@@ -340,12 +340,12 @@ function onTurnstileVerify(token: string): void {
|
|||||||
|
|
||||||
function onTurnstileExpire(): void {
|
function onTurnstileExpire(): void {
|
||||||
turnstileToken.value = ''
|
turnstileToken.value = ''
|
||||||
errors.turnstile = 'Verification expired, please try again'
|
errors.turnstile = t('auth.turnstileExpired')
|
||||||
}
|
}
|
||||||
|
|
||||||
function onTurnstileError(): void {
|
function onTurnstileError(): void {
|
||||||
turnstileToken.value = ''
|
turnstileToken.value = ''
|
||||||
errors.turnstile = 'Verification failed, please try again'
|
errors.turnstile = t('auth.turnstileFailed')
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== Validation ====================
|
// ==================== Validation ====================
|
||||||
@@ -365,25 +365,25 @@ function validateForm(): boolean {
|
|||||||
|
|
||||||
// Email validation
|
// Email validation
|
||||||
if (!formData.email.trim()) {
|
if (!formData.email.trim()) {
|
||||||
errors.email = 'Email is required'
|
errors.email = t('auth.emailRequired')
|
||||||
isValid = false
|
isValid = false
|
||||||
} else if (!validateEmail(formData.email)) {
|
} else if (!validateEmail(formData.email)) {
|
||||||
errors.email = 'Please enter a valid email address'
|
errors.email = t('auth.invalidEmail')
|
||||||
isValid = false
|
isValid = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Password validation
|
// Password validation
|
||||||
if (!formData.password) {
|
if (!formData.password) {
|
||||||
errors.password = 'Password is required'
|
errors.password = t('auth.passwordRequired')
|
||||||
isValid = false
|
isValid = false
|
||||||
} else if (formData.password.length < 6) {
|
} else if (formData.password.length < 6) {
|
||||||
errors.password = 'Password must be at least 6 characters'
|
errors.password = t('auth.passwordMinLength')
|
||||||
isValid = false
|
isValid = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Turnstile validation
|
// Turnstile validation
|
||||||
if (turnstileEnabled.value && !turnstileToken.value) {
|
if (turnstileEnabled.value && !turnstileToken.value) {
|
||||||
errors.turnstile = 'Please complete the verification'
|
errors.turnstile = t('auth.completeVerification')
|
||||||
isValid = false
|
isValid = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -429,7 +429,7 @@ async function handleRegister(): Promise<void> {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Show success toast
|
// Show success toast
|
||||||
appStore.showSuccess('Account created successfully! Welcome to ' + siteName.value + '.')
|
appStore.showSuccess(t('auth.accountCreatedSuccess', { siteName: siteName.value }))
|
||||||
|
|
||||||
// Redirect to dashboard
|
// Redirect to dashboard
|
||||||
await router.push('/dashboard')
|
await router.push('/dashboard')
|
||||||
@@ -448,7 +448,7 @@ async function handleRegister(): Promise<void> {
|
|||||||
} else if (err.message) {
|
} else if (err.message) {
|
||||||
errorMessage.value = err.message
|
errorMessage.value = err.message
|
||||||
} else {
|
} else {
|
||||||
errorMessage.value = 'Registration failed. Please try again.'
|
errorMessage.value = t('auth.registrationFailed')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also show error toast
|
// Also show error toast
|
||||||
|
|||||||
@@ -173,7 +173,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<!-- Import to CC Switch Button -->
|
<!-- Import to CC Switch Button -->
|
||||||
<button
|
<button
|
||||||
@click="importToCcswitch(row.key)"
|
@click="importToCcswitch(row)"
|
||||||
class="flex flex-col items-center gap-0.5 rounded-lg p-1.5 text-gray-500 transition-colors hover:bg-blue-50 hover:text-blue-600 dark:hover:bg-blue-900/20 dark:hover:text-blue-400"
|
class="flex flex-col items-center gap-0.5 rounded-lg p-1.5 text-gray-500 transition-colors hover:bg-blue-50 hover:text-blue-600 dark:hover:bg-blue-900/20 dark:hover:text-blue-400"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
@@ -453,6 +453,49 @@
|
|||||||
@close="closeUseKeyModal"
|
@close="closeUseKeyModal"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- CCS Client Selection Dialog for Antigravity -->
|
||||||
|
<BaseDialog
|
||||||
|
:show="showCcsClientSelect"
|
||||||
|
:title="t('keys.ccsClientSelect.title')"
|
||||||
|
width="narrow"
|
||||||
|
@close="closeCcsClientSelect"
|
||||||
|
>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
{{ t('keys.ccsClientSelect.description') }}
|
||||||
|
</p>
|
||||||
|
<div class="grid grid-cols-2 gap-3">
|
||||||
|
<button
|
||||||
|
@click="handleCcsClientSelect('claude')"
|
||||||
|
class="flex flex-col items-center gap-2 p-4 rounded-xl border-2 border-gray-200 dark:border-dark-600 hover:border-primary-500 dark:hover:border-primary-500 hover:bg-primary-50 dark:hover:bg-primary-900/20 transition-all"
|
||||||
|
>
|
||||||
|
<svg class="w-8 h-8 text-gray-600 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="m6.75 7.5 3 2.25-3 2.25m4.5 0h3m-9 8.25h13.5A2.25 2.25 0 0 0 21 17.25V6.75A2.25 2.25 0 0 0 18.75 4.5H5.25A2.25 2.25 0 0 0 3 6.75v10.5A2.25 2.25 0 0 0 5.25 20.25Z" />
|
||||||
|
</svg>
|
||||||
|
<span class="font-medium text-gray-900 dark:text-white">{{ t('keys.ccsClientSelect.claudeCode') }}</span>
|
||||||
|
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('keys.ccsClientSelect.claudeCodeDesc') }}</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="handleCcsClientSelect('gemini')"
|
||||||
|
class="flex flex-col items-center gap-2 p-4 rounded-xl border-2 border-gray-200 dark:border-dark-600 hover:border-primary-500 dark:hover:border-primary-500 hover:bg-primary-50 dark:hover:bg-primary-900/20 transition-all"
|
||||||
|
>
|
||||||
|
<svg class="w-8 h-8 text-gray-600 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M9.813 15.904 9 18.75l-.813-2.846a4.5 4.5 0 0 0-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 0 0 3.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 0 0 3.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 0 0-3.09 3.09ZM18.259 8.715 18 9.75l-.259-1.035a3.375 3.375 0 0 0-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 0 0 2.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 0 0 2.456 2.456L21.75 6l-1.035.259a3.375 3.375 0 0 0-2.456 2.456ZM16.894 20.567 16.5 21.75l-.394-1.183a2.25 2.25 0 0 0-1.423-1.423L13.5 18.75l1.183-.394a2.25 2.25 0 0 0 1.423-1.423l.394-1.183.394 1.183a2.25 2.25 0 0 0 1.423 1.423l1.183.394-1.183.394a2.25 2.25 0 0 0-1.423 1.423Z" />
|
||||||
|
</svg>
|
||||||
|
<span class="font-medium text-gray-900 dark:text-white">{{ t('keys.ccsClientSelect.geminiCli') }}</span>
|
||||||
|
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('keys.ccsClientSelect.geminiCliDesc') }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<button @click="closeCcsClientSelect" class="btn btn-secondary">
|
||||||
|
{{ t('common.cancel') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</BaseDialog>
|
||||||
|
|
||||||
<!-- Group Selector Dropdown (Teleported to body to avoid overflow clipping) -->
|
<!-- Group Selector Dropdown (Teleported to body to avoid overflow clipping) -->
|
||||||
<Teleport to="body">
|
<Teleport to="body">
|
||||||
<div
|
<div
|
||||||
@@ -563,6 +606,8 @@ const showCreateModal = ref(false)
|
|||||||
const showEditModal = ref(false)
|
const showEditModal = ref(false)
|
||||||
const showDeleteDialog = ref(false)
|
const showDeleteDialog = ref(false)
|
||||||
const showUseKeyModal = ref(false)
|
const showUseKeyModal = ref(false)
|
||||||
|
const showCcsClientSelect = ref(false)
|
||||||
|
const pendingCcsRow = ref<ApiKey | null>(null)
|
||||||
const selectedKey = ref<ApiKey | null>(null)
|
const selectedKey = ref<ApiKey | null>(null)
|
||||||
const copiedKeyId = ref<number | null>(null)
|
const copiedKeyId = ref<number | null>(null)
|
||||||
const groupSelectorKeyId = ref<number | null>(null)
|
const groupSelectorKeyId = ref<number | null>(null)
|
||||||
@@ -871,8 +916,48 @@ const closeModals = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const importToCcswitch = (apiKey: string) => {
|
const importToCcswitch = (row: ApiKey) => {
|
||||||
|
const platform = row.group?.platform || 'anthropic'
|
||||||
|
|
||||||
|
// For antigravity platform, show client selection dialog
|
||||||
|
if (platform === 'antigravity') {
|
||||||
|
pendingCcsRow.value = row
|
||||||
|
showCcsClientSelect.value = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// For other platforms, execute directly
|
||||||
|
executeCcsImport(row, platform === 'gemini' ? 'gemini' : 'claude')
|
||||||
|
}
|
||||||
|
|
||||||
|
const executeCcsImport = (row: ApiKey, clientType: 'claude' | 'gemini') => {
|
||||||
const baseUrl = publicSettings.value?.api_base_url || window.location.origin
|
const baseUrl = publicSettings.value?.api_base_url || window.location.origin
|
||||||
|
const platform = row.group?.platform || 'anthropic'
|
||||||
|
|
||||||
|
// Determine app name and endpoint based on platform and client type
|
||||||
|
let app: string
|
||||||
|
let endpoint: string
|
||||||
|
|
||||||
|
if (platform === 'antigravity') {
|
||||||
|
// Antigravity always uses /antigravity suffix
|
||||||
|
app = clientType === 'gemini' ? 'gemini' : 'claude'
|
||||||
|
endpoint = `${baseUrl}/antigravity`
|
||||||
|
} else {
|
||||||
|
switch (platform) {
|
||||||
|
case 'openai':
|
||||||
|
app = 'codex'
|
||||||
|
endpoint = baseUrl
|
||||||
|
break
|
||||||
|
case 'gemini':
|
||||||
|
app = 'gemini'
|
||||||
|
endpoint = baseUrl
|
||||||
|
break
|
||||||
|
default: // anthropic
|
||||||
|
app = 'claude'
|
||||||
|
endpoint = baseUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const usageScript = `({
|
const usageScript = `({
|
||||||
request: {
|
request: {
|
||||||
url: "{{baseUrl}}/v1/usage",
|
url: "{{baseUrl}}/v1/usage",
|
||||||
@@ -889,11 +974,11 @@ const importToCcswitch = (apiKey: string) => {
|
|||||||
})`
|
})`
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
resource: 'provider',
|
resource: 'provider',
|
||||||
app: 'claude',
|
app: app,
|
||||||
name: 'sub2api',
|
name: 'sub2api',
|
||||||
homepage: baseUrl,
|
homepage: baseUrl,
|
||||||
endpoint: baseUrl,
|
endpoint: endpoint,
|
||||||
apiKey: apiKey,
|
apiKey: row.key,
|
||||||
configFormat: 'json',
|
configFormat: 'json',
|
||||||
usageEnabled: 'true',
|
usageEnabled: 'true',
|
||||||
usageScript: btoa(usageScript),
|
usageScript: btoa(usageScript),
|
||||||
@@ -916,6 +1001,19 @@ const importToCcswitch = (apiKey: string) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleCcsClientSelect = (clientType: 'claude' | 'gemini') => {
|
||||||
|
if (pendingCcsRow.value) {
|
||||||
|
executeCcsImport(pendingCcsRow.value, clientType)
|
||||||
|
}
|
||||||
|
showCcsClientSelect.value = false
|
||||||
|
pendingCcsRow.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeCcsClientSelect = () => {
|
||||||
|
showCcsClientSelect.value = false
|
||||||
|
pendingCcsRow.value = null
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadApiKeys()
|
loadApiKeys()
|
||||||
loadGroups()
|
loadGroups()
|
||||||
|
|||||||
@@ -500,7 +500,7 @@ const getHistoryItemTitle = (item: RedeemHistoryItem) => {
|
|||||||
} else if (item.type === 'subscription') {
|
} else if (item.type === 'subscription') {
|
||||||
return t('redeem.subscriptionAssigned')
|
return t('redeem.subscriptionAssigned')
|
||||||
}
|
}
|
||||||
return 'Unknown'
|
return t('common.unknown')
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatHistoryValue = (item: RedeemHistoryItem) => {
|
const formatHistoryValue = (item: RedeemHistoryItem) => {
|
||||||
|
|||||||
@@ -279,7 +279,7 @@ async function loadSubscriptions() {
|
|||||||
subscriptions.value = await subscriptionsAPI.getMySubscriptions()
|
subscriptions.value = await subscriptionsAPI.getMySubscriptions()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load subscriptions:', error)
|
console.error('Failed to load subscriptions:', error)
|
||||||
appStore.showError('Failed to load subscriptions')
|
appStore.showError(t('userSubscriptions.failedToLoad'))
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user