mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-08 10:25:28 +08:00
feat: add i18n language toggle and bilingual docs
This commit is contained in:
@@ -11,8 +11,10 @@ import {
|
||||
Check
|
||||
} from 'lucide-react'
|
||||
import clsx from 'clsx'
|
||||
import { useI18n } from '../i18n'
|
||||
|
||||
export default function AccountManager({ config, onRefresh, onMessage, authFetch }) {
|
||||
const { t } = useI18n()
|
||||
const [showAddKey, setShowAddKey] = useState(false)
|
||||
const [showAddAccount, setShowAddAccount] = useState(false)
|
||||
const [newKey, setNewKey] = useState('')
|
||||
@@ -54,39 +56,39 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch
|
||||
body: JSON.stringify({ key: newKey.trim() }),
|
||||
})
|
||||
if (res.ok) {
|
||||
onMessage('success', 'API 密钥添加成功')
|
||||
onMessage('success', t('accountManager.addKeySuccess'))
|
||||
setNewKey('')
|
||||
setShowAddKey(false)
|
||||
onRefresh()
|
||||
} else {
|
||||
const data = await res.json()
|
||||
onMessage('error', data.detail || 'Failed to add')
|
||||
onMessage('error', data.detail || t('messages.failedToAdd'))
|
||||
}
|
||||
} catch (e) {
|
||||
onMessage('error', '网络错误')
|
||||
onMessage('error', t('messages.networkError'))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const deleteKey = async (key) => {
|
||||
if (!confirm('确定要删除此 API 密钥吗?')) return
|
||||
if (!confirm(t('accountManager.deleteKeyConfirm'))) return
|
||||
try {
|
||||
const res = await apiFetch(`/admin/keys/${encodeURIComponent(key)}`, { method: 'DELETE' })
|
||||
if (res.ok) {
|
||||
onMessage('success', 'Deleted successfully')
|
||||
onMessage('success', t('messages.deleted'))
|
||||
onRefresh()
|
||||
} else {
|
||||
onMessage('error', 'Delete failed')
|
||||
onMessage('error', t('messages.deleteFailed'))
|
||||
}
|
||||
} catch (e) {
|
||||
onMessage('error', 'Network error')
|
||||
onMessage('error', t('messages.networkError'))
|
||||
}
|
||||
}
|
||||
|
||||
const addAccount = async () => {
|
||||
if (!newAccount.password || (!newAccount.email && !newAccount.mobile)) {
|
||||
onMessage('error', 'Password and Email/Mobile are required')
|
||||
onMessage('error', t('accountManager.requiredFields'))
|
||||
return
|
||||
}
|
||||
setLoading(true)
|
||||
@@ -97,33 +99,33 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch
|
||||
body: JSON.stringify(newAccount),
|
||||
})
|
||||
if (res.ok) {
|
||||
onMessage('success', '账号添加成功')
|
||||
onMessage('success', t('accountManager.addAccountSuccess'))
|
||||
setNewAccount({ email: '', mobile: '', password: '' })
|
||||
setShowAddAccount(false)
|
||||
onRefresh()
|
||||
} else {
|
||||
const data = await res.json()
|
||||
onMessage('error', data.detail || 'Failed to add')
|
||||
onMessage('error', data.detail || t('messages.failedToAdd'))
|
||||
}
|
||||
} catch (e) {
|
||||
onMessage('error', '网络错误')
|
||||
onMessage('error', t('messages.networkError'))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const deleteAccount = async (id) => {
|
||||
if (!confirm('确定要删除此账号吗?')) return
|
||||
if (!confirm(t('accountManager.deleteAccountConfirm'))) return
|
||||
try {
|
||||
const res = await apiFetch(`/admin/accounts/${encodeURIComponent(id)}`, { method: 'DELETE' })
|
||||
if (res.ok) {
|
||||
onMessage('success', 'Deleted successfully')
|
||||
onMessage('success', t('messages.deleted'))
|
||||
onRefresh()
|
||||
} else {
|
||||
onMessage('error', 'Delete failed')
|
||||
onMessage('error', t('messages.deleteFailed'))
|
||||
}
|
||||
} catch (e) {
|
||||
onMessage('error', 'Network error')
|
||||
onMessage('error', t('messages.networkError'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,17 +138,20 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch
|
||||
body: JSON.stringify({ identifier }),
|
||||
})
|
||||
const data = await res.json()
|
||||
onMessage(data.success ? 'success' : 'error', `${identifier}: ${data.success ? `Success (${data.response_time}ms)` : data.message}`)
|
||||
const statusMessage = data.success
|
||||
? t('apiTester.testSuccess', { account: identifier, time: data.response_time })
|
||||
: `${identifier}: ${data.message}`
|
||||
onMessage(data.success ? 'success' : 'error', statusMessage)
|
||||
onRefresh()
|
||||
} catch (e) {
|
||||
onMessage('error', 'Test failed: ' + e.message)
|
||||
onMessage('error', t('accountManager.testFailed', { error: e.message }))
|
||||
} finally {
|
||||
setTesting(prev => ({ ...prev, [identifier]: false }))
|
||||
}
|
||||
}
|
||||
|
||||
const testAllAccounts = async () => {
|
||||
if (!confirm('测试所有账号的 API 连通性?')) return
|
||||
if (!confirm(t('accountManager.testAllConfirm'))) return
|
||||
const accounts = config.accounts || []
|
||||
if (accounts.length === 0) return
|
||||
|
||||
@@ -176,7 +181,7 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch
|
||||
setBatchProgress({ current: i + 1, total: accounts.length, results: [...results] })
|
||||
}
|
||||
|
||||
onMessage('success', `Completed: ${successCount}/${accounts.length} available`)
|
||||
onMessage('success', t('accountManager.testAllCompleted', { success: successCount, total: accounts.length }))
|
||||
onRefresh()
|
||||
setTestingAll(false)
|
||||
}
|
||||
@@ -191,30 +196,30 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch
|
||||
<div className="absolute right-0 top-0 p-4 opacity-5 group-hover:opacity-10 transition-opacity">
|
||||
<CheckCircle2 className="w-16 h-16" />
|
||||
</div>
|
||||
<p className="text-xs font-medium text-muted-foreground uppercase tracking-widest">可用</p>
|
||||
<p className="text-xs font-medium text-muted-foreground uppercase tracking-widest">{t('accountManager.available')}</p>
|
||||
<div className="mt-2 flex items-baseline gap-2">
|
||||
<span className="text-3xl font-bold text-foreground">{queueStatus.available}</span>
|
||||
<span className="text-xs text-muted-foreground">个账号</span>
|
||||
<span className="text-xs text-muted-foreground">{t('accountManager.accountsUnit')}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-card border border-border rounded-xl p-4 flex flex-col justify-between shadow-sm relative overflow-hidden group">
|
||||
<div className="absolute right-0 top-0 p-4 opacity-5 group-hover:opacity-10 transition-opacity">
|
||||
<Server className="w-16 h-16" />
|
||||
</div>
|
||||
<p className="text-xs font-medium text-muted-foreground uppercase tracking-widest">正在使用</p>
|
||||
<p className="text-xs font-medium text-muted-foreground uppercase tracking-widest">{t('accountManager.inUse')}</p>
|
||||
<div className="mt-2 flex items-baseline gap-2">
|
||||
<span className="text-3xl font-bold text-foreground">{queueStatus.in_use}</span>
|
||||
<span className="text-xs text-muted-foreground">线程</span>
|
||||
<span className="text-xs text-muted-foreground">{t('accountManager.threadsUnit')}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-card border border-border rounded-xl p-4 flex flex-col justify-between shadow-sm relative overflow-hidden group">
|
||||
<div className="absolute right-0 top-0 p-4 opacity-5 group-hover:opacity-10 transition-opacity">
|
||||
<ShieldCheck className="w-16 h-16" />
|
||||
</div>
|
||||
<p className="text-xs font-medium text-muted-foreground uppercase tracking-widest">账号池总数</p>
|
||||
<p className="text-xs font-medium text-muted-foreground uppercase tracking-widest">{t('accountManager.totalPool')}</p>
|
||||
<div className="mt-2 flex items-baseline gap-2">
|
||||
<span className="text-3xl font-bold text-foreground">{queueStatus.total}</span>
|
||||
<span className="text-xs text-muted-foreground">个账号</span>
|
||||
<span className="text-xs text-muted-foreground">{t('accountManager.accountsUnit')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -225,15 +230,15 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch
|
||||
<div className="bg-card border border-border rounded-xl overflow-hidden shadow-sm">
|
||||
<div className="p-6 border-b border-border flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold">API 密钥</h2>
|
||||
<p className="text-sm text-muted-foreground">管理 API 访问密钥池</p>
|
||||
<h2 className="text-lg font-semibold">{t('accountManager.apiKeysTitle')}</h2>
|
||||
<p className="text-sm text-muted-foreground">{t('accountManager.apiKeysDesc')}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowAddKey(true)}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors font-medium text-sm shadow-sm"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
添加密钥
|
||||
{t('accountManager.addKey')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -246,7 +251,7 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch
|
||||
{key.slice(0, 16)}****
|
||||
</div>
|
||||
{copiedKey === key && (
|
||||
<span className="text-xs text-green-500 animate-pulse">已复制</span>
|
||||
<span className="text-xs text-green-500 animate-pulse">{t('accountManager.copied')}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
@@ -257,14 +262,14 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch
|
||||
setTimeout(() => setCopiedKey(null), 2000)
|
||||
}}
|
||||
className="p-2 text-muted-foreground hover:text-primary hover:bg-primary/10 rounded-md transition-colors opacity-0 group-hover:opacity-100"
|
||||
title="复制密钥"
|
||||
title={t('accountManager.copyKeyTitle')}
|
||||
>
|
||||
{copiedKey === key ? <Check className="w-4 h-4 text-green-500" /> : <Copy className="w-4 h-4" />}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => deleteKey(key)}
|
||||
className="p-2 text-muted-foreground hover:text-destructive hover:bg-destructive/10 rounded-md transition-colors opacity-0 group-hover:opacity-100"
|
||||
title="删除密钥"
|
||||
title={t('accountManager.deleteKeyTitle')}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
@@ -272,7 +277,7 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="p-8 text-center text-muted-foreground">未找到 API 密钥</div>
|
||||
<div className="p-8 text-center text-muted-foreground">{t('accountManager.noApiKeys')}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -281,8 +286,8 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch
|
||||
<div className="bg-card border border-border rounded-xl overflow-hidden shadow-sm">
|
||||
<div className="p-6 border-b border-border flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold">DeepSeek 账号</h2>
|
||||
<p className="text-sm text-muted-foreground">管理 DeepSeek 账号池</p>
|
||||
<h2 className="text-lg font-semibold">{t('accountManager.accountsTitle')}</h2>
|
||||
<p className="text-sm text-muted-foreground">{t('accountManager.accountsDesc')}</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button
|
||||
@@ -291,14 +296,14 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch
|
||||
className="flex items-center px-3 py-2 bg-secondary text-secondary-foreground rounded-lg hover:bg-secondary/80 transition-colors text-xs font-medium border border-border disabled:opacity-50"
|
||||
>
|
||||
{testingAll ? <span className="animate-spin mr-2">⟳</span> : <Play className="w-3 h-3 mr-2" />}
|
||||
测试全部
|
||||
{t('accountManager.testAll')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowAddAccount(true)}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors font-medium text-sm shadow-sm"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
添加账号
|
||||
{t('accountManager.addAccount')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -307,7 +312,7 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch
|
||||
{testingAll && batchProgress.total > 0 && (
|
||||
<div className="p-4 border-b border-border bg-muted/30">
|
||||
<div className="flex items-center justify-between text-sm mb-2">
|
||||
<span className="font-medium">正在测试所有账号...</span>
|
||||
<span className="font-medium">{t('accountManager.testingAllAccounts')}</span>
|
||||
<span className="text-muted-foreground">{batchProgress.current} / {batchProgress.total}</span>
|
||||
</div>
|
||||
<div className="w-full bg-muted rounded-full h-2 overflow-hidden mb-4">
|
||||
@@ -345,7 +350,7 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch
|
||||
<div className="min-w-0">
|
||||
<div className="font-medium truncate">{id}</div>
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground mt-0.5">
|
||||
<span>{acc.has_token ? '已建立会话' : '需重新登录'}</span>
|
||||
<span>{acc.has_token ? t('accountManager.sessionActive') : t('accountManager.reauthRequired')}</span>
|
||||
{acc.token_preview && (
|
||||
<span className="font-mono bg-muted px-1.5 py-0.5 rounded text-[10px]">
|
||||
{acc.token_preview}
|
||||
@@ -360,7 +365,7 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch
|
||||
disabled={testing[id]}
|
||||
className="px-2 lg:px-3 py-1 lg:py-1.5 text-[10px] lg:text-xs font-medium border border-border rounded-md hover:bg-secondary transition-colors disabled:opacity-50"
|
||||
>
|
||||
{testing[id] ? '正在测试...' : '测试'}
|
||||
{testing[id] ? t('actions.testing') : t('actions.test')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => deleteAccount(id)}
|
||||
@@ -373,7 +378,7 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<div className="p-8 text-center text-muted-foreground">未找到任何账号</div>
|
||||
<div className="p-8 text-center text-muted-foreground">{t('accountManager.noAccounts')}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -384,19 +389,19 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4 animate-in fade-in">
|
||||
<div className="bg-card w-full max-w-md rounded-xl border border-border shadow-2xl overflow-hidden animate-in zoom-in-95">
|
||||
<div className="p-4 border-b border-border flex justify-between items-center">
|
||||
<h3 className="font-semibold">添加 API 密钥</h3>
|
||||
<h3 className="font-semibold">{t('accountManager.modalAddKeyTitle')}</h3>
|
||||
<button onClick={() => setShowAddKey(false)} className="text-muted-foreground hover:text-foreground">
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-6 space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1.5">新密钥值</label>
|
||||
<label className="block text-sm font-medium mb-1.5">{t('accountManager.newKeyLabel')}</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
className="input-field bg-[#09090b] flex-1"
|
||||
placeholder="输入自定义 API 密钥"
|
||||
placeholder={t('accountManager.newKeyPlaceholder')}
|
||||
value={newKey}
|
||||
onChange={e => setNewKey(e.target.value)}
|
||||
autoFocus
|
||||
@@ -406,15 +411,15 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch
|
||||
onClick={() => setNewKey('sk-' + crypto.randomUUID().replace(/-/g, ''))}
|
||||
className="px-3 py-2 bg-secondary text-secondary-foreground rounded-lg hover:bg-secondary/80 transition-colors text-sm font-medium border border-border whitespace-nowrap"
|
||||
>
|
||||
生成
|
||||
{t('accountManager.generate')}
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-1.5">点击「生成」自动创建随机密钥</p>
|
||||
<p className="text-xs text-muted-foreground mt-1.5">{t('accountManager.generateHint')}</p>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2 pt-2">
|
||||
<button onClick={() => setShowAddKey(false)} className="px-4 py-2 rounded-lg border border-border hover:bg-secondary transition-colors text-sm font-medium">取消</button>
|
||||
<button onClick={() => setShowAddKey(false)} className="px-4 py-2 rounded-lg border border-border hover:bg-secondary transition-colors text-sm font-medium">{t('actions.cancel')}</button>
|
||||
<button onClick={addKey} disabled={loading} className="px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors text-sm font-medium disabled:opacity-50">
|
||||
{loading ? '添加中...' : '添加密钥'}
|
||||
{loading ? t('accountManager.addKeyLoading') : t('accountManager.addKeyAction')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -428,14 +433,14 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4 animate-in fade-in">
|
||||
<div className="bg-card w-full max-w-md rounded-xl border border-border shadow-2xl overflow-hidden animate-in zoom-in-95">
|
||||
<div className="p-4 border-b border-border flex justify-between items-center">
|
||||
<h3 className="font-semibold">添加 DeepSeek 账号</h3>
|
||||
<h3 className="font-semibold">{t('accountManager.modalAddAccountTitle')}</h3>
|
||||
<button onClick={() => setShowAddAccount(false)} className="text-muted-foreground hover:text-foreground">
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-6 space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1.5">邮箱 (可选)</label>
|
||||
<label className="block text-sm font-medium mb-1.5">{t('accountManager.emailOptional')}</label>
|
||||
<input
|
||||
type="email"
|
||||
className="input-field"
|
||||
@@ -445,7 +450,7 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1.5">手机号 (可选)</label>
|
||||
<label className="block text-sm font-medium mb-1.5">{t('accountManager.mobileOptional')}</label>
|
||||
<input
|
||||
type="text"
|
||||
className="input-field"
|
||||
@@ -455,19 +460,19 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1.5">密码 <span className="text-destructive">*</span></label>
|
||||
<label className="block text-sm font-medium mb-1.5">{t('accountManager.passwordLabel')} <span className="text-destructive">*</span></label>
|
||||
<input
|
||||
type="password"
|
||||
className="input-field bg-[#09090b]"
|
||||
placeholder="账号密码"
|
||||
placeholder={t('accountManager.passwordPlaceholder')}
|
||||
value={newAccount.password}
|
||||
onChange={e => setNewAccount({ ...newAccount, password: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2 pt-2">
|
||||
<button onClick={() => setShowAddAccount(false)} className="px-4 py-2 rounded-lg border border-border hover:bg-secondary transition-colors text-sm font-medium">取消</button>
|
||||
<button onClick={() => setShowAddAccount(false)} className="px-4 py-2 rounded-lg border border-border hover:bg-secondary transition-colors text-sm font-medium">{t('actions.cancel')}</button>
|
||||
<button onClick={addAccount} disabled={loading} className="px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors text-sm font-medium disabled:opacity-50">
|
||||
{loading ? '添加中...' : '添加账号'}
|
||||
{loading ? t('accountManager.addAccountLoading') : t('accountManager.addAccountAction')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useRef } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import {
|
||||
Send,
|
||||
Square,
|
||||
@@ -17,17 +17,13 @@ import {
|
||||
Zap
|
||||
} from 'lucide-react'
|
||||
import clsx from 'clsx'
|
||||
|
||||
const MODELS = [
|
||||
{ id: "deepseek-chat", name: "deepseek-chat", icon: MessageSquare, desc: "非思考模型", color: "text-amber-500" },
|
||||
{ id: "deepseek-reasoner", name: "deepseek-reasoner", icon: Cpu, desc: "思考模型", color: "text-amber-600" },
|
||||
{ id: "deepseek-chat-search", name: "deepseek-chat-search", icon: SearchIcon, desc: "非思考模型 (带搜索)", color: "text-cyan-500" },
|
||||
{ id: "deepseek-reasoner-search", name: "deepseek-reasoner-search", icon: SearchIcon, desc: "思考模型 (带搜索)", color: "text-cyan-600" },
|
||||
];
|
||||
import { useI18n } from '../i18n'
|
||||
|
||||
export default function ApiTester({ config, onMessage, authFetch }) {
|
||||
const { t } = useI18n()
|
||||
const [model, setModel] = useState('deepseek-chat')
|
||||
const [message, setMessage] = useState('Hello, please introduce yourself in one sentence.')
|
||||
const defaultMessage = t('apiTester.defaultMessage')
|
||||
const [message, setMessage] = useState(defaultMessage)
|
||||
const [apiKey, setApiKey] = useState('')
|
||||
const [selectedAccount, setSelectedAccount] = useState('')
|
||||
const [response, setResponse] = useState(null)
|
||||
@@ -36,12 +32,19 @@ export default function ApiTester({ config, onMessage, authFetch }) {
|
||||
const [streamingThinking, setStreamingThinking] = useState('')
|
||||
const [isStreaming, setIsStreaming] = useState(false)
|
||||
const abortControllerRef = useRef(null)
|
||||
const defaultMessageRef = useRef(defaultMessage)
|
||||
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false)
|
||||
const [configExpanded, setConfigExpanded] = useState(false)
|
||||
|
||||
const apiFetch = authFetch || fetch
|
||||
const accounts = config.accounts || []
|
||||
const models = [
|
||||
{ id: "deepseek-chat", name: "deepseek-chat", icon: MessageSquare, desc: t('apiTester.models.chat'), color: "text-amber-500" },
|
||||
{ id: "deepseek-reasoner", name: "deepseek-reasoner", icon: Cpu, desc: t('apiTester.models.reasoner'), color: "text-amber-600" },
|
||||
{ id: "deepseek-chat-search", name: "deepseek-chat-search", icon: SearchIcon, desc: t('apiTester.models.chatSearch'), color: "text-cyan-500" },
|
||||
{ id: "deepseek-reasoner-search", name: "deepseek-reasoner-search", icon: SearchIcon, desc: t('apiTester.models.reasonerSearch'), color: "text-cyan-600" },
|
||||
]
|
||||
|
||||
const stopGeneration = () => {
|
||||
if (abortControllerRef.current) {
|
||||
@@ -66,7 +69,7 @@ export default function ApiTester({ config, onMessage, authFetch }) {
|
||||
try {
|
||||
const key = apiKey || (config.keys?.[0] || '')
|
||||
if (!key) {
|
||||
onMessage('error', '请提供 API 密钥')
|
||||
onMessage('error', t('apiTester.missingApiKey'))
|
||||
setLoading(false)
|
||||
setIsStreaming(false)
|
||||
return
|
||||
@@ -88,8 +91,8 @@ export default function ApiTester({ config, onMessage, authFetch }) {
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json()
|
||||
setResponse({ success: false, error: data.error?.message || '请求失败' })
|
||||
onMessage('error', data.error?.message || '请求失败')
|
||||
setResponse({ success: false, error: data.error?.message || t('apiTester.requestFailed') })
|
||||
onMessage('error', data.error?.message || t('apiTester.requestFailed'))
|
||||
setLoading(false)
|
||||
setIsStreaming(false)
|
||||
return
|
||||
@@ -138,9 +141,9 @@ export default function ApiTester({ config, onMessage, authFetch }) {
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.name === 'AbortError') {
|
||||
onMessage('info', '已停止生成')
|
||||
onMessage('info', t('messages.generationStopped'))
|
||||
} else {
|
||||
onMessage('error', '网络错误: ' + e.message)
|
||||
onMessage('error', t('apiTester.networkError', { error: e.message }))
|
||||
setResponse({ error: e.message, success: false })
|
||||
}
|
||||
} finally {
|
||||
@@ -172,12 +175,12 @@ export default function ApiTester({ config, onMessage, authFetch }) {
|
||||
account: selectedAccount,
|
||||
})
|
||||
if (data.success) {
|
||||
onMessage('success', `${selectedAccount}: 测试成功 (${data.response_time}ms)`)
|
||||
onMessage('success', t('apiTester.testSuccess', { account: selectedAccount, time: data.response_time }))
|
||||
} else {
|
||||
onMessage('error', `${selectedAccount}: ${data.message}`)
|
||||
}
|
||||
} catch (e) {
|
||||
onMessage('error', '网络错误: ' + e.message)
|
||||
onMessage('error', t('apiTester.networkError', { error: e.message }))
|
||||
setResponse({ error: e.message })
|
||||
} finally {
|
||||
setLoading(false)
|
||||
@@ -201,12 +204,12 @@ export default function ApiTester({ config, onMessage, authFetch }) {
|
||||
onClick={() => setConfigExpanded(!configExpanded)}
|
||||
className="lg:hidden flex items-center justify-between p-4 w-full bg-muted/20 hover:bg-muted/30 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-2.5 font-medium text-sm text-foreground">
|
||||
<div className="p-1.5 rounded-md bg-transparent text-foreground">
|
||||
<Terminal className="w-4 h-4" />
|
||||
<div className="flex items-center gap-2.5 font-medium text-sm text-foreground">
|
||||
<div className="p-1.5 rounded-md bg-transparent text-foreground">
|
||||
<Terminal className="w-4 h-4" />
|
||||
</div>
|
||||
<span>{t('apiTester.config')}</span>
|
||||
</div>
|
||||
<span>配置</span>
|
||||
</div>
|
||||
<div className={clsx("transition-transform duration-300 text-muted-foreground", configExpanded ? "rotate-180" : "")}>
|
||||
<ChevronDown className="w-4 h-4" />
|
||||
</div>
|
||||
@@ -217,9 +220,9 @@ export default function ApiTester({ config, onMessage, authFetch }) {
|
||||
!configExpanded && "hidden lg:block"
|
||||
)}>
|
||||
<div className="space-y-3">
|
||||
<label className="text-[11px] font-semibold text-muted-foreground uppercase tracking-wider ml-0.5">模型</label>
|
||||
<label className="text-[11px] font-semibold text-muted-foreground uppercase tracking-wider ml-0.5">{t('apiTester.modelLabel')}</label>
|
||||
<div className="grid grid-cols-1 gap-2">
|
||||
{MODELS.map(m => {
|
||||
{models.map(m => {
|
||||
const Icon = m.icon
|
||||
return (
|
||||
<button
|
||||
@@ -256,14 +259,14 @@ export default function ApiTester({ config, onMessage, authFetch }) {
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-[11px] font-semibold text-muted-foreground uppercase tracking-wider ml-0.5">账号策略</label>
|
||||
<label className="text-[11px] font-semibold text-muted-foreground uppercase tracking-wider ml-0.5">{t('apiTester.accountStrategy')}</label>
|
||||
<div className="relative">
|
||||
<select
|
||||
className="w-full h-10 pl-3 pr-8 bg-secondary border border-border rounded-lg text-sm appearance-none focus:outline-none focus:ring-1 focus:ring-ring focus:border-ring transition-all cursor-pointer hover:bg-muted"
|
||||
value={selectedAccount}
|
||||
onChange={e => setSelectedAccount(e.target.value)}
|
||||
>
|
||||
<option value="" className="bg-popover text-popover-foreground">🎲 随机切换 (支持流式预览)</option>
|
||||
<option value="" className="bg-popover text-popover-foreground">{t('apiTester.randomRotation')}</option>
|
||||
{accounts.map((acc, i) => (
|
||||
<option key={i} value={acc.email || acc.mobile} className="bg-popover text-popover-foreground">
|
||||
👤 {acc.email || acc.mobile}
|
||||
@@ -275,11 +278,11 @@ export default function ApiTester({ config, onMessage, authFetch }) {
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-[11px] font-semibold text-muted-foreground uppercase tracking-wider ml-0.5">API 密钥 (可选)</label>
|
||||
<label className="text-[11px] font-semibold text-muted-foreground uppercase tracking-wider ml-0.5">{t('apiTester.apiKeyOptional')}</label>
|
||||
<input
|
||||
type="password"
|
||||
className="w-full h-10 px-3 bg-muted/30 border border-border rounded-lg text-sm font-mono placeholder:text-muted-foreground/40 focus:outline-none focus:ring-1 focus:ring-ring focus:border-ring transition-all"
|
||||
placeholder={config.keys?.[0] ? `默认: ...${config.keys[0].slice(-6)}` : '输入自定义密钥'}
|
||||
placeholder={config.keys?.[0] ? t('apiTester.apiKeyDefault', { suffix: config.keys[0].slice(-6) }) : t('apiTester.apiKeyPlaceholder')}
|
||||
value={apiKey}
|
||||
onChange={e => setApiKey(e.target.value)}
|
||||
/>
|
||||
@@ -324,7 +327,7 @@ export default function ApiTester({ config, onMessage, authFetch }) {
|
||||
"text-[10px] px-1.5 py-0.5 rounded-sm border uppercase font-medium tracking-wider",
|
||||
response.success ? "border-emerald-500/20 text-emerald-500 bg-emerald-500/10" : "border-destructive/20 text-destructive bg-destructive/10"
|
||||
)}>
|
||||
{response.status_code || '错误'}
|
||||
{response.status_code || t('apiTester.statusError')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -333,7 +336,7 @@ export default function ApiTester({ config, onMessage, authFetch }) {
|
||||
<div className="text-xs bg-secondary/50 border border-border rounded-lg p-3 space-y-1.5">
|
||||
<div className="flex items-center gap-1.5 text-muted-foreground">
|
||||
<Zap className="w-3.5 h-3.5" />
|
||||
<span className="font-medium">思维链过程</span>
|
||||
<span className="font-medium">{t('apiTester.reasoningTrace')}</span>
|
||||
</div>
|
||||
<div className="whitespace-pre-wrap leading-relaxed text-muted-foreground font-mono text-[11px] max-h-60 overflow-y-auto custom-scrollbar pl-5 border-l-2 border-border/50">
|
||||
{streamingThinking || response?.response?.thinking}
|
||||
@@ -345,7 +348,7 @@ export default function ApiTester({ config, onMessage, authFetch }) {
|
||||
{!selectedAccount ? (
|
||||
streamingContent || (response?.error && <span className="text-destructive font-medium">{response.error}</span>)
|
||||
) : (
|
||||
response?.response?.message || <span className="text-muted-foreground italic">正在生成响应...</span>
|
||||
response?.response?.message || <span className="text-muted-foreground italic">{t('apiTester.generating')}</span>
|
||||
)}
|
||||
{isStreaming && <span className="inline-block w-1.5 h-4 bg-primary ml-1 align-middle animate-pulse" />}
|
||||
</div>
|
||||
@@ -357,9 +360,9 @@ export default function ApiTester({ config, onMessage, authFetch }) {
|
||||
{/* Input Area */}
|
||||
<div className="p-4 lg:p-6 border-t border-border bg-card">
|
||||
<div className="max-w-4xl mx-auto relative group">
|
||||
<textarea
|
||||
className="w-full bg-[#09090b] border border-border rounded-xl pl-4 pr-12 py-3 text-sm focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all resize-none custom-scrollbar placeholder:text-muted-foreground/50 text-foreground shadow-inner"
|
||||
placeholder="输入消息..."
|
||||
<textarea
|
||||
className="w-full bg-[#09090b] border border-border rounded-xl pl-4 pr-12 py-3 text-sm focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all resize-none custom-scrollbar placeholder:text-muted-foreground/50 text-foreground shadow-inner"
|
||||
placeholder={t('apiTester.enterMessage')}
|
||||
rows={1}
|
||||
style={{ minHeight: '52px' }}
|
||||
value={message}
|
||||
@@ -391,10 +394,14 @@ export default function ApiTester({ config, onMessage, authFetch }) {
|
||||
</div>
|
||||
</div>
|
||||
<div className="max-w-4xl mx-auto mt-3 flex justify-center">
|
||||
<span className="text-[10px] text-muted-foreground/40 font-medium">DeepSeek 管理员界面</span>
|
||||
<span className="text-[10px] text-muted-foreground/40 font-medium">{t('apiTester.adminConsoleLabel')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
useEffect(() => {
|
||||
setMessage((prev) => (prev === defaultMessageRef.current ? defaultMessage : prev))
|
||||
defaultMessageRef.current = defaultMessage
|
||||
}, [defaultMessage])
|
||||
|
||||
@@ -1,68 +1,69 @@
|
||||
import { useState } from 'react'
|
||||
import { FileCode, Download, Upload, Copy, Check, AlertTriangle } from 'lucide-react'
|
||||
import clsx from 'clsx'
|
||||
|
||||
const TEMPLATES = {
|
||||
full: {
|
||||
name: '全量配置模板',
|
||||
desc: '包含密钥、账号及模型映射',
|
||||
config: {
|
||||
keys: ["your-api-key-1", "your-api-key-2"],
|
||||
accounts: [
|
||||
{ email: "user1@example.com", password: "password1", token: "" },
|
||||
{ email: "user2@example.com", password: "password2", token: "" },
|
||||
{ mobile: "+8613800138001", password: "password3", token: "" }
|
||||
],
|
||||
claude_model_mapping: {
|
||||
fast: "deepseek-chat",
|
||||
slow: "deepseek-reasoner"
|
||||
}
|
||||
}
|
||||
},
|
||||
email_only: {
|
||||
name: '仅邮箱账号',
|
||||
desc: '批量导入邮箱格式账号',
|
||||
config: {
|
||||
keys: ["your-api-key"],
|
||||
accounts: [
|
||||
{ email: "account1@example.com", password: "pass1", token: "" },
|
||||
{ email: "account2@example.com", password: "pass2", token: "" },
|
||||
{ email: "account3@example.com", password: "pass3", token: "" }
|
||||
]
|
||||
}
|
||||
},
|
||||
mobile_only: {
|
||||
name: '仅手机号账号',
|
||||
desc: '批量导入手机号格式账号',
|
||||
config: {
|
||||
keys: ["your-api-key"],
|
||||
accounts: [
|
||||
{ mobile: "+8613800000001", password: "pass1", token: "" },
|
||||
{ mobile: "+8613800000002", password: "pass2", token: "" },
|
||||
{ mobile: "+8613800000003", password: "pass3", token: "" }
|
||||
]
|
||||
}
|
||||
},
|
||||
keys_only: {
|
||||
name: '仅 API 密钥',
|
||||
desc: '仅添加 API 访问密钥',
|
||||
config: {
|
||||
keys: ["key-1", "key-2", "key-3"]
|
||||
}
|
||||
}
|
||||
}
|
||||
import { useI18n } from '../i18n'
|
||||
|
||||
export default function BatchImport({ onRefresh, onMessage, authFetch }) {
|
||||
const { t } = useI18n()
|
||||
const [jsonInput, setJsonInput] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [result, setResult] = useState(null)
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
const apiFetch = authFetch || fetch
|
||||
const templates = {
|
||||
full: {
|
||||
name: t('batchImport.templates.full.name'),
|
||||
desc: t('batchImport.templates.full.desc'),
|
||||
config: {
|
||||
keys: ["your-api-key-1", "your-api-key-2"],
|
||||
accounts: [
|
||||
{ email: "user1@example.com", password: "password1", token: "" },
|
||||
{ email: "user2@example.com", password: "password2", token: "" },
|
||||
{ mobile: "+8613800138001", password: "password3", token: "" }
|
||||
],
|
||||
claude_model_mapping: {
|
||||
fast: "deepseek-chat",
|
||||
slow: "deepseek-reasoner"
|
||||
}
|
||||
}
|
||||
},
|
||||
email_only: {
|
||||
name: t('batchImport.templates.emailOnly.name'),
|
||||
desc: t('batchImport.templates.emailOnly.desc'),
|
||||
config: {
|
||||
keys: ["your-api-key"],
|
||||
accounts: [
|
||||
{ email: "account1@example.com", password: "pass1", token: "" },
|
||||
{ email: "account2@example.com", password: "pass2", token: "" },
|
||||
{ email: "account3@example.com", password: "pass3", token: "" }
|
||||
]
|
||||
}
|
||||
},
|
||||
mobile_only: {
|
||||
name: t('batchImport.templates.mobileOnly.name'),
|
||||
desc: t('batchImport.templates.mobileOnly.desc'),
|
||||
config: {
|
||||
keys: ["your-api-key"],
|
||||
accounts: [
|
||||
{ mobile: "+8613800000001", password: "pass1", token: "" },
|
||||
{ mobile: "+8613800000002", password: "pass2", token: "" },
|
||||
{ mobile: "+8613800000003", password: "pass3", token: "" }
|
||||
]
|
||||
}
|
||||
},
|
||||
keys_only: {
|
||||
name: t('batchImport.templates.keysOnly.name'),
|
||||
desc: t('batchImport.templates.keysOnly.desc'),
|
||||
config: {
|
||||
keys: ["key-1", "key-2", "key-3"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleImport = async () => {
|
||||
if (!jsonInput.trim()) {
|
||||
onMessage('error', '请输入 JSON 配置内容')
|
||||
onMessage('error', t('batchImport.enterJson'))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -70,7 +71,7 @@ export default function BatchImport({ onRefresh, onMessage, authFetch }) {
|
||||
try {
|
||||
config = JSON.parse(jsonInput)
|
||||
} catch (e) {
|
||||
onMessage('error', '无效的 JSON 格式')
|
||||
onMessage('error', t('messages.invalidJson'))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -85,23 +86,23 @@ export default function BatchImport({ onRefresh, onMessage, authFetch }) {
|
||||
const data = await res.json()
|
||||
if (res.ok) {
|
||||
setResult(data)
|
||||
onMessage('success', `导入成功: ${data.imported_keys} 个密钥, ${data.imported_accounts} 个账号`)
|
||||
onMessage('success', t('batchImport.importSuccess', { keys: data.imported_keys, accounts: data.imported_accounts }))
|
||||
onRefresh()
|
||||
} else {
|
||||
onMessage('error', data.detail || '导入失败')
|
||||
onMessage('error', data.detail || t('messages.importFailed'))
|
||||
}
|
||||
} catch (e) {
|
||||
onMessage('error', '网络错误')
|
||||
onMessage('error', t('messages.networkError'))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const loadTemplate = (key) => {
|
||||
const tpl = TEMPLATES[key]
|
||||
const tpl = templates[key]
|
||||
if (tpl) {
|
||||
setJsonInput(JSON.stringify(tpl.config, null, 2))
|
||||
onMessage('info', `已加载模板: ${tpl.name}`)
|
||||
onMessage('info', t('batchImport.templateLoaded', { name: tpl.name }))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,10 +112,10 @@ export default function BatchImport({ onRefresh, onMessage, authFetch }) {
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
setJsonInput(JSON.stringify(JSON.parse(data.json), null, 2))
|
||||
onMessage('success', '当前配置已加载')
|
||||
onMessage('success', t('batchImport.currentConfigLoaded'))
|
||||
}
|
||||
} catch (e) {
|
||||
onMessage('error', '获取配置失败')
|
||||
onMessage('error', t('batchImport.fetchConfigFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,10 +127,10 @@ export default function BatchImport({ onRefresh, onMessage, authFetch }) {
|
||||
await navigator.clipboard.writeText(data.base64)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
onMessage('success', 'Base64 配置已复制到剪贴板')
|
||||
onMessage('success', t('batchImport.copySuccess'))
|
||||
}
|
||||
} catch (e) {
|
||||
onMessage('error', '复制失败')
|
||||
onMessage('error', t('messages.copyFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,10 +141,10 @@ export default function BatchImport({ onRefresh, onMessage, authFetch }) {
|
||||
<div className="bg-card border border-border rounded-xl p-5 shadow-sm">
|
||||
<h3 className="font-semibold flex items-center gap-2 mb-4">
|
||||
<FileCode className="w-4 h-4 text-primary" />
|
||||
快速模板
|
||||
{t('batchImport.quickTemplates')}
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
{Object.entries(TEMPLATES).map(([key, tpl]) => (
|
||||
{Object.entries(templates).map(([key, tpl]) => (
|
||||
<button
|
||||
key={key}
|
||||
onClick={() => loadTemplate(key)}
|
||||
@@ -159,20 +160,20 @@ export default function BatchImport({ onRefresh, onMessage, authFetch }) {
|
||||
<div className="bg-linear-to-br from-primary/10 to-transparent border border-primary/20 rounded-xl p-5 shadow-sm">
|
||||
<h3 className="font-semibold flex items-center gap-2 mb-2 text-primary">
|
||||
<Download className="w-4 h-4" />
|
||||
数据导出
|
||||
{t('batchImport.dataExport')}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
获取配置的 Base64 字符串,用于 Vercel 环境变量。
|
||||
{t('batchImport.dataExportDesc')}
|
||||
</p>
|
||||
<button
|
||||
onClick={copyBase64}
|
||||
className="w-full flex items-center justify-center gap-2 py-2.5 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-all font-medium text-sm shadow-sm"
|
||||
>
|
||||
{copied ? <Check className="w-4 h-4" /> : <Copy className="w-4 h-4" />}
|
||||
{copied ? '已复制' : '复制 Base64 配置'}
|
||||
{copied ? t('batchImport.copied') : t('batchImport.copyBase64')}
|
||||
</button>
|
||||
<p className="text-[10px] text-muted-foreground mt-2 text-center">
|
||||
变量名: <code className="bg-background px-1 py-0.5 rounded border border-border">DS2API_CONFIG_JSON</code>
|
||||
{t('batchImport.variableName')}: <code className="bg-background px-1 py-0.5 rounded border border-border">DS2API_CONFIG_JSON</code>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -182,14 +183,14 @@ export default function BatchImport({ onRefresh, onMessage, authFetch }) {
|
||||
<div className="p-4 border-b border-border flex items-center justify-between bg-muted/20">
|
||||
<h3 className="font-semibold flex items-center gap-2">
|
||||
<Upload className="w-4 h-4 text-primary" />
|
||||
JSON 编辑器
|
||||
{t('batchImport.jsonEditor')}
|
||||
</h3>
|
||||
<div className="flex gap-2">
|
||||
<button onClick={handleExport} className="px-3 py-1.5 bg-secondary text-secondary-foreground rounded-lg hover:bg-secondary/80 transition-colors text-xs font-medium border border-border">
|
||||
加载当前配置
|
||||
{t('batchImport.loadCurrentConfig')}
|
||||
</button>
|
||||
<button onClick={handleImport} disabled={loading} className="px-3 py-1.5 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors text-xs font-medium disabled:opacity-50">
|
||||
{loading ? '正在导入...' : '应用配置'}
|
||||
{loading ? t('batchImport.importing') : t('batchImport.applyConfig')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -217,10 +218,10 @@ export default function BatchImport({ onRefresh, onMessage, authFetch }) {
|
||||
)}
|
||||
<div>
|
||||
<h4 className={clsx("font-medium", result.imported_keys || result.imported_accounts ? "text-emerald-500" : "text-destructive")}>
|
||||
导入操作已完成
|
||||
{t('batchImport.importComplete')}
|
||||
</h4>
|
||||
<p className="text-sm opacity-80 mt-1">
|
||||
成功导入了 {result.imported_keys} 个 API 密钥,并更新了 {result.imported_accounts} 个账号。
|
||||
{t('batchImport.importSummary', { keys: result.imported_keys, accounts: result.imported_accounts })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import React from 'react'
|
||||
import { useI18n } from '../i18n'
|
||||
import LanguageToggle from './LanguageToggle'
|
||||
|
||||
const LandingPage = ({ onEnter }) => {
|
||||
const { t } = useI18n()
|
||||
return (
|
||||
<div className="landing-container min-h-screen relative overflow-hidden flex flex-col items-center justify-center p-6 text-center">
|
||||
{/* Animated Background Elements - using Tailwind with some custom CSS in styles.css if needed,
|
||||
@@ -84,6 +87,10 @@ const LandingPage = ({ onEnter }) => {
|
||||
<div className="blob" style={{ top: '10%', left: '15%' }} />
|
||||
<div className="blob" style={{ bottom: '10%', right: '15%', animationDelay: '-5s' }} />
|
||||
|
||||
<div className="absolute top-6 right-6 z-20">
|
||||
<LanguageToggle />
|
||||
</div>
|
||||
|
||||
<div className="landing-content">
|
||||
<header className="mb-12">
|
||||
<h1 className="logo-text">DS2API</h1>
|
||||
@@ -97,14 +104,14 @@ const LandingPage = ({ onEnter }) => {
|
||||
onClick={onEnter}
|
||||
className="btn-premium text-white px-8 py-3 rounded-xl font-bold transition-all flex items-center gap-2"
|
||||
>
|
||||
<span>🎛️</span> 管理面板
|
||||
<span>🎛️</span> {t('landing.adminConsole')}
|
||||
</button>
|
||||
<a
|
||||
href="/v1/models"
|
||||
target="_blank"
|
||||
className="glass-card text-white px-8 py-3 rounded-xl font-semibold transition-all flex items-center gap-2"
|
||||
>
|
||||
<span>📡</span> API 状态
|
||||
<span>📡</span> {t('landing.apiStatus')}
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/CJackHwang/ds2api"
|
||||
@@ -117,10 +124,10 @@ const LandingPage = ({ onEnter }) => {
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-6 text-left">
|
||||
{[
|
||||
{ icon: '🚀', title: '全面兼容', desc: '适配 OpenAI 与 Claude 格式' },
|
||||
{ icon: '⚖️', title: '负载均衡', desc: '智能轮询,稳定高效' },
|
||||
{ icon: '🧠', title: '深度思考', desc: '支持推理过程输出' },
|
||||
{ icon: '🔍', title: '联网搜索', desc: '集成原生网页搜索能力' },
|
||||
{ icon: '🚀', title: t('landing.features.compatibility.title'), desc: t('landing.features.compatibility.desc') },
|
||||
{ icon: '⚖️', title: t('landing.features.loadBalancing.title'), desc: t('landing.features.loadBalancing.desc') },
|
||||
{ icon: '🧠', title: t('landing.features.reasoning.title'), desc: t('landing.features.reasoning.desc') },
|
||||
{ icon: '🔍', title: t('landing.features.search.title'), desc: t('landing.features.search.desc') },
|
||||
].map((feature, idx) => (
|
||||
<div key={idx} className="glass-card p-6 rounded-2xl">
|
||||
<span className="text-2xl mb-4 block">{feature.icon}</span>
|
||||
|
||||
18
webui/src/components/LanguageToggle.jsx
Normal file
18
webui/src/components/LanguageToggle.jsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { useI18n } from '../i18n'
|
||||
|
||||
export default function LanguageToggle({ className = '' }) {
|
||||
const { lang, setLang, t } = useI18n()
|
||||
const nextLang = lang === 'zh' ? 'en' : 'zh'
|
||||
const label = nextLang === 'zh' ? t('language.chinese') : t('language.english')
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setLang(nextLang)}
|
||||
className={`text-xs font-semibold px-2 py-1 rounded-md border border-border bg-secondary/50 text-muted-foreground hover:text-foreground hover:bg-secondary transition-colors ${className}`}
|
||||
title={t('language.label')}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
@@ -1,8 +1,11 @@
|
||||
import { useState } from 'react'
|
||||
import { Key, ArrowRight, ShieldCheck, Lock, Check } from 'lucide-react'
|
||||
import clsx from 'clsx'
|
||||
import { useI18n } from '../i18n'
|
||||
import LanguageToggle from './LanguageToggle'
|
||||
|
||||
export default function Login({ onLogin, onMessage }) {
|
||||
const { t } = useI18n()
|
||||
const [adminKey, setAdminKey] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [remember, setRemember] = useState(true)
|
||||
@@ -32,10 +35,10 @@ export default function Login({ onLogin, onMessage }) {
|
||||
onMessage('warning', data.message)
|
||||
}
|
||||
} else {
|
||||
onMessage('error', data.detail || '登录失败')
|
||||
onMessage('error', data.detail || t('login.signInFailed'))
|
||||
}
|
||||
} catch (e) {
|
||||
onMessage('error', '网络错误: ' + e.message)
|
||||
onMessage('error', t('login.networkError', { error: e.message }))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -43,6 +46,9 @@ export default function Login({ onLogin, onMessage }) {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen w-full flex flex-col items-center justify-center p-4 bg-background text-foreground">
|
||||
<div className="absolute top-6 right-6">
|
||||
<LanguageToggle />
|
||||
</div>
|
||||
|
||||
<div className="w-full max-w-[400px] relative z-10 animate-in fade-in zoom-in-95 duration-200">
|
||||
<div className="w-full bg-card border border-border rounded-xl p-8 shadow-sm">
|
||||
@@ -50,13 +56,13 @@ export default function Login({ onLogin, onMessage }) {
|
||||
<div className="inline-flex items-center justify-center w-12 h-12 rounded-xl bg-primary/10 text-primary mb-2">
|
||||
<Lock className="w-6 h-6" />
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold tracking-tight text-foreground">欢迎回来</h1>
|
||||
<p className="text-sm text-muted-foreground/80">请输入管理员密钥以继续</p>
|
||||
<h1 className="text-3xl font-bold tracking-tight text-foreground">{t('login.welcome')}</h1>
|
||||
<p className="text-sm text-muted-foreground/80">{t('login.subtitle')}</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleLogin} className="space-y-5 animate-in fade-in slide-in-from-bottom-4 duration-700 delay-150">
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-semibold text-muted-foreground uppercase tracking-widest ml-1">管理员密钥</label>
|
||||
<label className="text-xs font-semibold text-muted-foreground uppercase tracking-widest ml-1">{t('login.adminKeyLabel')}</label>
|
||||
<div className="relative group">
|
||||
<div className="absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none text-muted-foreground group-focus-within:text-primary transition-colors">
|
||||
<Key className="w-4 h-4" />
|
||||
@@ -64,7 +70,7 @@ export default function Login({ onLogin, onMessage }) {
|
||||
<input
|
||||
type="password"
|
||||
className="w-full bg-[#09090b] border border-border rounded-xl pl-10 pr-4 py-3 text-sm focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all placeholder:text-muted-foreground/30 text-foreground"
|
||||
placeholder="输入您的管理员密钥..."
|
||||
placeholder={t('login.adminKeyPlaceholder')}
|
||||
value={adminKey}
|
||||
onChange={e => setAdminKey(e.target.value)}
|
||||
autoFocus
|
||||
@@ -84,7 +90,7 @@ export default function Login({ onLogin, onMessage }) {
|
||||
<div className="w-4.5 h-4.5 bg-secondary border border-border rounded-md peer-checked:bg-primary peer-checked:border-primary transition-all shadow-sm"></div>
|
||||
<Check className="absolute w-3 h-3 text-primary-foreground opacity-0 peer-checked:opacity-100 left-0.5 transition-opacity" />
|
||||
</div>
|
||||
<span className="text-xs font-medium text-muted-foreground group-hover:text-foreground transition-colors">记住登录状态</span>
|
||||
<span className="text-xs font-medium text-muted-foreground group-hover:text-foreground transition-colors">{t('login.rememberSession')}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -97,7 +103,7 @@ export default function Login({ onLogin, onMessage }) {
|
||||
<div className="w-5 h-5 border-2 border-primary-foreground/30 border-t-primary-foreground rounded-full animate-spin" />
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<span>登录</span>
|
||||
<span>{t('login.signIn')}</span>
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</div>
|
||||
)}
|
||||
@@ -107,13 +113,13 @@ export default function Login({ onLogin, onMessage }) {
|
||||
<div className="mt-6 pt-6 border-t border-border flex justify-center">
|
||||
<div className="flex items-center gap-1.5 text-[10px] text-muted-foreground/60 font-medium tracking-wide uppercase">
|
||||
<ShieldCheck className="w-3 h-3" />
|
||||
<span>安全连接</span>
|
||||
<span>{t('login.secureConnection')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 text-center">
|
||||
<p className="text-[10px] text-muted-foreground/30 font-mono text-center">DS2API 管理员门户</p>
|
||||
<p className="text-[10px] text-muted-foreground/30 font-mono text-center">{t('login.adminPortal')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Cloud, ArrowRight, ExternalLink, Info, CheckCircle2, XCircle } from 'lucide-react'
|
||||
import { useI18n } from '../i18n'
|
||||
|
||||
export default function VercelSync({ onMessage, authFetch }) {
|
||||
const { t } = useI18n()
|
||||
const [vercelToken, setVercelToken] = useState('')
|
||||
const [projectId, setProjectId] = useState('')
|
||||
const [teamId, setTeamId] = useState('')
|
||||
@@ -32,11 +34,11 @@ export default function VercelSync({ onMessage, authFetch }) {
|
||||
const tokenToUse = preconfig?.has_token && !vercelToken ? '__USE_PRECONFIG__' : vercelToken
|
||||
|
||||
if (!tokenToUse && !preconfig?.has_token) {
|
||||
onMessage('error', '需要 Vercel 访问令牌')
|
||||
onMessage('error', t('vercel.tokenRequired'))
|
||||
return
|
||||
}
|
||||
if (!projectId) {
|
||||
onMessage('error', '需要项目 ID')
|
||||
onMessage('error', t('vercel.projectRequired'))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -58,10 +60,10 @@ export default function VercelSync({ onMessage, authFetch }) {
|
||||
onMessage('success', data.message)
|
||||
} else {
|
||||
setResult({ ...data, success: false })
|
||||
onMessage('error', data.detail || '同步失败')
|
||||
onMessage('error', data.detail || t('vercel.syncFailed'))
|
||||
}
|
||||
} catch (e) {
|
||||
onMessage('error', '网络错误')
|
||||
onMessage('error', t('vercel.networkError'))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -74,26 +76,26 @@ export default function VercelSync({ onMessage, authFetch }) {
|
||||
<div className="border-b border-border pb-6">
|
||||
<h2 className="text-xl font-semibold flex items-center gap-2">
|
||||
<Cloud className="w-6 h-6 text-primary" />
|
||||
Vercel 部署
|
||||
{t('vercel.title')}
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-sm mt-1">
|
||||
将当前密钥和账号配置直接同步到 Vercel 环境变量中。
|
||||
{t('vercel.description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium flex items-center justify-between">
|
||||
Vercel 访问令牌
|
||||
{t('vercel.tokenLabel')}
|
||||
<a href="https://vercel.com/account/tokens" target="_blank" rel="noopener noreferrer" className="text-xs text-primary hover:underline flex items-center gap-1">
|
||||
获取令牌 <ExternalLink className="w-3 h-3" />
|
||||
{t('vercel.getToken')} <ExternalLink className="w-3 h-3" />
|
||||
</a>
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="password"
|
||||
className="w-full h-10 px-3 bg-background border border-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring transition-all pr-10"
|
||||
placeholder={preconfig?.has_token ? "正在使用预配置的令牌" : "输入 Vercel 访问令牌"}
|
||||
placeholder={preconfig?.has_token ? t('vercel.tokenPlaceholderPreconfig') : t('vercel.tokenPlaceholder')}
|
||||
value={vercelToken}
|
||||
onChange={e => setVercelToken(e.target.value)}
|
||||
/>
|
||||
@@ -106,7 +108,7 @@ export default function VercelSync({ onMessage, authFetch }) {
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">项目 ID</label>
|
||||
<label className="text-sm font-medium">{t('vercel.projectIdLabel')}</label>
|
||||
<input
|
||||
type="text"
|
||||
className="w-full h-10 px-3 bg-background border border-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring transition-all"
|
||||
@@ -114,12 +116,12 @@ export default function VercelSync({ onMessage, authFetch }) {
|
||||
value={projectId}
|
||||
onChange={e => setProjectId(e.target.value)}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">可在项目设置 (Project Settings) → 常规 (General) 中找到</p>
|
||||
<p className="text-xs text-muted-foreground">{t('vercel.projectIdHint')}</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium flex items-center gap-2">
|
||||
团队 ID <span className="text-xs text-muted-foreground font-normal">(可选)</span>
|
||||
{t('vercel.teamIdLabel')} <span className="text-xs text-muted-foreground font-normal">({t('vercel.optional')})</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -140,16 +142,16 @@ export default function VercelSync({ onMessage, authFetch }) {
|
||||
{loading ? (
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="w-4 h-4 border-2 border-current border-t-transparent rounded-full animate-spin" />
|
||||
正在同步...
|
||||
{t('vercel.syncing')}
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex items-center gap-2">
|
||||
同步并重新部署 <ArrowRight className="w-4 h-4" />
|
||||
{t('vercel.syncRedeploy')} <ArrowRight className="w-4 h-4" />
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
<p className="text-xs text-center text-muted-foreground mt-4">
|
||||
这将触发 Vercel 的重新部署,大约需要 30-60 秒。
|
||||
{t('vercel.redeployHint')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -170,14 +172,14 @@ export default function VercelSync({ onMessage, authFetch }) {
|
||||
)}
|
||||
<div className="space-y-1">
|
||||
<h3 className={`font-semibold text-lg ${result.success ? 'text-emerald-500' : 'text-destructive'}`}>
|
||||
{result.success ? '同步成功' : '同步失败'}
|
||||
{result.success ? t('vercel.syncSucceeded') : t('vercel.syncFailedLabel')}
|
||||
</h3>
|
||||
<p className="text-sm opacity-90">{result.message}</p>
|
||||
|
||||
{result.deployment_url && (
|
||||
<div className="pt-3 mt-3 border-t border-emerald-500/20">
|
||||
<a href={`https://${result.deployment_url}`} target="_blank" rel="noopener noreferrer" className="inline-flex items-center gap-1 text-sm font-medium hover:underline">
|
||||
访问部署地址 <ExternalLink className="w-3 h-3" />
|
||||
{t('vercel.openDeployment')} <ExternalLink className="w-3 h-3" />
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
@@ -189,24 +191,26 @@ export default function VercelSync({ onMessage, authFetch }) {
|
||||
<div className="bg-secondary/20 border border-border rounded-xl p-6">
|
||||
<h3 className="font-semibold flex items-center gap-2 mb-4">
|
||||
<Info className="w-5 h-5 text-primary" />
|
||||
工作原理
|
||||
{t('vercel.howItWorks')}
|
||||
</h3>
|
||||
<ul className="space-y-4">
|
||||
<li className="flex gap-3">
|
||||
<span className="shrink-0 w-6 h-6 rounded-full bg-background border border-border flex items-center justify-center text-xs font-bold text-muted-foreground">1</span>
|
||||
<p className="text-sm text-muted-foreground">当前配置 (密钥和账号) 被导出为 JSON 字符串。</p>
|
||||
<p className="text-sm text-muted-foreground">{t('vercel.steps.one')}</p>
|
||||
</li>
|
||||
<li className="flex gap-3">
|
||||
<span className="shrink-0 w-6 h-6 rounded-full bg-background border border-border flex items-center justify-center text-xs font-bold text-muted-foreground">2</span>
|
||||
<p className="text-sm text-muted-foreground">JSON 被编码为 Base64 以确保格式兼容性。</p>
|
||||
<p className="text-sm text-muted-foreground">{t('vercel.steps.two')}</p>
|
||||
</li>
|
||||
<li className="flex gap-3">
|
||||
<span className="shrink-0 w-6 h-6 rounded-full bg-background border border-border flex items-center justify-center text-xs font-bold text-muted-foreground">3</span>
|
||||
<p className="text-sm text-muted-foreground">更新 Vercel 项目中的 <code className="bg-background px-1 py-0.5 rounded border border-border text-xs">DS2API_CONFIG_JSON</code> 环境变量。</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('vercel.steps.three')} <code className="bg-background px-1 py-0.5 rounded border border-border text-xs">DS2API_CONFIG_JSON</code>
|
||||
</p>
|
||||
</li>
|
||||
<li className="flex gap-3">
|
||||
<span className="shrink-0 w-6 h-6 rounded-full bg-background border border-border flex items-center justify-center text-xs font-bold text-muted-foreground">4</span>
|
||||
<p className="text-sm text-muted-foreground">触发重新部署以应用新的环境变量。</p>
|
||||
<p className="text-sm text-muted-foreground">{t('vercel.steps.four')}</p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user