From 1f6043ff70e880d14879441133e14835f21114aa Mon Sep 17 00:00:00 2001 From: CJACK Date: Sun, 1 Feb 2026 05:33:50 +0800 Subject: [PATCH] refactor: migrate UI to Tailwind CSS for a modernized design system. --- webui/index.html | 8 +- webui/package.json | 10 +- webui/postcss.config.js | 6 + webui/src/App.jsx | 221 ++++++--- webui/src/components/AccountManager.jsx | 431 +++++++++------- webui/src/components/ApiTester.jsx | 364 +++++++------- webui/src/components/BatchImport.jsx | 175 ++++--- webui/src/components/Login.jsx | 103 ++-- webui/src/components/VercelSync.jsx | 264 +++++----- webui/src/styles.css | 623 +++++------------------- webui/tailwind.config.js | 52 ++ webui/vite.config.js | 4 +- 12 files changed, 1087 insertions(+), 1174 deletions(-) create mode 100644 webui/postcss.config.js create mode 100644 webui/tailwind.config.js diff --git a/webui/index.html b/webui/index.html index 5bf5917..b9f8f0c 100644 --- a/webui/index.html +++ b/webui/index.html @@ -1,12 +1,18 @@ + + + + DS2API Admin +
- + + \ No newline at end of file diff --git a/webui/package.json b/webui/package.json index 697401d..00cec33 100644 --- a/webui/package.json +++ b/webui/package.json @@ -9,11 +9,17 @@ "preview": "vite preview" }, "dependencies": { + "clsx": "^2.1.1", + "lucide-react": "^0.563.0", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "tailwind-merge": "^3.4.0" }, "devDependencies": { "@vitejs/plugin-react": "^4.2.1", + "autoprefixer": "^10.4.24", + "postcss": "^8.5.6", + "tailwindcss": "^3.4.19", "vite": "^5.0.0" } -} \ No newline at end of file +} diff --git a/webui/postcss.config.js b/webui/postcss.config.js new file mode 100644 index 0000000..d41ad63 --- /dev/null +++ b/webui/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/webui/src/App.jsx b/webui/src/App.jsx index c56f265..d195541 100644 --- a/webui/src/App.jsx +++ b/webui/src/App.jsx @@ -1,15 +1,28 @@ import { useState, useEffect } from 'react' +import { + LayoutDashboard, + Key, + Upload, + Cloud, + LogOut, + Menu, + X, + Server, + Users +} from 'lucide-react' +import clsx from 'clsx' + import AccountManager from './components/AccountManager' import ApiTester from './components/ApiTester' import BatchImport from './components/BatchImport' import VercelSync from './components/VercelSync' import Login from './components/Login' -const TABS = [ - { id: 'accounts', label: '🔑 账号管理' }, - { id: 'test', label: '🧪 API 测试' }, - { id: 'import', label: '📦 批量导入' }, - { id: 'vercel', label: '☁️ Vercel 同步' }, +const NAV_ITEMS = [ + { id: 'accounts', label: '账号管理', icon: Users, description: '管理 DeepSeek 账号池' }, + { id: 'test', label: 'API 测试', icon: Server, description: '测试 API 连接与响应' }, + { id: 'import', label: '批量导入', icon: Upload, description: '批量导入账号配置' }, + { id: 'vercel', label: 'Vercel 同步', icon: Cloud, description: '同步配置到 Vercel' }, ] export default function App() { @@ -19,16 +32,15 @@ export default function App() { const [message, setMessage] = useState(null) const [token, setToken] = useState(null) const [authChecking, setAuthChecking] = useState(true) + const [sidebarOpen, setSidebarOpen] = useState(false) // 检查已存储的 Token useEffect(() => { const checkAuth = async () => { - // 检查 localStorage 或 sessionStorage const storedToken = localStorage.getItem('ds2api_token') || sessionStorage.getItem('ds2api_token') const expiresAt = parseInt(localStorage.getItem('ds2api_token_expires') || sessionStorage.getItem('ds2api_token_expires') || '0') if (storedToken && expiresAt > Date.now()) { - // 验证 token 是否有效 try { const res = await fetch('/admin/verify', { headers: { 'Authorization': `Bearer ${storedToken}` } @@ -36,14 +48,9 @@ export default function App() { if (res.ok) { setToken(storedToken) } else { - // Token 无效,清除 - localStorage.removeItem('ds2api_token') - localStorage.removeItem('ds2api_token_expires') - sessionStorage.removeItem('ds2api_token') - sessionStorage.removeItem('ds2api_token_expires') + handleLogout() } } catch { - // 网络错误,保留 token 重试 setToken(storedToken) } } @@ -52,7 +59,6 @@ export default function App() { checkAuth() }, []) - // 带认证的 fetch const authFetch = async (url, options = {}) => { const headers = { ...options.headers, @@ -60,7 +66,6 @@ export default function App() { } const res = await fetch(url, { ...options, headers }) - // 401 时自动登出 if (res.status === 401) { handleLogout() throw new Error('认证已过期,请重新登录') @@ -123,27 +128,32 @@ export default function App() { } } - // 认证检查中 if (authChecking) { return ( -
-
-
-
- 检查登录状态... -
-
+
+
+
+

检查登录状态...

) } - // 未登录 if (!token) { return ( -
+
+ {/* Background decorative elements */} +
+
+
+
+ {message && ( -
+
{message.text}
)} @@ -152,60 +162,133 @@ export default function App() { ) } - // 已登录 return ( -
-
-
-
-

