feat: add i18n language toggle and bilingual docs

This commit is contained in:
CJACK.
2026-02-06 02:36:49 +08:00
parent 9626d6ccbd
commit 015ec6eb3c
21 changed files with 2269 additions and 235 deletions

View File

@@ -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>