diff --git a/web/backend/__pycache__/__init__.cpython-312.pyc b/web/backend/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 15c429f..0000000 Binary files a/web/backend/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/web/backend/__pycache__/database.cpython-312.pyc b/web/backend/__pycache__/database.cpython-312.pyc deleted file mode 100644 index 562d44a..0000000 Binary files a/web/backend/__pycache__/database.cpython-312.pyc and /dev/null differ diff --git a/web/backend/__pycache__/deps.cpython-312.pyc b/web/backend/__pycache__/deps.cpython-312.pyc deleted file mode 100644 index fae3d62..0000000 Binary files a/web/backend/__pycache__/deps.cpython-312.pyc and /dev/null differ diff --git a/web/backend/__pycache__/main.cpython-312.pyc b/web/backend/__pycache__/main.cpython-312.pyc deleted file mode 100644 index 483a9b2..0000000 Binary files a/web/backend/__pycache__/main.cpython-312.pyc and /dev/null differ diff --git a/web/backend/__pycache__/models.cpython-312.pyc b/web/backend/__pycache__/models.cpython-312.pyc deleted file mode 100644 index 1f10992..0000000 Binary files a/web/backend/__pycache__/models.cpython-312.pyc and /dev/null differ diff --git a/web/backend/__pycache__/permissions.cpython-312.pyc b/web/backend/__pycache__/permissions.cpython-312.pyc deleted file mode 100644 index 5375917..0000000 Binary files a/web/backend/__pycache__/permissions.cpython-312.pyc and /dev/null differ diff --git a/web/backend/__pycache__/schemas.cpython-312.pyc b/web/backend/__pycache__/schemas.cpython-312.pyc deleted file mode 100644 index 0239c89..0000000 Binary files a/web/backend/__pycache__/schemas.cpython-312.pyc and /dev/null differ diff --git a/web/backend/__pycache__/security.cpython-312.pyc b/web/backend/__pycache__/security.cpython-312.pyc deleted file mode 100644 index 691f993..0000000 Binary files a/web/backend/__pycache__/security.cpython-312.pyc and /dev/null differ diff --git a/web/backend/routers/__pycache__/__init__.cpython-312.pyc b/web/backend/routers/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index eecbfe6..0000000 Binary files a/web/backend/routers/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/web/backend/routers/__pycache__/accounts.cpython-312.pyc b/web/backend/routers/__pycache__/accounts.cpython-312.pyc deleted file mode 100644 index 7248339..0000000 Binary files a/web/backend/routers/__pycache__/accounts.cpython-312.pyc and /dev/null differ diff --git a/web/backend/routers/__pycache__/auth.cpython-312.pyc b/web/backend/routers/__pycache__/auth.cpython-312.pyc deleted file mode 100644 index 63b3864..0000000 Binary files a/web/backend/routers/__pycache__/auth.cpython-312.pyc and /dev/null differ diff --git a/web/backend/routers/__pycache__/login.cpython-312.pyc b/web/backend/routers/__pycache__/login.cpython-312.pyc deleted file mode 100644 index 3f5eaf1..0000000 Binary files a/web/backend/routers/__pycache__/login.cpython-312.pyc and /dev/null differ diff --git a/web/backend/routers/__pycache__/proxy.cpython-312.pyc b/web/backend/routers/__pycache__/proxy.cpython-312.pyc deleted file mode 100644 index 51b9606..0000000 Binary files a/web/backend/routers/__pycache__/proxy.cpython-312.pyc and /dev/null differ diff --git a/web/backend/routers/__pycache__/users.cpython-312.pyc b/web/backend/routers/__pycache__/users.cpython-312.pyc deleted file mode 100644 index 109c854..0000000 Binary files a/web/backend/routers/__pycache__/users.cpython-312.pyc and /dev/null differ diff --git a/web/backend/services/__pycache__/__init__.cpython-312.pyc b/web/backend/services/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 3d3e53c..0000000 Binary files a/web/backend/services/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/web/backend/services/__pycache__/login_service.cpython-312.pyc b/web/backend/services/__pycache__/login_service.cpython-312.pyc deleted file mode 100644 index f7b5690..0000000 Binary files a/web/backend/services/__pycache__/login_service.cpython-312.pyc and /dev/null differ diff --git a/web/frontend/src/App.tsx b/web/frontend/src/App.tsx index 575bfb3..651e7bc 100644 --- a/web/frontend/src/App.tsx +++ b/web/frontend/src/App.tsx @@ -1,6 +1,6 @@ import { useState, useCallback } from 'react'; import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; -import { ConfigProvider } from 'antd'; +import { ConfigProvider, theme } from 'antd'; import zhCN from 'antd/locale/zh_CN'; import LoginPage from './pages/LoginPage'; import MainLayout from './layouts/MainLayout'; @@ -12,15 +12,25 @@ import ProxyPage from './pages/ProxyPage'; import UsersPage from './pages/UsersPage'; import CookiePage from './pages/CookiePage'; import { getToken } from './store/auth'; +import { ThemeProvider, useTheme } from './store/theme'; -function App() { +function AppContent() { // 用 state 驱动重渲染,登录/登出时调 refreshAuth() const [authVersion, setAuthVersion] = useState(0); const refreshAuth = useCallback(() => setAuthVersion((v) => v + 1), []); const isLoggedIn = !!getToken(); + const { isDark } = useTheme(); return ( - + } /> @@ -43,4 +53,12 @@ function App() { ); } +function App() { + return ( + + + + ); +} + export default App; diff --git a/web/frontend/src/index.css b/web/frontend/src/index.css index f60ecaf..b5bdcd8 100644 --- a/web/frontend/src/index.css +++ b/web/frontend/src/index.css @@ -6,4 +6,9 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + transition: background-color 0.3s, color 0.3s; +} + +html[data-theme='dark'] body { + color-scheme: dark; } diff --git a/web/frontend/src/layouts/MainLayout.tsx b/web/frontend/src/layouts/MainLayout.tsx index 5d4bc3c..39930d4 100644 --- a/web/frontend/src/layouts/MainLayout.tsx +++ b/web/frontend/src/layouts/MainLayout.tsx @@ -1,13 +1,15 @@ import { useEffect, useState } from 'react'; -import { Layout, Menu, Avatar, Space, Typography, Button, Modal } from 'antd'; +import { Layout, Menu, Avatar, Space, Typography, Button, Modal, Dropdown } from 'antd'; import { DashboardOutlined, UserOutlined, LogoutOutlined, CloudServerOutlined, TeamOutlined, ApiOutlined, KeyOutlined, MenuFoldOutlined, MenuUnfoldOutlined, SwapOutlined, + SunOutlined, MoonOutlined, DesktopOutlined, } from '@ant-design/icons'; import { useNavigate, useLocation, Outlet } from 'react-router-dom'; import { getUser, clearAuth, hasPerm, type AuthUser } from '../store/auth'; import { authApi } from '../api/modules'; +import { useTheme, type ThemeMode } from '../store/theme'; const { Sider, Content } = Layout; const { Text } = Typography; @@ -18,11 +20,18 @@ const ROLE_LABELS: Record = { support: '客服', }; +const THEME_OPTIONS: { key: ThemeMode; label: string; icon: React.ReactNode }[] = [ + { key: 'light', label: '亮色', icon: }, + { key: 'dark', label: '暗色', icon: }, + { key: 'system', label: '跟随系统', icon: }, +]; + export default function MainLayout({ onLogout }: { onLogout?: () => void }) { const navigate = useNavigate(); const location = useLocation(); const [user] = useState(getUser()); const [collapsed, setCollapsed] = useState(false); + const { mode, isDark, setMode } = useTheme(); useEffect(() => { if (!user) navigate('/login'); @@ -84,17 +93,21 @@ export default function MainLayout({ onLogout }: { onLogout?: () => void }) { }); }; + const siderTheme = isDark ? 'dark' : 'light'; + const siderTextColor = isDark ? '#fff' : undefined; + return ( - +
{collapsed ? '鱼' : '斗鱼登录后台'} @@ -104,11 +117,11 @@ export default function MainLayout({ onLogout }: { onLogout?: () => void }) { size="small" icon={collapsed ? : } onClick={() => setCollapsed(!collapsed)} - style={{ color: '#fff', flexShrink: 0 }} + style={{ color: siderTextColor, flexShrink: 0 }} />
void }) { style={{ flex: 1, overflow: 'auto', marginTop: 8 }} />
- + } size="small" /> {!collapsed && ( <> - {user.username} - + {user.username} + ({ROLE_LABELS[user.role] || user.role}) )} {collapsed ? ( - + + ({ key: o.key, label: o.label, icon: o.icon, onClick: () => setMode(o.key) })) }} + trigger={['click']} + > + + + + )}
- + diff --git a/web/frontend/src/pages/AssignmentsPage.tsx b/web/frontend/src/pages/AssignmentsPage.tsx index 2e7f672..2c9ee32 100644 --- a/web/frontend/src/pages/AssignmentsPage.tsx +++ b/web/frontend/src/pages/AssignmentsPage.tsx @@ -1,7 +1,7 @@ import { useEffect, useState, useMemo } from 'react'; import { Card, Table, Button, Select, Input, Tag, Row, Col, Statistic, - message, Space, Tabs, Typography, Badge, + message, Space, Tabs, Typography, Badge, theme, } from 'antd'; import { UserOutlined, CheckCircleOutlined, TeamOutlined, @@ -28,6 +28,7 @@ interface AccountItem { } export default function AssignmentsPage() { + const { token } = theme.useToken(); const [accounts, setAccounts] = useState([]); const [supportUsers, setSupportUsers] = useState([]); const [selectedUser, setSelectedUser] = useState(null); @@ -219,12 +220,12 @@ export default function AssignmentsPage() { - } /> + } /> - } /> + } /> @@ -258,22 +259,22 @@ export default function AssignmentsPage() { padding: '10px 14px', borderRadius: 6, cursor: 'pointer', - border: `1px solid ${isSelected ? '#1677ff' : '#f0f0f0'}`, - background: isSelected ? '#e6f4ff' : '#fff', + border: `1px solid ${isSelected ? token.colorPrimary : token.colorBorderSecondary}`, + background: isSelected ? token.colorPrimaryBg : token.colorBgContainer, transition: 'all 0.2s', }} onMouseEnter={(e) => { - if (!isSelected) (e.currentTarget as HTMLElement).style.borderColor = '#1677ff'; + if (!isSelected) (e.currentTarget as HTMLElement).style.borderColor = token.colorPrimary; }} onMouseLeave={(e) => { - if (!isSelected) (e.currentTarget as HTMLElement).style.borderColor = '#f0f0f0'; + if (!isSelected) (e.currentTarget as HTMLElement).style.borderColor = token.colorBorderSecondary; }} > - + {user.username} - + ); })} @@ -295,7 +296,7 @@ export default function AssignmentsPage() { {selectedUser ? ( <> 已选中客服: - + {selectedUser.username}(已分配 {selectedUser.assigned_count} 个) @@ -308,7 +309,7 @@ export default function AssignmentsPage() { bodyStyle={{ flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0, padding: 0 }} > {/* 筛选栏 */} -
+
([]); const [loading, setLoading] = useState(false); const [selectedRowKeys, setSelectedRowKeys] = useState([]); @@ -184,7 +185,7 @@ export default function CookiePage() { @@ -193,7 +194,7 @@ export default function CookiePage() { diff --git a/web/frontend/src/pages/DashboardPage.tsx b/web/frontend/src/pages/DashboardPage.tsx index 10818c0..88f6a8a 100644 --- a/web/frontend/src/pages/DashboardPage.tsx +++ b/web/frontend/src/pages/DashboardPage.tsx @@ -1,8 +1,9 @@ -import { Card, Col, Row, Statistic } from 'antd'; +import { Card, Col, Row, Statistic, theme } from 'antd'; import { useEffect, useState } from 'react'; import { accountApi, loginApi } from '../api/modules'; export default function DashboardPage() { + const { token } = theme.useToken(); const [stats, setStats] = useState({ accounts: 0, tasks: 0, success: 0, failed: 0 }); useEffect(() => { @@ -34,12 +35,12 @@ export default function DashboardPage() { - + - + diff --git a/web/frontend/src/pages/LoginPage.tsx b/web/frontend/src/pages/LoginPage.tsx index c19668a..c4245c4 100644 --- a/web/frontend/src/pages/LoginPage.tsx +++ b/web/frontend/src/pages/LoginPage.tsx @@ -4,12 +4,14 @@ import { LockOutlined, UserOutlined } from '@ant-design/icons'; import { useNavigate } from 'react-router-dom'; import { authApi } from '../api/modules'; import { setAuth, type AuthUser } from '../store/auth'; +import { useTheme } from '../store/theme'; const { Title } = Typography; export default function LoginPage({ onLogin }: { onLogin?: () => void }) { const [loading, setLoading] = useState(false); const navigate = useNavigate(); + const { isDark } = useTheme(); const onFinish = async (values: { username: string; password: string }) => { setLoading(true); @@ -38,9 +40,11 @@ export default function LoginPage({ onLogin }: { onLogin?: () => void }) { display: 'flex', justifyContent: 'center', alignItems: 'center', - background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', + background: isDark + ? 'linear-gradient(135deg, #1a1a2e 0%, #16213e 100%)' + : 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', }}> - + 斗鱼批量登录后台 @@ -57,7 +61,7 @@ export default function LoginPage({ onLogin }: { onLogin?: () => void }) { -
+
默认管理员: admin / admin123
diff --git a/web/frontend/src/pages/LoginTasksPage.tsx b/web/frontend/src/pages/LoginTasksPage.tsx index 3fd46ac..bf3ef18 100644 --- a/web/frontend/src/pages/LoginTasksPage.tsx +++ b/web/frontend/src/pages/LoginTasksPage.tsx @@ -1,6 +1,6 @@ import { useEffect, useState, useRef, useMemo, useCallback } from 'react'; import { - Table, Button, Select, message, Tag, Space, Spin, InputNumber, Tooltip, Popconfirm, + Table, Button, Select, message, Tag, Space, Spin, InputNumber, Tooltip, Popconfirm, theme, } from 'antd'; import { PlayCircleOutlined, StopOutlined, FilterOutlined, ThunderboltOutlined, ReloadOutlined, DownOutlined, UpOutlined, DeleteOutlined } from '@ant-design/icons'; import { accountApi, loginApi } from '../api/modules'; @@ -39,6 +39,8 @@ export default function LoginTasksPage() { const logEndRef = useRef(null); const user = getUser(); + const { token } = theme.useToken(); + const canBatch = hasPerm(user, 'login:batch'); // 从账号中提取所有标签 @@ -259,7 +261,7 @@ export default function LoginTasksPage() { return (
{/* 标题 + 筛选栏 */} -
+

登录任务

{canBatch && (
@@ -314,7 +316,7 @@ export default function LoginTasksPage() { }} dropdownRender={(menu) => ( <> -
+
@@ -327,13 +329,13 @@ export default function LoginTasksPage() { )} /> {selectedTags.length > 0 && ( - + 标签选中 {accounts.filter((a) => selectedTags.includes((a.tag || '').trim())).length} 个 )} - + {/* 概览 */} -
+
{tasks.length} 个任务 - 成功 {successCount} - 失败 {failedCount} + 成功 {successCount} + 失败 {failedCount} {batchId && 批次: {batchId}}
{selectedRowKeys.length > 0 && ( @@ -410,14 +412,14 @@ export default function LoginTasksPage() {
{/* 实时日志 - 底部可折叠 */} -
+
setLogVisible((v) => !v)} > 实时日志 {logVisible ? : } - {logs.length > 0 && {logs.length} 条} + {logs.length > 0 && {logs.length} 条} {wsConnected && 连接中}
{logVisible && ( @@ -428,7 +430,7 @@ export default function LoginTasksPage() { fontFamily: 'monospace', fontSize: 12, padding: 4, - backgroundColor: '#fafafa', + backgroundColor: token.colorBgLayout, borderRadius: 4, }} > @@ -440,10 +442,10 @@ export default function LoginTasksPage() { key={i} style={{ color: - log.level === 'error' ? '#ff4d4f' : - log.level === 'success' ? '#52c41a' : - log.level === 'warning' ? '#fa8c16' : - 'rgba(0,0,0,0.65)', + log.level === 'error' ? token.colorError : + log.level === 'success' ? token.colorSuccess : + log.level === 'warning' ? token.colorWarning : + token.colorText, }} > {log.message} diff --git a/web/frontend/src/pages/ProxyPage.tsx b/web/frontend/src/pages/ProxyPage.tsx index c7856e6..ca96ab5 100644 --- a/web/frontend/src/pages/ProxyPage.tsx +++ b/web/frontend/src/pages/ProxyPage.tsx @@ -1,10 +1,11 @@ import { useEffect, useState, useRef } from 'react'; -import { Form, Input, Switch, Button, Card, message, Row, Col } from 'antd'; +import { Form, Input, Switch, Button, Card, message, Row, Col, theme } from 'antd'; import { proxyApi } from '../api/modules'; const WS_BASE = `ws://${window.location.hostname}:8000`; export default function ProxyPage() { + const { token } = theme.useToken(); const [form] = Form.useForm(); const [loading, setLoading] = useState(false); const [testing, setTesting] = useState(false); @@ -100,10 +101,10 @@ export default function ProxyPage() { const logColors: Record = { - error: '#ff4d4f', - success: '#52c41a', - warning: '#faad14', - info: '#333', + error: token.colorError, + success: token.colorSuccess, + warning: token.colorWarning, + info: token.colorText, }; return ( @@ -171,10 +172,10 @@ export default function ProxyPage() { styles={{ body: { height: '100%', overflow: 'auto', fontFamily: 'monospace', fontSize: 12, padding: '8px 16px' } }} > {logs.length === 0 ? ( - 点击"测试代理"或"测试白名单"查看日志 + 点击"测试代理"或"测试白名单"查看日志 ) : ( logs.map((log, i) => ( -
+
{log.message}
)) diff --git a/web/frontend/src/store/theme.tsx b/web/frontend/src/store/theme.tsx new file mode 100644 index 0000000..7e1271e --- /dev/null +++ b/web/frontend/src/store/theme.tsx @@ -0,0 +1,65 @@ +import { createContext, useContext, useState, useEffect, useCallback, type ReactNode } from 'react'; + +export type ThemeMode = 'light' | 'dark' | 'system'; + +const STORAGE_KEY = 'theme_mode'; +const DEFAULT_MODE: ThemeMode = 'system'; + +interface ThemeContextValue { + mode: ThemeMode; + isDark: boolean; + setMode: (mode: ThemeMode) => void; + toggle: () => void; +} + +const ThemeContext = createContext({ + mode: DEFAULT_MODE, + isDark: false, + setMode: () => {}, + toggle: () => {}, +}); + +export function ThemeProvider({ children }: { children: ReactNode }) { + const [mode, setModeState] = useState(() => { + const saved = localStorage.getItem(STORAGE_KEY); + return (saved as ThemeMode) || DEFAULT_MODE; + }); + + // 监听系统主题变化 + const [systemIsDark, setSystemIsDark] = useState(() => + window.matchMedia('(prefers-color-scheme: dark)').matches + ); + + useEffect(() => { + const mql = window.matchMedia('(prefers-color-scheme: dark)'); + const handler = (e: MediaQueryListEvent) => setSystemIsDark(e.matches); + mql.addEventListener('change', handler); + return () => mql.removeEventListener('change', handler); + }, []); + + const isDark = mode === 'dark' || (mode === 'system' && systemIsDark); + + const setMode = useCallback((newMode: ThemeMode) => { + setModeState(newMode); + localStorage.setItem(STORAGE_KEY, newMode); + }, []); + + const toggle = useCallback(() => { + setMode(isDark ? 'light' : 'dark'); + }, [isDark, setMode]); + + // 同步到 HTML root,方便 CSS 使用 + useEffect(() => { + document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light'); + }, [isDark]); + + return ( + + {children} + + ); +} + +export function useTheme() { + return useContext(ThemeContext); +}