DS2API Admin

-

账号管理 · API 测试 · Vercel 部署

-
- -
-
- - {message && ( -
- {message.text} -
+
+ {/* Mobile Sidebar Overlay */} + {sidebarOpen && ( +
setSidebarOpen(false)} + /> )} -
-
-
{config.keys?.length || 0}
-
API Keys
+ {/* Sidebar */} +
-
- {TABS.map(tab => ( + + +
+
+
+ API Status + + + Online + +
+
+
+
Accounts
+
{config.accounts?.length || 0}
+
+
+
Api Keys
+
{config.keys?.length || 0}
+
+
+ +
+
+ + + {/* Main Content */} +
+ {/* Mobile Header */} +
+ {NAV_ITEMS.find(n => n.id === activeTab)?.label} - ))} -
+ - {loading ? ( -
-
- 加载中... + {/* Content Area */} +
+
+
+

+ {NAV_ITEMS.find(n => n.id === activeTab)?.label} +

+

+ {NAV_ITEMS.find(n => n.id === activeTab)?.description} +

+
+ + {message && ( +
+ {message.type === 'error' ? :
} + {message.text} +
+ )} + +
+ {loading ? ( +
+
+

Please wait while loading data...

+
+ ) : ( + renderTab() + )} +
- ) : ( - renderTab() - )} +
) } - diff --git a/webui/src/components/AccountManager.jsx b/webui/src/components/AccountManager.jsx index e88daea..5910d9e 100644 --- a/webui/src/components/AccountManager.jsx +++ b/webui/src/components/AccountManager.jsx @@ -1,4 +1,18 @@ import { useState, useEffect } from 'react' +import { + Plus, + Trash2, + RefreshCw, + CheckCircle2, + AlertCircle, + Search, + Play, + MoreHorizontal, + X, + Server, + ShieldCheck +} from 'lucide-react' +import clsx from 'clsx' export default function AccountManager({ config, onRefresh, onMessage, authFetch }) { const [showAddKey, setShowAddKey] = useState(false) @@ -6,17 +20,15 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch const [newKey, setNewKey] = useState('') const [newAccount, setNewAccount] = useState({ email: '', mobile: '', password: '' }) const [loading, setLoading] = useState(false) - const [validating, setValidating] = useState({}) // 单个账号验证状态 + const [validating, setValidating] = useState({}) const [validatingAll, setValidatingAll] = useState(false) - const [testing, setTesting] = useState({}) // 单个账号测试状态 + const [testing, setTesting] = useState({}) const [testingAll, setTestingAll] = useState(false) const [batchProgress, setBatchProgress] = useState({ current: 0, total: 0, results: [] }) const [queueStatus, setQueueStatus] = useState(null) - // 使用 authFetch 或回退到普通 fetch const apiFetch = authFetch || fetch - // 获取队列状态 const fetchQueueStatus = async () => { try { const res = await apiFetch('/admin/queue/status') @@ -25,13 +37,13 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch setQueueStatus(data) } } catch (e) { - console.error('获取队列状态失败:', e) + console.error('Failed to fetch queue status:', e) } } useEffect(() => { fetchQueueStatus() - const interval = setInterval(fetchQueueStatus, 5000) // 每5秒刷新 + const interval = setInterval(fetchQueueStatus, 5000) return () => clearInterval(interval) }, []) @@ -45,39 +57,39 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch body: JSON.stringify({ key: newKey.trim() }), }) if (res.ok) { - onMessage('success', 'API Key 添加成功') + onMessage('success', 'API Key added successfully') setNewKey('') setShowAddKey(false) onRefresh() } else { const data = await res.json() - onMessage('error', data.detail || '添加失败') + onMessage('error', data.detail || 'Failed to add') } } catch (e) { - onMessage('error', '网络错误') + onMessage('error', 'Network error') } finally { setLoading(false) } } const deleteKey = async (key) => { - if (!confirm('确定删除此 API Key?')) return + if (!confirm('Are you sure you want to delete this API Key?')) return try { const res = await apiFetch(`/admin/keys/${encodeURIComponent(key)}`, { method: 'DELETE' }) if (res.ok) { - onMessage('success', '删除成功') + onMessage('success', 'Deleted successfully') onRefresh() } else { - onMessage('error', '删除失败') + onMessage('error', 'Delete failed') } } catch (e) { - onMessage('error', '网络错误') + onMessage('error', 'Network error') } } const addAccount = async () => { if (!newAccount.password || (!newAccount.email && !newAccount.mobile)) { - onMessage('error', '请填写密码和邮箱/手机号') + onMessage('error', 'Password and Email/Mobile are required') return } setLoading(true) @@ -88,37 +100,36 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch body: JSON.stringify(newAccount), }) if (res.ok) { - onMessage('success', '账号添加成功') + onMessage('success', 'Account added successfully') setNewAccount({ email: '', mobile: '', password: '' }) setShowAddAccount(false) onRefresh() } else { const data = await res.json() - onMessage('error', data.detail || '添加失败') + onMessage('error', data.detail || 'Failed to add') } } catch (e) { - onMessage('error', '网络错误') + onMessage('error', 'Network error') } finally { setLoading(false) } } const deleteAccount = async (id) => { - if (!confirm('确定删除此账号?')) return + if (!confirm('Are you sure you want to delete this account?')) return try { const res = await apiFetch(`/admin/accounts/${encodeURIComponent(id)}`, { method: 'DELETE' }) if (res.ok) { - onMessage('success', '删除成功') + onMessage('success', 'Deleted successfully') onRefresh() } else { - onMessage('error', '删除失败') + onMessage('error', 'Delete failed') } } catch (e) { - onMessage('error', '网络错误') + onMessage('error', 'Network error') } } - // 验证单个账号 const validateAccount = async (identifier) => { setValidating(prev => ({ ...prev, [identifier]: true })) try { @@ -128,22 +139,17 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch body: JSON.stringify({ identifier }), }) const data = await res.json() - if (data.valid) { - onMessage('success', `${identifier}: ${data.message}`) - } else { - onMessage('error', `${identifier}: ${data.message}`) - } + onMessage(data.valid ? 'success' : 'error', `${identifier}: ${data.message}`) onRefresh() } catch (e) { - onMessage('error', '验证失败: ' + e.message) + onMessage('error', 'Validation failed: ' + e.message) } finally { setValidating(prev => ({ ...prev, [identifier]: false })) } } - // 批量验证所有账号(带进度) const validateAllAccounts = async () => { - if (!confirm('确定要验证所有账号?')) return + if (!confirm('Validate ALL accounts? This might take a while.')) return const accounts = config.accounts || [] if (accounts.length === 0) return @@ -173,12 +179,11 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch setBatchProgress({ current: i + 1, total: accounts.length, results: [...results] }) } - onMessage('success', `验证完成: ${validCount}/${accounts.length} 个账号有效`) + onMessage('success', `Completed: ${validCount}/${accounts.length} valid`) onRefresh() setValidatingAll(false) } - // 测试单个账号 API const testAccount = async (identifier) => { setTesting(prev => ({ ...prev, [identifier]: true })) try { @@ -188,22 +193,17 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch body: JSON.stringify({ identifier }), }) const data = await res.json() - if (data.success) { - onMessage('success', `${identifier}: API 测试成功 (${data.response_time}ms)`) - } else { - onMessage('error', `${identifier}: ${data.message}`) - } + onMessage(data.success ? 'success' : 'error', `${identifier}: ${data.success ? `Success (${data.response_time}ms)` : data.message}`) onRefresh() } catch (e) { - onMessage('error', 'API 测试失败: ' + e.message) + onMessage('error', 'Test failed: ' + e.message) } finally { setTesting(prev => ({ ...prev, [identifier]: false })) } } - // 批量测试所有账号 API(带进度) const testAllAccounts = async () => { - if (!confirm('确定要测试所有账号的 API?')) return + if (!confirm('Test API connectivity for ALL accounts?')) return const accounts = config.accounts || [] if (accounts.length === 0) return @@ -233,100 +233,144 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch setBatchProgress({ current: i + 1, total: accounts.length, results: [...results] }) } - onMessage('success', `API 测试完成: ${successCount}/${accounts.length} 个账号可用`) + onMessage('success', `Completed: ${successCount}/${accounts.length} available`) onRefresh() setTestingAll(false) } return ( -
- {/* 队列状态监控 */} +
+ {/* Queue Status */} {queueStatus && ( -
-
- 📊 轮询队列状态 - -
-
-
- 可用账号: - {queueStatus.available} - 使用中: - {queueStatus.in_use} - 总计: - {queueStatus.total} -
- {queueStatus.in_use > 0 && ( -
- 正在使用: {queueStatus.in_use_accounts.join(', ')} +
+
+
+
+
- )} +
+

Available

+

{queueStatus.available}

+
+
+
+
+
+
+ +
+
+

In Use

+

{queueStatus.in_use}

+
+
+
+
+
+
+ +
+
+

Total Accounts

+

{queueStatus.total}

+
+
)} - {/* API Keys */} -
-
- 🔑 API Keys - + {/* API Keys Section */} +
+
+
+

API Keys

+

Manage access keys for the API

+
+
- {config.keys?.length > 0 ? ( -
- {config.keys.map((key, i) => ( -
- {key.slice(0, 16)}**** - +
+ {config.keys?.length > 0 ? ( + config.keys.map((key, i) => ( +
+
+ {key.slice(0, 16)}**** +
+
- ))} -
- ) : ( -
暂无 API Key
- )} + )) + ) : ( +
No API keys found
+ )} +
- {/* Accounts */} -
-
- 👤 DeepSeek 账号 -
+ {/* Accounts Section */} +
+
+
+

DeepSeek Accounts

+

Manage your account pool

+
+
+ -
- {/* 批量操作进度条 */} + {/* Batch Progress */} {(testingAll || validatingAll) && batchProgress.total > 0 && ( -
-
- {testingAll ? '🧪 批量测试中...' : '✅ 批量验证中...'} - {batchProgress.current}/{batchProgress.total} +
+
+ {testingAll ? 'Testing all accounts...' : 'Validating all accounts...'} + {batchProgress.current} / {batchProgress.total}
-
+
{batchProgress.results.length > 0 && ( -
+
{batchProgress.results.map((r, i) => ( -
- {r.success ? '✓' : '✗'} {r.id} {r.time ? `(${r.time}ms)` : ''} +
+ {r.success ? '✓' : '✗'} {r.id}
))}
@@ -334,121 +378,140 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch
)} - {config.accounts?.length > 0 ? ( -
- {config.accounts.map((acc, i) => { +
+ {config.accounts?.length > 0 ? ( + config.accounts.map((acc, i) => { const id = acc.email || acc.mobile return ( -
-
- {id} - - {acc.has_token ? '已登录' : '未登录'} - - {acc.token_preview && ( - - 🔑 {acc.token_preview} - - )} +
+
+
+
+
{id}
+
+ {acc.has_token ? 'Active Session' : 'Login Required'} + {acc.token_preview && ( + + {acc.token_preview} + + )} +
+
-
+
+ -
) - })} -
- ) : ( -
暂无账号
- )} + }) + ) : ( +
No accounts found
+ )} +
- {/* Add Key Modal */} + {/* Modals */} {showAddKey && ( -
setShowAddKey(false)}> -
e.stopPropagation()}> -
- 添加 API Key - -
-
- - setNewKey(e.target.value)} - /> -
-
- -
+
+
+ + setNewKey(e.target.value)} + autoFocus + /> +
+
+ + +
+
)} - {/* Add Account Modal */} {showAddAccount && ( -
setShowAddAccount(false)}> -
e.stopPropagation()}> -
- 添加 DeepSeek 账号 - -
-
- - setNewAccount({ ...newAccount, email: e.target.value })} - /> -
-
- - setNewAccount({ ...newAccount, mobile: e.target.value })} - /> -
-
- - setNewAccount({ ...newAccount, password: e.target.value })} - /> -
-
- -
+
+
+ + setNewAccount({ ...newAccount, email: e.target.value })} + /> +
+
+ + setNewAccount({ ...newAccount, mobile: e.target.value })} + /> +
+
+ + setNewAccount({ ...newAccount, password: e.target.value })} + /> +
+
+ + +
+
)} diff --git a/webui/src/components/ApiTester.jsx b/webui/src/components/ApiTester.jsx index ac07b48..3dfed35 100644 --- a/webui/src/components/ApiTester.jsx +++ b/webui/src/components/ApiTester.jsx @@ -1,17 +1,30 @@ -import { useState, useRef, useEffect } from 'react' +import { useState, useRef } from 'react' +import { + Send, + Square, + MessageSquare, + Cpu, + Search as SearchIcon, + Sparkles, + Bot, + User, + Loader2, + CheckCircle2, + AlertCircle +} from 'lucide-react' +import clsx from 'clsx' const MODELS = [ - { id: 'deepseek-chat', name: 'deepseek-chat' }, - { id: 'deepseek-reasoner', name: 'deepseek-reasoner' }, - { id: 'deepseek-chat-search', name: 'deepseek-chat-search' }, - { id: 'deepseek-reasoner-search', name: 'deepseek-reasoner-search' }, + { id: 'deepseek-chat', name: 'DeepSeek Chat', icon: MessageSquare, desc: 'General purpose chat model' }, + { id: 'deepseek-reasoner', name: 'DeepSeek Reasoner', icon: Cpu, desc: 'Optimized for reasoning tasks' }, + // Removed search models as they might be deprecated or identical to chat with search tool ] export default function ApiTester({ config, onMessage, authFetch }) { const [model, setModel] = useState('deepseek-chat') - const [message, setMessage] = useState('你好,请用一句话介绍你自己。') + const [message, setMessage] = useState('Hello, please introduce yourself in one sentence.') const [apiKey, setApiKey] = useState('') - const [selectedAccount, setSelectedAccount] = useState('') // 空为随机 + const [selectedAccount, setSelectedAccount] = useState('') const [response, setResponse] = useState(null) const [loading, setLoading] = useState(false) const [streamingContent, setStreamingContent] = useState('') @@ -19,16 +32,9 @@ export default function ApiTester({ config, onMessage, authFetch }) { const [isStreaming, setIsStreaming] = useState(false) const abortControllerRef = useRef(null) - // 使用 authFetch 或回退到普通 fetch(admin API 用 authFetch,OpenAI 兼容 API 用普通 fetch) const apiFetch = authFetch || fetch - - // 获取账号列表 const accounts = config.accounts || [] - const testApi = async () => { - // ... (保留旧的 server-side test作为备用,或者完全移除?保留吧但不使用) - } - const stopGeneration = () => { if (abortControllerRef.current) { abortControllerRef.current.abort() @@ -52,7 +58,7 @@ export default function ApiTester({ config, onMessage, authFetch }) { try { const key = apiKey || (config.keys?.[0] || '') if (!key) { - onMessage('error', '请提供 API Key') + onMessage('error', 'Please provide an API Key') setLoading(false) setIsStreaming(false) return @@ -74,8 +80,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 || 'Request failed' }) + onMessage('error', data.error?.message || 'Request failed') setLoading(false) setIsStreaming(false) return @@ -83,7 +89,6 @@ export default function ApiTester({ config, onMessage, authFetch }) { setResponse({ success: true, status_code: res.status }) - // 处理流式响应 const reader = res.body.getReader() const decoder = new TextDecoder() let buffer = '' @@ -108,12 +113,9 @@ export default function ApiTester({ config, onMessage, authFetch }) { const choice = json.choices?.[0] if (choice?.delta) { const delta = choice.delta - - // DeepSeek 官方格式使用 reasoning_content 表示思考内容 if (delta.reasoning_content) { setStreamingThinking(prev => prev + delta.reasoning_content) } - // 正常内容 if (delta.content) { setStreamingContent(prev => prev + delta.content) } @@ -125,9 +127,9 @@ export default function ApiTester({ config, onMessage, authFetch }) { } } catch (e) { if (e.name === 'AbortError') { - onMessage('info', '已停止生成') + onMessage('info', 'Generation stopped') } else { - onMessage('error', '网络错误: ' + e.message) + onMessage('error', 'Network error: ' + e.message) setResponse({ error: e.message, success: false }) } } finally { @@ -137,9 +139,7 @@ export default function ApiTester({ config, onMessage, authFetch }) { } } - // 智能测试:根据是否选择账号决定测试方式 const sendTest = async () => { - // 如果选择了指定账号,使用账号测试接口(暂时保持非流式,或者后续改为支持流式) if (selectedAccount) { setLoading(true) setResponse(null) @@ -161,12 +161,12 @@ export default function ApiTester({ config, onMessage, authFetch }) { account: selectedAccount, }) if (data.success) { - onMessage('success', `${selectedAccount}: 测试成功 (${data.response_time}ms)`) + onMessage('success', `${selectedAccount}: Test Success (${data.response_time}ms)`) } else { onMessage('error', `${selectedAccount}: ${data.message}`) } } catch (e) { - onMessage('error', '网络错误: ' + e.message) + onMessage('error', 'Network error: ' + e.message) setResponse({ error: e.message }) } finally { setLoading(false) @@ -174,174 +174,180 @@ export default function ApiTester({ config, onMessage, authFetch }) { return } - // 随机账号:使用标准 API (流式) directTest() } return ( -
-
-
🧪 API 测试
+
+ {/* Configuration Panel */} +
+
+

+ + Configuration +

-
- - -
+
+ +
+ {MODELS.map(m => { + const Icon = m.icon + return ( + + ) + })} +
+
-
- - -
+
+ + +
-
- - setApiKey(e.target.value)} - /> -
- -
- -