mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-15 05:35:07 +08:00
refactor: migrate UI to Tailwind CSS for a modernized design system.
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
import { useState } from 'react'
|
||||
import { FileCode, Download, Upload, Copy, Check, AlertTriangle } from 'lucide-react'
|
||||
import clsx from 'clsx'
|
||||
|
||||
// 模板配置
|
||||
const TEMPLATES = {
|
||||
full: {
|
||||
name: '完整模板',
|
||||
desc: '包含所有配置项',
|
||||
name: 'Full Configuration',
|
||||
desc: 'Includes keys, accounts, and model mappings',
|
||||
config: {
|
||||
keys: ["your-api-key-1", "your-api-key-2"],
|
||||
accounts: [
|
||||
@@ -19,8 +20,8 @@ const TEMPLATES = {
|
||||
}
|
||||
},
|
||||
email_only: {
|
||||
name: '邮箱账号模板',
|
||||
desc: '仅邮箱账号',
|
||||
name: 'Email Only',
|
||||
desc: 'Batch import email accounts',
|
||||
config: {
|
||||
keys: ["your-api-key"],
|
||||
accounts: [
|
||||
@@ -31,8 +32,8 @@ const TEMPLATES = {
|
||||
}
|
||||
},
|
||||
mobile_only: {
|
||||
name: '手机号账号模板',
|
||||
desc: '仅手机号账号',
|
||||
name: 'Mobile Only',
|
||||
desc: 'Batch import mobile number accounts',
|
||||
config: {
|
||||
keys: ["your-api-key"],
|
||||
accounts: [
|
||||
@@ -43,8 +44,8 @@ const TEMPLATES = {
|
||||
}
|
||||
},
|
||||
keys_only: {
|
||||
name: '仅 API Keys',
|
||||
desc: '只添加 API Keys',
|
||||
name: 'API Keys Only',
|
||||
desc: 'Just adding API access keys',
|
||||
config: {
|
||||
keys: ["key-1", "key-2", "key-3"]
|
||||
}
|
||||
@@ -55,13 +56,13 @@ export default function BatchImport({ onRefresh, onMessage, authFetch }) {
|
||||
const [jsonInput, setJsonInput] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [result, setResult] = useState(null)
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
// 使用 authFetch 或回退到普通 fetch
|
||||
const apiFetch = authFetch || fetch
|
||||
|
||||
const handleImport = async () => {
|
||||
if (!jsonInput.trim()) {
|
||||
onMessage('error', '请输入 JSON 配置')
|
||||
onMessage('error', 'Please enter JSON configuration')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -69,7 +70,7 @@ export default function BatchImport({ onRefresh, onMessage, authFetch }) {
|
||||
try {
|
||||
config = JSON.parse(jsonInput)
|
||||
} catch (e) {
|
||||
onMessage('error', 'JSON 格式无效')
|
||||
onMessage('error', 'Invalid JSON format')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -84,13 +85,13 @@ export default function BatchImport({ onRefresh, onMessage, authFetch }) {
|
||||
const data = await res.json()
|
||||
if (res.ok) {
|
||||
setResult(data)
|
||||
onMessage('success', `导入成功: ${data.imported_keys} 个 Key, ${data.imported_accounts} 个账号`)
|
||||
onMessage('success', `Imported: ${data.imported_keys} Keys, ${data.imported_accounts} Accounts`)
|
||||
onRefresh()
|
||||
} else {
|
||||
onMessage('error', data.detail || '导入失败')
|
||||
onMessage('error', data.detail || 'Import failed')
|
||||
}
|
||||
} catch (e) {
|
||||
onMessage('error', '网络错误')
|
||||
onMessage('error', 'Network error')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -100,7 +101,7 @@ export default function BatchImport({ onRefresh, onMessage, authFetch }) {
|
||||
const tpl = TEMPLATES[key]
|
||||
if (tpl) {
|
||||
setJsonInput(JSON.stringify(tpl.config, null, 2))
|
||||
onMessage('info', `已加载「${tpl.name}」`)
|
||||
onMessage('info', `Loaded template: ${tpl.name}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,10 +111,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', 'Configuration loaded')
|
||||
}
|
||||
} catch (e) {
|
||||
onMessage('error', '获取配置失败')
|
||||
onMessage('error', 'Failed to fetch config')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,81 +124,109 @@ export default function BatchImport({ onRefresh, onMessage, authFetch }) {
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
await navigator.clipboard.writeText(data.base64)
|
||||
onMessage('success', 'Base64 已复制到剪贴板')
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
onMessage('success', 'Base64 copied to clipboard')
|
||||
}
|
||||
} catch (e) {
|
||||
onMessage('error', '复制失败')
|
||||
onMessage('error', 'Copy failed')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="section">
|
||||
{/* 模板选择 */}
|
||||
<div className="card">
|
||||
<div className="card-title" style={{ marginBottom: '1rem' }}>📋 快速模板</div>
|
||||
<div className="grid grid-2">
|
||||
{Object.entries(TEMPLATES).map(([key, tpl]) => (
|
||||
<div
|
||||
key={key}
|
||||
style={{
|
||||
padding: '1rem',
|
||||
background: 'var(--bg-tertiary)',
|
||||
borderRadius: 'var(--radius)',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s',
|
||||
border: '1px solid transparent'
|
||||
}}
|
||||
onClick={() => loadTemplate(key)}
|
||||
onMouseEnter={e => e.currentTarget.style.borderColor = 'var(--accent)'}
|
||||
onMouseLeave={e => e.currentTarget.style.borderColor = 'transparent'}
|
||||
>
|
||||
<div style={{ fontWeight: 600, marginBottom: '0.25rem' }}>{tpl.name}</div>
|
||||
<div style={{ fontSize: '0.85rem', color: 'var(--text-secondary)' }}>{tpl.desc}</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 h-[calc(100vh-140px)]">
|
||||
{/* Templates Panel */}
|
||||
<div className="md:col-span-1 space-y-4">
|
||||
<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" />
|
||||
Quick Templates
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
{Object.entries(TEMPLATES).map(([key, tpl]) => (
|
||||
<button
|
||||
key={key}
|
||||
onClick={() => loadTemplate(key)}
|
||||
className="w-full text-left p-3 rounded-lg border border-border bg-secondary/20 hover:bg-secondary/50 hover:border-primary/50 transition-all custom-focus group"
|
||||
>
|
||||
<div className="font-medium text-sm group-hover:text-primary transition-colors">{tpl.name}</div>
|
||||
<div className="text-xs text-muted-foreground mt-0.5">{tpl.desc}</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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" />
|
||||
Export Data
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
Get your configuration as a Base64 string for Vercel environment variables.
|
||||
</p>
|
||||
<button
|
||||
onClick={copyBase64}
|
||||
className="w-full btn btn-primary bg-primary/90 hover:bg-primary shadow-lg shadow-primary/20"
|
||||
>
|
||||
{copied ? <Check className="w-4 h-4 mr-2" /> : <Copy className="w-4 h-4 mr-2" />}
|
||||
{copied ? 'Copied!' : 'Copy Base64 Config'}
|
||||
</button>
|
||||
<p className="text-[10px] text-muted-foreground mt-2 text-center">
|
||||
Variable Name: <code className="bg-background px-1 py-0.5 rounded border border-border">DS2API_CONFIG_JSON</code>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 导入区域 */}
|
||||
<div className="card">
|
||||
<div className="card-title" style={{ marginBottom: '1rem' }}>📦 批量导入</div>
|
||||
{/* Editor Panel */}
|
||||
<div className="md:col-span-2 flex flex-col bg-card border border-border rounded-xl shadow-sm overflow-hidden h-full">
|
||||
<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 Editor
|
||||
</h3>
|
||||
<div className="flex gap-2">
|
||||
<button onClick={handleExport} className="btn btn-secondary text-xs h-8">
|
||||
Load Current
|
||||
</button>
|
||||
<button onClick={handleImport} disabled={loading} className="btn btn-primary text-xs h-8">
|
||||
{loading ? 'Importing...' : 'Simulate Import'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="form-label">JSON 配置(点击上方模板快速填充)</label>
|
||||
<div className="flex-1 relative">
|
||||
<textarea
|
||||
className="form-input"
|
||||
style={{ minHeight: '200px' }}
|
||||
className="absolute inset-0 w-full h-full p-4 font-mono text-sm bg-secondary/10 resize-none focus:outline-none custom-scrollbar"
|
||||
value={jsonInput}
|
||||
onChange={e => setJsonInput(e.target.value)}
|
||||
placeholder='{\n "keys": ["你的API密钥"],\n "accounts": [\n {"email": "邮箱", "password": "密码", "token": ""}\n ]\n}'
|
||||
placeholder={'{\n "keys": ["your-api-key"],\n "accounts": [\n {"email": "...", "password": "...", "token": ""}\n ]\n}'}
|
||||
spellCheck={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="btn-group" style={{ marginBottom: '1rem' }}>
|
||||
<button className="btn btn-secondary" onClick={handleExport}>
|
||||
⬇️ 导出当前
|
||||
</button>
|
||||
<button className="btn btn-primary" onClick={handleImport} disabled={loading}>
|
||||
{loading ? <span className="loading"></span> : '📥 导入配置'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{result && (
|
||||
<div className="alert alert-success">
|
||||
✅ 导入完成:{result.imported_keys} 个 API Key,{result.imported_accounts} 个账号
|
||||
<div className={clsx(
|
||||
"p-4 border-t",
|
||||
result.imported_keys || result.imported_accounts ? "bg-emerald-500/10 border-emerald-500/20" : "bg-destructive/10 border-destructive/20"
|
||||
)}>
|
||||
<div className="flex items-start gap-3">
|
||||
{result.imported_keys || result.imported_accounts ? (
|
||||
<Check className="w-5 h-5 text-emerald-500 mt-0.5" />
|
||||
) : (
|
||||
<AlertTriangle className="w-5 h-5 text-destructive mt-0.5" />
|
||||
)}
|
||||
<div>
|
||||
<h4 className={clsx("font-medium", result.imported_keys || result.imported_accounts ? "text-emerald-500" : "text-destructive")}>
|
||||
Import Operation Completed
|
||||
</h4>
|
||||
<p className="text-sm opacity-80 mt-1">
|
||||
Successfully imported {result.imported_keys} API keys and updated {result.imported_accounts} accounts.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="card">
|
||||
<div className="card-title" style={{ marginBottom: '1rem' }}>📤 导出 Base64</div>
|
||||
<p style={{ color: 'var(--text-secondary)', marginBottom: '1rem' }}>
|
||||
导出 Base64 格式配置,可直接粘贴到 Vercel 环境变量 <code>DS2API_CONFIG_JSON</code>
|
||||
</p>
|
||||
<button className="btn btn-success" onClick={copyBase64}>
|
||||
📋 复制 Base64 到剪贴板
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user