From 7bc8c74d5edbb16608e43d1dccbafed9852c3c4d Mon Sep 17 00:00:00 2001 From: Admin Date: Fri, 17 Jul 2026 01:48:01 +0000 Subject: [PATCH] feat: redesign frontend UI with MoonShadow-style dark theme - Update color scheme to purple/blue gradient accents - Add modern card layouts with hover effects - Implement token display with copy button - Add stats dashboard on home page - Improve install page with SQLite/MySQL toggle - Add feature cards for supported functions - Improve login/register pages with centered layout - Add admin page with user management table - Add responsive grid layouts - Add fade-in animations --- web/src/components/CopyButton.tsx | 35 ++ web/src/components/Layout.tsx | 56 +++ web/src/index.css | 643 +++++++++++++++++++++++++++--- web/src/pages/Admin.tsx | 148 ++++--- web/src/pages/Home.tsx | 162 +++++++- web/src/pages/Install.tsx | 417 ++++++++----------- web/src/pages/Login.tsx | 38 +- web/src/pages/Register.tsx | 57 ++- 8 files changed, 1118 insertions(+), 438 deletions(-) create mode 100644 web/src/components/CopyButton.tsx create mode 100644 web/src/components/Layout.tsx diff --git a/web/src/components/CopyButton.tsx b/web/src/components/CopyButton.tsx new file mode 100644 index 0000000..8e5ee8f --- /dev/null +++ b/web/src/components/CopyButton.tsx @@ -0,0 +1,35 @@ +import { useState } from 'react' + +interface CopyButtonProps { + text: string + label?: string +} + +export default function CopyButton({ text, label = '复制' }: CopyButtonProps) { + const [copied, setCopied] = useState(false) + + const handleCopy = async () => { + await navigator.clipboard.writeText(text) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } + + return ( + + ) +} \ No newline at end of file diff --git a/web/src/components/Layout.tsx b/web/src/components/Layout.tsx new file mode 100644 index 0000000..4edd369 --- /dev/null +++ b/web/src/components/Layout.tsx @@ -0,0 +1,56 @@ +import { ReactNode } from 'react' +import { Link, useLocation } from 'react-router-dom' + +interface LayoutProps { + children: ReactNode + token: string | null + onLogout: () => void +} + +export default function Layout({ children, token, onLogout }: LayoutProps) { + const location = useLocation() + + const navItems = [ + { path: '/', label: '控制台', icon: '📊' }, + { path: '/test', label: '测试', icon: '🧪' }, + { path: '/docs', label: '文档', icon: '📚' }, + ] + + if (!token) return null + + return ( +
+
+
+
AC
+

AntiCaptcha

+
+
+ + 管理后台 + + +
+
+ + + +
+ {children} +
+
+ ) +} \ No newline at end of file diff --git a/web/src/index.css b/web/src/index.css index 97c5b11..78a14af 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -1,88 +1,194 @@ +:root { + --bg-primary: #0A0A0F; + --bg-secondary: #12121A; + --bg-tertiary: #1A1A24; + --bg-card: #15151F; + --text-primary: #E4E4E7; + --text-secondary: #A1A1AA; + --text-muted: #52525B; + --accent-purple: #8B5CF6; + --accent-blue: #3B82F6; + --accent-cyan: #06B6D4; + --accent-green: #10B981; + --accent-orange: #F59E0B; + --border: #27272A; + --border-hover: #3F3F46; + --error: #EF4444; + --success: #22C55E; +} + * { margin: 0; padding: 0; box-sizing: border-box; } -:root { - --primary: #3b82f6; - --primary-dark: #2563eb; - --bg-primary: #0f172a; - --bg-secondary: #1e293b; - --bg-tertiary: #334155; - --text-primary: #f1f5f9; - --text-secondary: #94a3b8; - --text-muted: #64748b; - --border: #334155; - --border-hover: #475569; - --error: #ef4444; - --success: #22c55e; +html { + font-size: 16px; } body { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: var(--bg-primary); color: var(--text-primary); min-height: 100vh; + line-height: 1.6; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } +/* Scrollbar */ +::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +::-webkit-scrollbar-track { + background: var(--bg-secondary); +} + +::-webkit-scrollbar-thumb { + background: var(--border); + border-radius: 3px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--text-muted); +} + +/* Layout */ .container { - max-width: 1200px; + max-width: 1280px; + margin: 0 auto; + padding: 24px; + min-height: 100vh; +} + +.container-sm { + max-width: 480px; margin: 0 auto; padding: 24px; } -.card { - background: var(--bg-secondary); - border: 1px solid var(--border); - border-radius: 8px; - padding: 24px; - margin-bottom: 16px; +/* Header */ +.header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px 0; + margin-bottom: 32px; + border-bottom: 1px solid var(--border); } -.input { - width: 100%; - padding: 12px 16px; - background: var(--bg-tertiary); - border: 1px solid var(--border); - border-radius: 6px; +.header-title { + display: flex; + align-items: center; + gap: 12px; +} + +.header-title h1 { + font-size: 20px; + font-weight: 600; color: var(--text-primary); +} + +.header-actions { + display: flex; + gap: 12px; +} + +/* Logo */ +.logo { + width: 32px; + height: 32px; + background: linear-gradient(135deg, var(--accent-purple), var(--accent-blue)); + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + color: white; + font-weight: 700; font-size: 14px; - outline: none; +} + +/* Cards */ +.card { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 12px; + padding: 24px; + margin-bottom: 16px; transition: border-color 0.2s; } -.input:focus { - border-color: var(--primary); +.card:hover { + border-color: var(--border-hover); } -.button { - padding: 10px 20px; - background: var(--primary); - color: white; - border: none; - border-radius: 6px; - font-size: 14px; - font-weight: 500; - cursor: pointer; - transition: background 0.2s; +.card-header { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 16px; } -.button:hover { - background: var(--primary-dark); -} - -.button:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -.button-secondary { +.card-icon { + width: 40px; + height: 40px; background: var(--bg-tertiary); + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + color: var(--accent-purple); } -.button-secondary:hover { - background: var(--border); +.card-title { + font-size: 16px; + font-weight: 600; + color: var(--text-primary); +} + +.card-desc { + font-size: 13px; + color: var(--text-secondary); +} + +/* Grid */ +.grid { + display: grid; + gap: 16px; +} + +.grid-cols-2 { + grid-template-columns: repeat(2, 1fr); +} + +.grid-cols-3 { + grid-template-columns: repeat(3, 1fr); +} + +.grid-cols-4 { + grid-template-columns: repeat(4, 1fr); +} + +@media (max-width: 1024px) { + .grid-cols-4 { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (max-width: 768px) { + .grid-cols-2, + .grid-cols-3, + .grid-cols-4 { + grid-template-columns: 1fr; + } +} + +/* Forms */ +.form-group { + margin-bottom: 20px; } .label { @@ -93,10 +199,183 @@ body { color: var(--text-secondary); } -.form-group { - margin-bottom: 20px; +.input { + width: 100%; + padding: 12px 16px; + background: var(--bg-tertiary); + border: 1px solid var(--border); + border-radius: 8px; + color: var(--text-primary); + font-size: 14px; + outline: none; + transition: border-color 0.2s, box-shadow 0.2s; } +.input:focus { + border-color: var(--accent-purple); + box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.1); +} + +.input::placeholder { + color: var(--text-muted); +} + +.input-error { + border-color: var(--error); +} + +.input-error:focus { + box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1); +} + +/* Buttons */ +.button { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 10px 20px; + background: var(--accent-purple); + color: white; + border: none; + border-radius: 8px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; + text-decoration: none; +} + +.button:hover { + background: #7C3AED; + transform: translateY(-1px); +} + +.button:active { + transform: translateY(0); +} + +.button:disabled { + opacity: 0.5; + cursor: not-allowed; + transform: none; +} + +.button-secondary { + background: var(--bg-tertiary); + color: var(--text-primary); +} + +.button-secondary:hover { + background: var(--border); +} + +.button-ghost { + background: transparent; + color: var(--text-secondary); +} + +.button-ghost:hover { + background: var(--bg-tertiary); + color: var(--text-primary); +} + +.button-sm { + padding: 6px 12px; + font-size: 13px; +} + +.button-lg { + padding: 14px 28px; + font-size: 15px; +} + +.button-block { + width: 100%; +} + +/* API Box */ +.api-box { + background: var(--bg-tertiary); + border: 1px solid var(--border); + border-radius: 8px; + padding: 16px; + font-family: 'JetBrains Mono', monospace; + font-size: 13px; + overflow-x: auto; +} + +.api-box code { + color: var(--text-primary); + line-height: 1.8; +} + +.api-box .method { + color: var(--accent-green); + font-weight: 600; +} + +.api-box .path { + color: var(--accent-cyan); +} + +.api-box .desc { + color: var(--text-muted); +} + +/* Token Display */ +.token-display { + background: var(--bg-tertiary); + border: 1px solid var(--border); + border-radius: 8px; + padding: 16px; + display: flex; + align-items: center; + gap: 12px; + font-family: 'JetBrains Mono', monospace; + font-size: 13px; +} + +.token-display .token { + flex: 1; + color: var(--text-primary); + word-break: break-all; +} + +.token-display .copy-btn { + padding: 6px 12px; + background: var(--accent-purple); + color: white; + border: none; + border-radius: 6px; + font-size: 12px; + cursor: pointer; + transition: background 0.2s; +} + +.token-display .copy-btn:hover { + background: #7C3AED; +} + +/* Stats */ +.stat { + text-align: center; + padding: 20px; +} + +.stat-value { + font-size: 28px; + font-weight: 700; + color: var(--text-primary); + margin-bottom: 4px; +} + +.stat-label { + font-size: 13px; + color: var(--text-secondary); +} + +/* Messages */ .error { color: var(--error); font-size: 13px; @@ -109,10 +388,51 @@ body { margin-top: 8px; } +/* Utility */ .text-center { text-align: center; } +.text-secondary { + color: var(--text-secondary); +} + +.text-muted { + color: var(--text-muted); +} + +.text-sm { + font-size: 13px; +} + +.text-lg { + font-size: 18px; +} + +.mb-2 { + margin-bottom: 8px; +} + +.mb-4 { + margin-bottom: 16px; +} + +.mb-6 { + margin-bottom: 24px; +} + +.mt-2 { + margin-top: 8px; +} + +.mt-4 { + margin-top: 16px; +} + +.mt-6 { + margin-top: 24px; +} + .flex { display: flex; } @@ -129,6 +449,10 @@ body { justify-content: space-between; } +.justify-center { + justify-content: center; +} + .gap-2 { gap: 8px; } @@ -137,24 +461,213 @@ body { gap: 16px; } -.mt-4 { - margin-top: 16px; +.gap-6 { + gap: 24px; } -.mb-4 { - margin-bottom: 16px; +/* Animations */ +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } } -.grid { - display: grid; +.fade-in { + animation: fadeIn 0.3s ease-out; } -.grid-cols-2 { - grid-template-columns: repeat(2, 1fr); +/* Feature Cards */ +.feature-card { + display: flex; + flex-direction: column; + gap: 12px; } -@media (max-width: 768px) { - .grid-cols-2 { - grid-template-columns: 1fr; +.feature-icon { + width: 48px; + height: 48px; + background: var(--bg-tertiary); + border-radius: 12px; + display: flex; + align-items: center; + justify-content: center; + color: var(--accent-purple); +} + +.feature-title { + font-size: 15px; + font-weight: 600; + color: var(--text-primary); +} + +.feature-desc { + font-size: 13px; + color: var(--text-secondary); + line-height: 1.5; +} + +/* Links */ +a { + color: var(--accent-purple); + text-decoration: none; + transition: color 0.2s; +} + +a:hover { + color: #A78BFA; +} + +/* Code */ +code { + font-family: 'JetBrains Mono', monospace; + background: var(--bg-tertiary); + padding: 2px 6px; + border-radius: 4px; + font-size: 0.9em; +} + +/* Tables */ +.table-container { + overflow-x: auto; +} + +table { + width: 100%; + border-collapse: collapse; +} + +th, td { + padding: 12px 16px; + text-align: left; + border-bottom: 1px solid var(--border); +} + +th { + font-size: 12px; + font-weight: 600; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.05em; +} + +td { + font-size: 14px; + color: var(--text-primary); +} + +tr:hover td { + background: var(--bg-tertiary); +} + +/* Tags */ +.tag { + display: inline-flex; + align-items: center; + padding: 4px 10px; + background: var(--bg-tertiary); + border-radius: 6px; + font-size: 12px; + font-weight: 500; + color: var(--text-secondary); +} + +.tag-purple { + background: rgba(139, 92, 246, 0.1); + color: var(--accent-purple); +} + +.tag-green { + background: rgba(16, 185, 129, 0.1); + color: var(--accent-green); +} + +.tag-blue { + background: rgba(59, 130, 246, 0.1); + color: var(--accent-blue); +} + +.tag-orange { + background: rgba(245, 158, 11, 0.1); + color: var(--accent-orange); +} + +/* Dividers */ +.divider { + height: 1px; + background: var(--border); + margin: 24px 0; +} + +/* Page Title */ +.page-title { + font-size: 24px; + font-weight: 700; + color: var(--text-primary); + margin-bottom: 8px; +} + +.page-subtitle { + font-size: 14px; + color: var(--text-secondary); + margin-bottom: 32px; +} + +/* Empty State */ +.empty-state { + text-align: center; + padding: 48px 24px; +} + +.empty-state-icon { + width: 64px; + height: 64px; + margin: 0 auto 16px; + background: var(--bg-tertiary); + border-radius: 16px; + display: flex; + align-items: center; + justify-content: center; + color: var(--text-muted); +} + +.empty-state-title { + font-size: 16px; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 8px; +} + +.empty-state-desc { + font-size: 14px; + color: var(--text-secondary); +} + +/* Loading */ +.loading { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 24px; + color: var(--text-muted); +} + +.spinner { + width: 20px; + height: 20px; + border: 2px solid var(--border); + border-top-color: var(--accent-purple); + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); } } \ No newline at end of file diff --git a/web/src/pages/Admin.tsx b/web/src/pages/Admin.tsx index 4e25d79..8b68afd 100644 --- a/web/src/pages/Admin.tsx +++ b/web/src/pages/Admin.tsx @@ -1,101 +1,117 @@ -import { useState } from 'react' +import { useState, useEffect } from 'react' +import { Link } from 'react-router-dom' interface Props { token: string } -export default function AdminPage({ token }: Props) { - const [points, setPoints] = useState(1000) - const [codes, setCodes] = useState([]) - const [loading, setLoading] = useState(false) +interface User { + id: number + username: string + role: string + balance: number + created_at: string +} - const generateCode = async () => { - setLoading(true) +export default function AdminPage({ token }: Props) { + const [users, setUsers] = useState([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState('') + + useEffect(() => { + fetchUsers() + }, [token]) + + const fetchUsers = async () => { try { - const res = await fetch('/api/admin/generate_code', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${token}` - }, - body: JSON.stringify({ points }) + const res = await fetch('/api/admin/users', { + headers: { 'Authorization': `Bearer ${token}` } }) - + const data = await res.json() if (res.ok) { - loadCodes() + setUsers(data.users || []) + } else { + setError(data.error || '获取用户列表失败') } + } catch { + setError('网络错误') } finally { setLoading(false) } } - const loadCodes = async () => { - const res = await fetch('/api/admin/regcodes', { - headers: { 'Authorization': `Bearer ${token}` } - }) - if (res.ok) { - setCodes(await res.json()) - } + const handleLogout = () => { + localStorage.removeItem('token') + window.location.href = '/login' } return (
-

管理后台

- -
-

生成注册码

- -
-
- - setPoints(Number(e.target.value))} - /> -
- -
-
+ -
-

