feat(frontend): 增强用户界面和使用教程
主要改进: - 扩展 UseKeyModal 支持 Antigravity/Gemini 平台教程 - 添加 CCS (Claude Code Settings) 导入说明 - 添加混合渠道风险警告提示 - 优化登录/注册页面样式 - 更新 Antigravity 混合调度选项文案 - 完善中英文国际化文案
This commit is contained in:
@@ -240,7 +240,7 @@
|
||||
href="https://dash.cloudflare.com/"
|
||||
target="_blank"
|
||||
class="text-primary-600 hover:text-primary-500"
|
||||
>Cloudflare Dashboard</a
|
||||
>{{ t('admin.settings.turnstile.cloudflareDashboard') }}</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -295,12 +295,12 @@ function onTurnstileVerify(token: string): void {
|
||||
|
||||
function onTurnstileExpire(): void {
|
||||
turnstileToken.value = ''
|
||||
errors.turnstile = 'Verification expired, please try again'
|
||||
errors.turnstile = t('auth.turnstileExpired')
|
||||
}
|
||||
|
||||
function onTurnstileError(): void {
|
||||
turnstileToken.value = ''
|
||||
errors.turnstile = 'Verification failed, please try again'
|
||||
errors.turnstile = t('auth.turnstileFailed')
|
||||
}
|
||||
|
||||
// ==================== Validation ====================
|
||||
@@ -315,25 +315,25 @@ function validateForm(): boolean {
|
||||
|
||||
// Email validation
|
||||
if (!formData.email.trim()) {
|
||||
errors.email = 'Email is required'
|
||||
errors.email = t('auth.emailRequired')
|
||||
isValid = false
|
||||
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
|
||||
errors.email = 'Please enter a valid email address'
|
||||
errors.email = t('auth.invalidEmail')
|
||||
isValid = false
|
||||
}
|
||||
|
||||
// Password validation
|
||||
if (!formData.password) {
|
||||
errors.password = 'Password is required'
|
||||
errors.password = t('auth.passwordRequired')
|
||||
isValid = false
|
||||
} else if (formData.password.length < 6) {
|
||||
errors.password = 'Password must be at least 6 characters'
|
||||
errors.password = t('auth.passwordMinLength')
|
||||
isValid = false
|
||||
}
|
||||
|
||||
// Turnstile validation
|
||||
if (turnstileEnabled.value && !turnstileToken.value) {
|
||||
errors.turnstile = 'Please complete the verification'
|
||||
errors.turnstile = t('auth.completeVerification')
|
||||
isValid = false
|
||||
}
|
||||
|
||||
@@ -362,7 +362,7 @@ async function handleLogin(): Promise<void> {
|
||||
})
|
||||
|
||||
// Show success toast
|
||||
appStore.showSuccess('Login successful! Welcome back.')
|
||||
appStore.showSuccess(t('auth.loginSuccess'))
|
||||
|
||||
// Redirect to dashboard or intended route
|
||||
const redirectTo = (router.currentRoute.value.query.redirect as string) || '/dashboard'
|
||||
@@ -382,7 +382,7 @@ async function handleLogin(): Promise<void> {
|
||||
} else if (err.message) {
|
||||
errorMessage.value = err.message
|
||||
} else {
|
||||
errorMessage.value = 'Login failed. Please check your credentials and try again.'
|
||||
errorMessage.value = t('auth.loginFailed')
|
||||
}
|
||||
|
||||
// Also show error toast
|
||||
|
||||
@@ -340,12 +340,12 @@ function onTurnstileVerify(token: string): void {
|
||||
|
||||
function onTurnstileExpire(): void {
|
||||
turnstileToken.value = ''
|
||||
errors.turnstile = 'Verification expired, please try again'
|
||||
errors.turnstile = t('auth.turnstileExpired')
|
||||
}
|
||||
|
||||
function onTurnstileError(): void {
|
||||
turnstileToken.value = ''
|
||||
errors.turnstile = 'Verification failed, please try again'
|
||||
errors.turnstile = t('auth.turnstileFailed')
|
||||
}
|
||||
|
||||
// ==================== Validation ====================
|
||||
@@ -365,25 +365,25 @@ function validateForm(): boolean {
|
||||
|
||||
// Email validation
|
||||
if (!formData.email.trim()) {
|
||||
errors.email = 'Email is required'
|
||||
errors.email = t('auth.emailRequired')
|
||||
isValid = false
|
||||
} else if (!validateEmail(formData.email)) {
|
||||
errors.email = 'Please enter a valid email address'
|
||||
errors.email = t('auth.invalidEmail')
|
||||
isValid = false
|
||||
}
|
||||
|
||||
// Password validation
|
||||
if (!formData.password) {
|
||||
errors.password = 'Password is required'
|
||||
errors.password = t('auth.passwordRequired')
|
||||
isValid = false
|
||||
} else if (formData.password.length < 6) {
|
||||
errors.password = 'Password must be at least 6 characters'
|
||||
errors.password = t('auth.passwordMinLength')
|
||||
isValid = false
|
||||
}
|
||||
|
||||
// Turnstile validation
|
||||
if (turnstileEnabled.value && !turnstileToken.value) {
|
||||
errors.turnstile = 'Please complete the verification'
|
||||
errors.turnstile = t('auth.completeVerification')
|
||||
isValid = false
|
||||
}
|
||||
|
||||
@@ -429,7 +429,7 @@ async function handleRegister(): Promise<void> {
|
||||
})
|
||||
|
||||
// Show success toast
|
||||
appStore.showSuccess('Account created successfully! Welcome to ' + siteName.value + '.')
|
||||
appStore.showSuccess(t('auth.accountCreatedSuccess', { siteName: siteName.value }))
|
||||
|
||||
// Redirect to dashboard
|
||||
await router.push('/dashboard')
|
||||
@@ -448,7 +448,7 @@ async function handleRegister(): Promise<void> {
|
||||
} else if (err.message) {
|
||||
errorMessage.value = err.message
|
||||
} else {
|
||||
errorMessage.value = 'Registration failed. Please try again.'
|
||||
errorMessage.value = t('auth.registrationFailed')
|
||||
}
|
||||
|
||||
// Also show error toast
|
||||
|
||||
@@ -173,7 +173,7 @@
|
||||
</button>
|
||||
<!-- Import to CC Switch 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"
|
||||
>
|
||||
<svg
|
||||
@@ -453,6 +453,49 @@
|
||||
@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) -->
|
||||
<Teleport to="body">
|
||||
<div
|
||||
@@ -563,6 +606,8 @@ const showCreateModal = ref(false)
|
||||
const showEditModal = ref(false)
|
||||
const showDeleteDialog = ref(false)
|
||||
const showUseKeyModal = ref(false)
|
||||
const showCcsClientSelect = ref(false)
|
||||
const pendingCcsRow = ref<ApiKey | null>(null)
|
||||
const selectedKey = ref<ApiKey | null>(null)
|
||||
const copiedKeyId = 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 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 = `({
|
||||
request: {
|
||||
url: "{{baseUrl}}/v1/usage",
|
||||
@@ -889,11 +974,11 @@ const importToCcswitch = (apiKey: string) => {
|
||||
})`
|
||||
const params = new URLSearchParams({
|
||||
resource: 'provider',
|
||||
app: 'claude',
|
||||
app: app,
|
||||
name: 'sub2api',
|
||||
homepage: baseUrl,
|
||||
endpoint: baseUrl,
|
||||
apiKey: apiKey,
|
||||
endpoint: endpoint,
|
||||
apiKey: row.key,
|
||||
configFormat: 'json',
|
||||
usageEnabled: 'true',
|
||||
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(() => {
|
||||
loadApiKeys()
|
||||
loadGroups()
|
||||
|
||||
@@ -500,7 +500,7 @@ const getHistoryItemTitle = (item: RedeemHistoryItem) => {
|
||||
} else if (item.type === 'subscription') {
|
||||
return t('redeem.subscriptionAssigned')
|
||||
}
|
||||
return 'Unknown'
|
||||
return t('common.unknown')
|
||||
}
|
||||
|
||||
const formatHistoryValue = (item: RedeemHistoryItem) => {
|
||||
|
||||
@@ -279,7 +279,7 @@ async function loadSubscriptions() {
|
||||
subscriptions.value = await subscriptionsAPI.getMySubscriptions()
|
||||
} catch (error) {
|
||||
console.error('Failed to load subscriptions:', error)
|
||||
appStore.showError('Failed to load subscriptions')
|
||||
appStore.showError(t('userSubscriptions.failedToLoad'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user