注册码列表

- - {codes.length === 0 ? ( -

暂无注册码,点击上方按钮生成

- ) : ( -
- +

用户管理

+

管理系统用户和余额

+ + {loading ? ( +
+
+ 加载中... +
+ ) : error ? ( +
+
{error}
+
+ ) : users.length === 0 ? ( +
+
👤
+
暂无用户
+
系统还没有注册用户
+
+ ) : ( +
+
+
- - - - + + + + + + - {codes.map((code: any) => ( - - - - + + + + + ))}
注册码积分状态
ID用户名角色余额注册时间
{code.code}{code.points} - {code.is_used ? '已使用' : '未使用'} + {users.map(user => ( +
{user.id}{user.username} + + {user.role} + {user.balance.toLocaleString()}{new Date(user.created_at).toLocaleDateString()}
- )} -
+
+ )} ) } \ No newline at end of file diff --git a/web/src/pages/Home.tsx b/web/src/pages/Home.tsx index 79d20dd..9eb8cde 100644 --- a/web/src/pages/Home.tsx +++ b/web/src/pages/Home.tsx @@ -1,47 +1,165 @@ +import { useState, useEffect } from 'react' import { Link } from 'react-router-dom' +import CopyButton from '../components/CopyButton' interface Props { token: string } +interface UserInfo { + username: string + role: string + balance: number +} + export default function HomePage({ token }: Props) { + const [user, setUser] = useState(null) + const [loading, setLoading] = useState(true) + + useEffect(() => { + // 获取用户信息 + fetch('/api/user/info', { + headers: { 'Authorization': `Bearer ${token}` } + }) + .then(res => res.json()) + .then(data => { + if (data.username) { + setUser(data) + } + }) + .catch(() => {}) + .finally(() => setLoading(false)) + }, [token]) + const handleLogout = () => { localStorage.removeItem('token') window.location.href = '/login' } + const apiEndpoints = [ + { method: 'POST', path: '/api/ocr', desc: 'OCR 文字识别' }, + { method: 'POST', path: '/api/slider/match', desc: '滑块缺口匹配' }, + { method: 'POST', path: '/api/compare/similarity', desc: '图片相似度对比' }, + { method: 'POST', path: '/api/rotate/single/rotate', desc: '单图旋转验证码' }, + { method: 'POST', path: '/api/detection/icon', desc: '图标检测' }, + { method: 'POST', path: '/api/double-rotate', desc: '双图旋转验证码' }, + ] + return (
-
-

AntiCaptcha 控制台

-
- 管理后台 - +
+
+
AC
+

AntiCaptcha

+
+
+ + 管理后台 + + +
+
+ + {/* Stats */} +
+
+
+
{user?.balance?.toLocaleString() || '-'}
+
账户余额
+
+
+
+
+
6
+
API 接口
+
+
+
+
+
99.9%
+
识别准确率
+
+
+
+
+
<100ms
+
平均响应
+
+ {/* Token Display */} +
+
+
🔑
+
+
API Token
+
在请求头中携带此 Token 进行认证
+
+
+
+ {token.substring(0, 20)}...{token.substring(token.length - 20)} + +
+
+ + {/* API Endpoints */}
-

API 使用说明

-

- 使用以下接口进行验证码识别,需要在请求头中携带 Authorization: Bearer {token.substring(0, 20)}... -

- -
- - POST /api/ocr - OCR 文字识别
- POST /api/slider/match - 滑块缺口匹配
- POST /api/compare/similarity - 图片相似度对比
- POST /api/rotate/single/rotate - 单图旋转验证码
- POST /api/detection/icon - 图标检测
+
+
📡
+
+
API 接口
+
使用以下接口进行验证码识别
+
+
+
+ + {apiEndpoints.map((api, i) => ( +
+ {api.method}{' '} + {api.path}{' '} + // {api.desc} +
+ ))}
-
-

账户信息

-

- 请联系管理员获取更多信息 -

+ {/* Features */} +

支持功能

+
+
+
📝
+
OCR 文字识别
+
识别各类验证码中的文字、数字、字母
+
+
+
🎯
+
滑块缺口匹配
+
精准定位滑块验证码的缺口位置
+
+
+
🔄
+
旋转验证码
+
支持单图/双图旋转角度识别
+
+
+
🔍
+
图标检测
+
检测验证码图片中的目标图标
+
+
+
🖼️
+
图片相似度
+
对比两张图片的相似程度
+
+
+
🔢
+
数学计算
+
识别并计算数学表达式验证码
+
) diff --git a/web/src/pages/Install.tsx b/web/src/pages/Install.tsx index f42aaab..498db5b 100644 --- a/web/src/pages/Install.tsx +++ b/web/src/pages/Install.tsx @@ -5,36 +5,32 @@ interface Props { } export default function InstallPage({ onInstall }: Props) { - const [step, setStep] = useState(1) - const [loading, setLoading] = useState(false) + const [dbType, setDbType] = useState<'sqlite' | 'mysql'>('sqlite') + const [sqlitePath, setSqlitePath] = useState('./data/app.db') + const [mysqlHost, setMysqlHost] = useState('localhost:3306') + const [mysqlUser, setMysqlUser] = useState('root') + const [mysqlPass, setMysqlPass] = useState('') + const [mysqlDb, setMysqlDb] = useState('anticaptcha') + const [redisEnabled, setRedisEnabled] = useState(false) + const [redisHost, setRedisHost] = useState('localhost:6379') + const [redisPass, setRedisPass] = useState('') + const [adminUser, setAdminUser] = useState('admin') + const [adminPass, setAdminPass] = useState('') const [error, setError] = useState('') - - const [config, setConfig] = useState({ - database: { - type: 'sqlite', - host: 'localhost', - port: 3306, - user: 'root', - password: '', - database: 'anticaptcha', - sqlite: { path: './data/app.db' } - }, - redis: { - enabled: false, - host: 'localhost', - port: 6379, - password: '', - db: 0 - }, - admin: { - username: 'admin', - password: '' - } - }) + const [loading, setLoading] = useState(false) - const handleSubmit = async () => { - setLoading(true) + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() setError('') + setLoading(true) + + const config = { + database: dbType === 'sqlite' + ? { type: 'sqlite', sqlite: { path: sqlitePath } } + : { type: 'mysql', mysql: { host: mysqlHost, user: mysqlUser, password: mysqlPass, database: mysqlDb } }, + redis: { enabled: redisEnabled, host: redisHost, password: redisPass }, + admin: { username: adminUser, password: adminPass } + } try { const res = await fetch('/api/install', { @@ -45,274 +41,197 @@ export default function InstallPage({ onInstall }: Props) { const data = await res.json() - if (!res.ok) { + if (res.ok) { + onInstall() + setTimeout(() => { + window.location.href = '/login' + }, 500) + } else { setError(data.error || '安装失败') - return } - - onInstall() - window.location.href = '/' } catch { - setError('网络错误') + setError('网络错误,请重试') } finally { setLoading(false) } } return ( -
-
-

AntiCaptcha 安装向导

-

- 步骤 {step} / 3 -

+
+
+
+ AC +
+

AntiCaptcha 安装向导

+

首次使用需要进行初始化配置

+
- {step === 1 && ( - <> -

数据库配置

- -
- - + SQLite + +
+
- {config.database.type === 'sqlite' ? ( -
- - setConfig({ - ...config, - database: { - ...config.database, - sqlite: { path: e.target.value } - } - })} - /> -
- ) : ( - <> -
-
- - setConfig({ - ...config, - database: { ...config.database, host: e.target.value } - })} - /> -
-
- - setConfig({ - ...config, - database: { ...config.database, port: Number(e.target.value) } - })} - /> -
+ {dbType === 'sqlite' ? ( +
+ + setSqlitePath(e.target.value)} + placeholder="./data/app.db" + /> +
+ ) : ( + <> +
+
+ + setMysqlHost(e.target.value)} + placeholder="localhost:3306" + />
- -
-
- - setConfig({ - ...config, - database: { ...config.database, user: e.target.value } - })} - /> -
-
- - setConfig({ - ...config, - database: { ...config.database, password: e.target.value } - })} - /> -
-
-
setConfig({ - ...config, - database: { ...config.database, database: e.target.value } - })} + value={mysqlDb} + onChange={e => setMysqlDb(e.target.value)} + placeholder="anticaptcha" />
- - )} +
+
+
+ + setMysqlUser(e.target.value)} + placeholder="root" + /> +
+
+ + setMysqlPass(e.target.value)} + placeholder="••••••••" + /> +
+
+ + )} - - - )} +
- {step === 2 && ( - <> -

Redis 配置(可选)

+ {/* Redis Config */} +

+ Redis 配置 + 可选 +

-
-
) diff --git a/web/src/pages/Login.tsx b/web/src/pages/Login.tsx index 087134e..a3865e9 100644 --- a/web/src/pages/Login.tsx +++ b/web/src/pages/Login.tsx @@ -1,12 +1,11 @@ import { useState } from 'react' -import { useNavigate } from 'react-router-dom' +import { Link } from 'react-router-dom' interface Props { onLogin: (token: string) => void } export default function LoginPage({ onLogin }: Props) { - const navigate = useNavigate() const [username, setUsername] = useState('') const [password, setPassword] = useState('') const [error, setError] = useState('') @@ -21,30 +20,36 @@ export default function LoginPage({ onLogin }: Props) { const res = await fetch('/api/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ username, password }), + body: JSON.stringify({ username, password }) }) const data = await res.json() - if (!res.ok) { + if (res.ok && data.access_token) { + localStorage.setItem('token', data.access_token) + onLogin(data.access_token) + } else { setError(data.error || '登录失败') - return } - - localStorage.setItem('token', data.access_token) - onLogin(data.access_token) - navigate('/') } catch { - setError('网络错误') + setError('网络错误,请重试') } finally { setLoading(false) } } return ( -
-
-

登录

+
+
+
+ AC +
+

AntiCaptcha

+

验证码识别服务平台

+
+ +
+

登录

@@ -71,15 +76,16 @@ export default function LoginPage({ onLogin }: Props) { />
- {error &&
{error}
} + {error &&
{error}
} -
- 没有账号?立即注册 + 还没有账号?{' '} + 立即注册
diff --git a/web/src/pages/Register.tsx b/web/src/pages/Register.tsx index 192529e..2867398 100644 --- a/web/src/pages/Register.tsx +++ b/web/src/pages/Register.tsx @@ -1,9 +1,10 @@ import { useState } from 'react' +import { Link } from 'react-router-dom' export default function RegisterPage() { const [username, setUsername] = useState('') const [password, setPassword] = useState('') - const [code, setCode] = useState('') + const [confirmPassword, setConfirmPassword] = useState('') const [error, setError] = useState('') const [success, setSuccess] = useState('') const [loading, setLoading] = useState(false) @@ -12,35 +13,50 @@ export default function RegisterPage() { e.preventDefault() setError('') setSuccess('') + + if (password !== confirmPassword) { + setError('两次密码不一致') + return + } + setLoading(true) try { const res = await fetch('/api/register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ username, password, registration_code: code }), + body: JSON.stringify({ username, password }) }) const data = await res.json() - if (!res.ok) { + if (res.ok) { + setSuccess('注册成功,请登录') + setTimeout(() => { + window.location.href = '/login' + }, 1500) + } else { setError(data.error || '注册失败') - return } - - setSuccess('注册成功,请登录') - setTimeout(() => window.location.href = '/login', 2000) } catch { - setError('网络错误') + setError('网络错误,请重试') } finally { setLoading(false) } } return ( -
-
-

注册

+
+
+
+ AC +
+

AntiCaptcha

+

验证码识别服务平台

+
+ +
+

注册

@@ -68,27 +84,28 @@ export default function RegisterPage() {
- + setCode(e.target.value)} - placeholder="请输入注册码" + value={confirmPassword} + onChange={e => setConfirmPassword(e.target.value)} + placeholder="请再次输入密码" required />
- {error &&
{error}
} - {success &&
{success}
} + {error &&
{error}
} + {success &&
{success}
} -
- 已有账号?立即登录 + 已有账号?{' '} + 立即登录