From 6f6fececa8639710cd047f594db6134ab1909953 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 4 Feb 2026 02:22:28 +0000 Subject: [PATCH] =?UTF-8?q?style:=20=E7=BE=8E=E5=8C=96=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E9=9A=A7=E9=81=93=E6=9D=83=E9=99=90=E5=88=86=E9=85=8D=E8=A1=A8?= =?UTF-8?q?=E5=8D=95UI=E5=B9=B6=E7=BB=9F=E4=B8=80=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 使用HeroUI Checkbox组件替换原生checkbox - 重构隧道列表为card-based tile风格 - 添加选中/未选中/已分配状态视觉区分 - 应用eslint --fix代码格式统一 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus --- vite-frontend/src/App.tsx | 126 +- vite-frontend/src/api/index.ts | 100 +- vite-frontend/src/api/network.ts | 166 +- vite-frontend/src/components/icons.tsx | 16 +- vite-frontend/src/components/navbar.tsx | 49 +- vite-frontend/src/components/page-wrapper.tsx | 25 +- .../src/components/theme-provider.tsx | 32 +- vite-frontend/src/config/site.ts | 84 +- vite-frontend/src/layouts/admin.tsx | 473 ++- vite-frontend/src/layouts/h5-simple.tsx | 38 +- vite-frontend/src/layouts/h5.tsx | 120 +- vite-frontend/src/main.tsx | 4 +- vite-frontend/src/pages/change-password.tsx | 137 +- vite-frontend/src/pages/config.tsx | 441 +-- vite-frontend/src/pages/dashboard.tsx | 1378 +++++--- vite-frontend/src/pages/forward.tsx | 3134 ++++++++++------- vite-frontend/src/pages/index.tsx | 187 +- vite-frontend/src/pages/limit.tsx | 595 ++-- vite-frontend/src/pages/node.tsx | 1898 +++++----- vite-frontend/src/pages/profile.tsx | 322 +- vite-frontend/src/pages/settings.tsx | 106 +- vite-frontend/src/pages/tunnel.tsx | 3021 +++++++++------- vite-frontend/src/pages/user.tsx | 1141 +++--- vite-frontend/src/provider.tsx | 25 +- vite-frontend/src/types/recharts.d.ts | 8 +- vite-frontend/src/types/tac.d.ts | 2 +- vite-frontend/src/utils/auth.ts | 17 +- vite-frontend/src/utils/jwt.ts | 37 +- vite-frontend/src/utils/logout.ts | 2 +- vite-frontend/src/utils/panel.ts | 115 +- vite-frontend/vite.config.ts | 21 +- 31 files changed, 8080 insertions(+), 5740 deletions(-) diff --git a/vite-frontend/src/App.tsx b/vite-frontend/src/App.tsx index 6061f62..4d5bbdd 100644 --- a/vite-frontend/src/App.tsx +++ b/vite-frontend/src/App.tsx @@ -12,11 +12,9 @@ import ProfilePage from "@/pages/profile"; import LimitPage from "@/pages/limit"; import ConfigPage from "@/pages/config"; import { SettingsPage } from "@/pages/settings"; - import AdminLayout from "@/layouts/admin"; import H5Layout from "@/layouts/h5"; import H5SimpleLayout from "@/layouts/h5-simple"; - import { isLoggedIn } from "@/utils/auth"; import { siteConfig } from "@/config/site"; @@ -27,11 +25,14 @@ const useH5Mode = () => { // 检测移动设备或小屏幕 const isMobile = window.innerWidth <= 768; // 检测是否为移动端浏览器 - const isMobileBrowser = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); + const isMobileBrowser = + /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test( + navigator.userAgent, + ); // 检测URL参数是否包含h5模式 const urlParams = new URLSearchParams(window.location.search); - const isH5Param = urlParams.get('h5') === 'true'; - + const isH5Param = urlParams.get("h5") === "true"; + return isMobile || isMobileBrowser || isH5Param; }; @@ -42,39 +43,50 @@ const useH5Mode = () => { // 检测移动设备或小屏幕 const isMobile = window.innerWidth <= 768; // 检测是否为移动端浏览器 - const isMobileBrowser = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); + const isMobileBrowser = + /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test( + navigator.userAgent, + ); // 检测URL参数是否包含h5模式 const urlParams = new URLSearchParams(window.location.search); - const isH5Param = urlParams.get('h5') === 'true'; - + const isH5Param = urlParams.get("h5") === "true"; + setIsH5(isMobile || isMobileBrowser || isH5Param); }; - window.addEventListener('resize', checkH5Mode); - - return () => window.removeEventListener('resize', checkH5Mode); + window.addEventListener("resize", checkH5Mode); + + return () => window.removeEventListener("resize", checkH5Mode); }, []); return isH5; }; // 简化的路由保护组件 - 使用 React Router 导航避免循环 -const ProtectedRoute = ({ children, useSimpleLayout = false, skipLayout = false }: { children: React.ReactNode, useSimpleLayout?: boolean, skipLayout?: boolean }) => { +const ProtectedRoute = ({ + children, + useSimpleLayout = false, + skipLayout = false, +}: { + children: React.ReactNode; + useSimpleLayout?: boolean; + skipLayout?: boolean; +}) => { const authenticated = isLoggedIn(); const isH5 = useH5Mode(); const navigate = useNavigate(); - + useEffect(() => { if (!authenticated) { // 使用 React Router 导航,避免无限跳转 - navigate('/', { replace: true }); + navigate("/", { replace: true }); } }, [authenticated, navigate]); if (!authenticated) { return (
-
+
); } @@ -86,6 +98,7 @@ const ProtectedRoute = ({ children, useSimpleLayout = false, skipLayout = false // 根据模式和页面类型选择布局 let Layout; + if (isH5 && useSimpleLayout) { Layout = H5SimpleLayout; } else if (isH5) { @@ -93,31 +106,30 @@ const ProtectedRoute = ({ children, useSimpleLayout = false, skipLayout = false } else { Layout = AdminLayout; } - + return {children}; }; - // 登录页面路由组件 - 已登录则重定向到dashboard const LoginRoute = () => { const authenticated = isLoggedIn(); const navigate = useNavigate(); - + useEffect(() => { if (authenticated) { // 使用 React Router 导航,避免无限跳转 - navigate('/dashboard', { replace: true }); + navigate("/dashboard", { replace: true }); } }, [authenticated, navigate]); - + if (authenticated) { return (
-
+
); } - + return ; }; @@ -125,18 +137,19 @@ function App() { // 立即设置页面标题(使用已从缓存读取的配置) useEffect(() => { document.title = siteConfig.name; - + // 异步检查是否有配置更新 const checkTitleUpdate = async () => { try { // 引入必要的函数 - const { getCachedConfig } = await import('@/config/site'); - const cachedAppName = await getCachedConfig('app_name'); + const { getCachedConfig } = await import("@/config/site"); + const cachedAppName = await getCachedConfig("app_name"); + if (cachedAppName && cachedAppName !== document.title) { document.title = cachedAppName; } } catch (error) { - console.warn('检查标题更新失败:', error); + console.warn("检查标题更新失败:", error); } }; @@ -148,83 +161,80 @@ function App() { return ( - } /> - } path="/" /> + - } + } + path="/change-password" /> - - } + } + path="/dashboard" /> - - } + } + path="/forward" /> - - } + } + path="/tunnel" /> - - } + } + path="/node" /> - - } + } + path="/user" /> - - } + } + path="/profile" /> - - } + } + path="/limit" /> - - } - /> - } + } + path="/config" /> + } path="/settings" /> ); } diff --git a/vite-frontend/src/api/index.ts b/vite-frontend/src/api/index.ts index 6e72465..8a53ddc 100644 --- a/vite-frontend/src/api/index.ts +++ b/vite-frontend/src/api/index.ts @@ -1,4 +1,4 @@ -import Network from './network'; +import Network from "./network"; // 登陆相关接口 export interface LoginData { @@ -14,11 +14,13 @@ export interface LoginResponse { requirePasswordChange?: boolean; } -export const login = (data: LoginData) => Network.post("/user/login", data); +export const login = (data: LoginData) => + Network.post("/user/login", data); // 用户CRUD操作 - 全部使用POST请求 export const createUser = (data: any) => Network.post("/user/create", data); -export const getAllUsers = (pageData: any = {}) => Network.post("/user/list", pageData); +export const getAllUsers = (pageData: any = {}) => + Network.post("/user/list", pageData); export const updateUser = (data: any) => Network.post("/user/update", data); export const deleteUser = (id: number) => Network.post("/user/delete", { id }); export const getUserPackageInfo = () => Network.post("/user/package"); @@ -28,68 +30,100 @@ export const createNode = (data: any) => Network.post("/node/create", data); export const getNodeList = () => Network.post("/node/list"); export const updateNode = (data: any) => Network.post("/node/update", data); export const deleteNode = (id: number) => Network.post("/node/delete", { id }); -export const getNodeInstallCommand = (id: number) => Network.post("/node/install", { id }); -export const updateNodeOrder = (data: { nodes: Array<{ id: number; inx: number }> }) => Network.post("/node/update-order", data); +export const getNodeInstallCommand = (id: number) => + Network.post("/node/install", { id }); +export const updateNodeOrder = (data: { + nodes: Array<{ id: number; inx: number }>; +}) => Network.post("/node/update-order", data); export const checkNodeStatus = (nodeId?: number) => { const params = nodeId ? { nodeId } : {}; + return Network.post("/node/check-status", params); }; // 隧道CRUD操作 - 全部使用POST请求 export const createTunnel = (data: any) => Network.post("/tunnel/create", data); export const getTunnelList = () => Network.post("/tunnel/list"); -export const getTunnelById = (id: number) => Network.post("/tunnel/get", { id }); +export const getTunnelById = (id: number) => + Network.post("/tunnel/get", { id }); export const updateTunnel = (data: any) => Network.post("/tunnel/update", data); -export const deleteTunnel = (id: number) => Network.post("/tunnel/delete", { id }); -export const diagnoseTunnel = (tunnelId: number) => Network.post("/tunnel/diagnose", { tunnelId }); -export const updateTunnelOrder = (data: { tunnels: Array<{ id: number; inx: number }> }) => Network.post("/tunnel/update-order", data); +export const deleteTunnel = (id: number) => + Network.post("/tunnel/delete", { id }); +export const diagnoseTunnel = (tunnelId: number) => + Network.post("/tunnel/diagnose", { tunnelId }); +export const updateTunnelOrder = (data: { + tunnels: Array<{ id: number; inx: number }>; +}) => Network.post("/tunnel/update-order", data); // 用户隧道权限管理操作 - 全部使用POST请求 -export const assignUserTunnel = (data: any) => Network.post("/tunnel/user/assign", data); -export const batchAssignUserTunnel = (data: { userId: number; tunnels: Array<{ tunnelId: number; speedId?: number | null }> }) => - Network.post("/tunnel/user/batch-assign", data); -export const getUserTunnelList = (queryData: any = {}) => Network.post("/tunnel/user/list", queryData); -export const removeUserTunnel = (params: any) => Network.post("/tunnel/user/remove", params); -export const updateUserTunnel = (data: any) => Network.post("/tunnel/user/update", data); +export const assignUserTunnel = (data: any) => + Network.post("/tunnel/user/assign", data); +export const batchAssignUserTunnel = (data: { + userId: number; + tunnels: Array<{ tunnelId: number; speedId?: number | null }>; +}) => Network.post("/tunnel/user/batch-assign", data); +export const getUserTunnelList = (queryData: any = {}) => + Network.post("/tunnel/user/list", queryData); +export const removeUserTunnel = (params: any) => + Network.post("/tunnel/user/remove", params); +export const updateUserTunnel = (data: any) => + Network.post("/tunnel/user/update", data); export const userTunnel = () => Network.post("/tunnel/user/tunnel"); // 转发CRUD操作 - 全部使用POST请求 -export const createForward = (data: any) => Network.post("/forward/create", data); +export const createForward = (data: any) => + Network.post("/forward/create", data); export const getForwardList = () => Network.post("/forward/list"); -export const updateForward = (data: any) => Network.post("/forward/update", data); -export const deleteForward = (id: number) => Network.post("/forward/delete", { id }); -export const forceDeleteForward = (id: number) => Network.post("/forward/force-delete", { id }); +export const updateForward = (data: any) => + Network.post("/forward/update", data); +export const deleteForward = (id: number) => + Network.post("/forward/delete", { id }); +export const forceDeleteForward = (id: number) => + Network.post("/forward/force-delete", { id }); // 转发服务控制操作 - 通过Java后端接口 -export const pauseForwardService = (forwardId: number) => Network.post("/forward/pause", { id: forwardId }); -export const resumeForwardService = (forwardId: number) => Network.post("/forward/resume", { id: forwardId }); +export const pauseForwardService = (forwardId: number) => + Network.post("/forward/pause", { id: forwardId }); +export const resumeForwardService = (forwardId: number) => + Network.post("/forward/resume", { id: forwardId }); // 转发诊断操作 -export const diagnoseForward = (forwardId: number) => Network.post("/forward/diagnose", { forwardId }); +export const diagnoseForward = (forwardId: number) => + Network.post("/forward/diagnose", { forwardId }); // 转发排序操作 -export const updateForwardOrder = (data: { forwards: Array<{ id: number; inx: number }> }) => Network.post("/forward/update-order", data); +export const updateForwardOrder = (data: { + forwards: Array<{ id: number; inx: number }>; +}) => Network.post("/forward/update-order", data); // 限速规则CRUD操作 - 全部使用POST请求 -export const createSpeedLimit = (data: any) => Network.post("/speed-limit/create", data); +export const createSpeedLimit = (data: any) => + Network.post("/speed-limit/create", data); export const getSpeedLimitList = () => Network.post("/speed-limit/list"); -export const updateSpeedLimit = (data: any) => Network.post("/speed-limit/update", data); -export const deleteSpeedLimit = (id: number) => Network.post("/speed-limit/delete", { id }); +export const updateSpeedLimit = (data: any) => + Network.post("/speed-limit/update", data); +export const deleteSpeedLimit = (id: number) => + Network.post("/speed-limit/delete", { id }); // 修改密码接口 -export const updatePassword = (data: any) => Network.post("/user/updatePassword", data); +export const updatePassword = (data: any) => + Network.post("/user/updatePassword", data); // 重置流量接口 -export const resetUserFlow = (data: { id: number; type: number }) => Network.post("/user/reset", data); +export const resetUserFlow = (data: { id: number; type: number }) => + Network.post("/user/reset", data); // 网站配置相关接口 export const getConfigs = () => Network.post("/config/list"); -export const getConfigByName = (name: string) => Network.post("/config/get", { name }); -export const updateConfigs = (configMap: Record) => Network.post("/config/update", configMap); -export const updateConfig = (name: string, value: string) => Network.post("/config/update-single", { name, value }); - +export const getConfigByName = (name: string) => + Network.post("/config/get", { name }); +export const updateConfigs = (configMap: Record) => + Network.post("/config/update", configMap); +export const updateConfig = (name: string, value: string) => + Network.post("/config/update-single", { name, value }); // 验证码相关接口 export const checkCaptcha = () => Network.post("/captcha/check"); export const generateCaptcha = () => Network.post(`/captcha/generate`); -export const verifyCaptcha = (data: { captchaId: string; trackData: string }) => Network.post("/captcha/verify", data); +export const verifyCaptcha = (data: { captchaId: string; trackData: string }) => + Network.post("/captcha/verify", data); diff --git a/vite-frontend/src/api/network.ts b/vite-frontend/src/api/network.ts index 958a1bf..0ea6734 100644 --- a/vite-frontend/src/api/network.ts +++ b/vite-frontend/src/api/network.ts @@ -1,41 +1,42 @@ -import axios, { AxiosResponse } from 'axios'; -import { getPanelAddresses, isWebViewFunc} from '@/utils/panel'; +import axios, { AxiosResponse } from "axios"; +import { getPanelAddresses, isWebViewFunc } from "@/utils/panel"; interface PanelAddress { name: string; - address: string; + address: string; inx: boolean; } const setPanelAddressesFunc = (newAddress: PanelAddress[]) => { - newAddress.forEach(item => { + newAddress.forEach((item) => { if (item.inx) { baseURL = `${item.address}/api/v1/`; axios.defaults.baseURL = baseURL; } }); -} - -function getWebViewPanelAddress() { - (window as any).setAddresses = setPanelAddressesFunc - getPanelAddresses("setAddresses"); }; -let baseURL: string = ''; +function getWebViewPanelAddress() { + (window as any).setAddresses = setPanelAddressesFunc; + getPanelAddresses("setAddresses"); +} + +let baseURL: string = ""; export const reinitializeBaseURL = () => { if (isWebViewFunc()) { getWebViewPanelAddress(); } else { - baseURL = import.meta.env.VITE_API_BASE ? `${import.meta.env.VITE_API_BASE}/api/v1/` : '/api/v1/'; + baseURL = import.meta.env.VITE_API_BASE + ? `${import.meta.env.VITE_API_BASE}/api/v1/` + : "/api/v1/"; axios.defaults.baseURL = baseURL; } }; reinitializeBaseURL(); - interface ApiResponse { code: number; msg: string; @@ -45,98 +46,123 @@ interface ApiResponse { // 处理token失效的逻辑 function handleTokenExpired() { // 清除localStorage中的token - window.localStorage.removeItem('token'); - window.localStorage.removeItem('role_id'); - window.localStorage.removeItem('name'); - + window.localStorage.removeItem("token"); + window.localStorage.removeItem("role_id"); + window.localStorage.removeItem("name"); + // 跳转到登录页面 - if (window.location.pathname !== '/') { - window.location.href = '/'; + if (window.location.pathname !== "/") { + window.location.href = "/"; } } // 检查响应是否为token失效 function isTokenExpired(response: ApiResponse) { - return response && response.code === 401 && - (response.msg === '未登录或token已过期' || - response.msg === '无效的token或token已过期' || - response.msg === '无法获取用户权限信息'); + return ( + response && + response.code === 401 && + (response.msg === "未登录或token已过期" || + response.msg === "无效的token或token已过期" || + response.msg === "无法获取用户权限信息") + ); } const Network = { - get: function(path: string = '', data: any = {}): Promise> { - return new Promise(function(resolve) { + get: function ( + path: string = "", + data: any = {}, + ): Promise> { + return new Promise(function (resolve) { // 如果baseURL是默认值且是WebView环境,说明没有设置面板地址 - if (baseURL === '') { - resolve({"code": -1, "msg": " - 请先设置面板地址", "data": null as T}); + if (baseURL === "") { + resolve({ code: -1, msg: " - 请先设置面板地址", data: null as T }); + return; } - axios.get(path, { - params: data, - timeout: 30000, - headers: { - "Authorization": window.localStorage.getItem('token') - } - }) - .then(function(response: AxiosResponse>) { + axios + .get(path, { + params: data, + timeout: 30000, + headers: { + Authorization: window.localStorage.getItem("token"), + }, + }) + .then(function (response: AxiosResponse>) { // 检查是否token失效 if (isTokenExpired(response.data)) { handleTokenExpired(); + return; } resolve(response.data); }) - .catch(function(error: any) { - console.error('GET请求错误:', error); - - // 检查是否是401错误(token失效) - if (error.response && error.response.status === 401) { - handleTokenExpired(); - return; - } - - resolve({"code": -1, "msg": error.message || "网络请求失败", "data": null as T}); - }); + .catch(function (error: any) { + console.error("GET请求错误:", error); + + // 检查是否是401错误(token失效) + if (error.response && error.response.status === 401) { + handleTokenExpired(); + + return; + } + + resolve({ + code: -1, + msg: error.message || "网络请求失败", + data: null as T, + }); + }); }); }, - post: function(path: string = '', data: any = {}): Promise> { - return new Promise(function(resolve) { + post: function ( + path: string = "", + data: any = {}, + ): Promise> { + return new Promise(function (resolve) { // 如果baseURL是默认值且是WebView环境,说明没有设置面板地址 - if (baseURL === '') { - resolve({"code": -1, "msg": " - 请先设置面板地址", "data": null as T}); + if (baseURL === "") { + resolve({ code: -1, msg: " - 请先设置面板地址", data: null as T }); + return; } - axios.post(path, data, { - timeout: 30000, - headers: { - "Authorization": window.localStorage.getItem('token'), - "Content-Type": "application/json" - } - }) - .then(function(response: AxiosResponse>) { + axios + .post(path, data, { + timeout: 30000, + headers: { + Authorization: window.localStorage.getItem("token"), + "Content-Type": "application/json", + }, + }) + .then(function (response: AxiosResponse>) { // 检查是否token失效 if (isTokenExpired(response.data)) { handleTokenExpired(); + return; } resolve(response.data); }) - .catch(function(error: any) { - console.error('POST请求错误:', error); - - // 检查是否是401错误(token失效) - if (error.response && error.response.status === 401) { - handleTokenExpired(); - return; - } - - resolve({"code": -1, "msg": error.message || "网络请求失败", "data": null as T}); - }); + .catch(function (error: any) { + console.error("POST请求错误:", error); + + // 检查是否是401错误(token失效) + if (error.response && error.response.status === 401) { + handleTokenExpired(); + + return; + } + + resolve({ + code: -1, + msg: error.message || "网络请求失败", + data: null as T, + }); + }); }); - } + }, }; -export default Network; \ No newline at end of file +export default Network; diff --git a/vite-frontend/src/components/icons.tsx b/vite-frontend/src/components/icons.tsx index a597693..720ed7a 100644 --- a/vite-frontend/src/components/icons.tsx +++ b/vite-frontend/src/components/icons.tsx @@ -84,11 +84,11 @@ export const SearchIcon = ({ > ); @@ -183,11 +183,11 @@ export const DeleteIcon = ({ /> ); @@ -209,19 +209,19 @@ export const UserIcon = ({ > ); @@ -243,19 +243,19 @@ export const SettingsIcon = ({ > ); diff --git a/vite-frontend/src/components/navbar.tsx b/vite-frontend/src/components/navbar.tsx index d3e1742..17ba3e3 100644 --- a/vite-frontend/src/components/navbar.tsx +++ b/vite-frontend/src/components/navbar.tsx @@ -5,9 +5,9 @@ import { NavbarBrand, NavbarContent, } from "@heroui/navbar"; -import { isWebViewFunc } from '@/utils/panel'; import { useNavigate } from "react-router-dom"; +import { isWebViewFunc } from "@/utils/panel"; import { Logo } from "@/components/icons"; import { siteConfig, getCachedConfig } from "@/config/site"; @@ -20,21 +20,21 @@ export const Navbar = () => { // 检测是否在WebView中运行 useEffect(() => { setIsWebView(isWebViewFunc()); - }, []); useEffect(() => { // 异步检查是否有更新的配置 const checkForUpdates = async () => { try { - const cachedAppName = await getCachedConfig('app_name'); + const cachedAppName = await getCachedConfig("app_name"); + if (cachedAppName && cachedAppName !== appName) { setAppName(cachedAppName); // 同步更新siteConfig siteConfig.name = cachedAppName; } } catch (error) { - console.warn('检查配置更新失败:', error); + console.warn("检查配置更新失败:", error); } }; @@ -44,27 +44,33 @@ export const Navbar = () => { // 监听配置更新事件 const handleConfigUpdate = async () => { try { - const cachedAppName = await getCachedConfig('app_name'); + const cachedAppName = await getCachedConfig("app_name"); + if (cachedAppName) { setAppName(cachedAppName); siteConfig.name = cachedAppName; } } catch (error) { - console.warn('更新配置失败:', error); + console.warn("更新配置失败:", error); } }; - window.addEventListener('configUpdated', handleConfigUpdate); - + window.addEventListener("configUpdated", handleConfigUpdate); + return () => { clearTimeout(timer); - window.removeEventListener('configUpdated', handleConfigUpdate); + window.removeEventListener("configUpdated", handleConfigUpdate); }; }, [appName]); return ( <> - + { {isWebView && ( )} diff --git a/vite-frontend/src/components/page-wrapper.tsx b/vite-frontend/src/components/page-wrapper.tsx index ddf1a84..43f5da0 100644 --- a/vite-frontend/src/components/page-wrapper.tsx +++ b/vite-frontend/src/components/page-wrapper.tsx @@ -1,5 +1,6 @@ -import React, { useState, useEffect } from 'react'; -import AdminLayout from '@/layouts/admin'; +import React, { useState, useEffect } from "react"; + +import AdminLayout from "@/layouts/admin"; interface PageWrapperProps { children: React.ReactNode; @@ -8,11 +9,11 @@ interface PageWrapperProps { className?: string; } -export default function PageWrapper({ - children, - title, - description, - className = "container mx-auto max-w-7xl px-3 lg:px-6 py-8" +export default function PageWrapper({ + children, + title, + description, + className = "container mx-auto max-w-7xl px-3 lg:px-6 py-8", }: PageWrapperProps) { const [isReady, setIsReady] = useState(false); @@ -31,8 +32,8 @@ export default function PageWrapper({
-
- +
+
@@ -45,12 +46,10 @@ export default function PageWrapper({

{title}

- {description && ( -

{description}

- )} + {description &&

{description}

}
{children}
); -} \ No newline at end of file +} diff --git a/vite-frontend/src/components/theme-provider.tsx b/vite-frontend/src/components/theme-provider.tsx index 525f886..66cdecf 100644 --- a/vite-frontend/src/components/theme-provider.tsx +++ b/vite-frontend/src/components/theme-provider.tsx @@ -1,5 +1,5 @@ -import React, { useEffect } from 'react'; -import { useTheme } from '@heroui/use-theme'; +import React, { useEffect } from "react"; +import { useTheme } from "@heroui/use-theme"; interface ThemeProviderProps { children: React.ReactNode; @@ -11,17 +11,21 @@ export const ThemeProvider: React.FC = ({ children }) => { useEffect(() => { // 确保主题与HTML class同步 const updateThemeClass = (currentTheme: string) => { - if (currentTheme === 'dark') { - document.documentElement.classList.add('dark'); - document.documentElement.style.colorScheme = 'dark'; + if (currentTheme === "dark") { + document.documentElement.classList.add("dark"); + document.documentElement.style.colorScheme = "dark"; } else { - document.documentElement.classList.remove('dark'); - document.documentElement.style.colorScheme = 'light'; + document.documentElement.classList.remove("dark"); + document.documentElement.style.colorScheme = "light"; } }; // 始终跟随系统主题 - const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; + const systemTheme = window.matchMedia("(prefers-color-scheme: dark)") + .matches + ? "dark" + : "light"; + if (systemTheme !== theme) { setTheme(systemTheme); } @@ -30,15 +34,17 @@ export const ThemeProvider: React.FC = ({ children }) => { updateThemeClass(theme); // 监听系统主题变化 - const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); const handleThemeChange = (e: MediaQueryListEvent) => { - const newTheme = e.matches ? 'dark' : 'light'; + const newTheme = e.matches ? "dark" : "light"; + setTheme(newTheme); }; - mediaQuery.addEventListener('change', handleThemeChange); - return () => mediaQuery.removeEventListener('change', handleThemeChange); + mediaQuery.addEventListener("change", handleThemeChange); + + return () => mediaQuery.removeEventListener("change", handleThemeChange); }, [theme, setTheme]); return <>{children}; -}; \ No newline at end of file +}; diff --git a/vite-frontend/src/config/site.ts b/vite-frontend/src/config/site.ts index 50b8e70..be054e3 100644 --- a/vite-frontend/src/config/site.ts +++ b/vite-frontend/src/config/site.ts @@ -1,15 +1,16 @@ -import { getConfigByName, getConfigs } from '@/api'; +import { getConfigByName, getConfigs } from "@/api"; export type SiteConfig = typeof siteConfig; // 缓存相关常量 -const CACHE_PREFIX = 'vite_config_'; +const CACHE_PREFIX = "vite_config_"; const VERSION = import.meta.env.VITE_APP_VERSION || "dev"; const APP_VERSION = "1.0.3"; -const GITHUB_REPO = import.meta.env.VITE_GITHUB_REPO || "https://github.com/Sagit-chu/flux-panel"; +const GITHUB_REPO = + import.meta.env.VITE_GITHUB_REPO || "https://github.com/Sagit-chu/flux-panel"; const getInitialConfig = () => { - if (typeof window === 'undefined') { + if (typeof window === "undefined") { return { name: "flux", version: VERSION, @@ -18,15 +19,17 @@ const getInitialConfig = () => { }; } - const cachedAppName = localStorage.getItem(CACHE_PREFIX + 'app_name'); - if (cachedAppName) { - return { - name: cachedAppName, - version: VERSION, - app_version: APP_VERSION, - github_repo: GITHUB_REPO, - }; - } + const cachedAppName = localStorage.getItem(CACHE_PREFIX + "app_name"); + + if (cachedAppName) { + return { + name: cachedAppName, + version: VERSION, + app_version: APP_VERSION, + github_repo: GITHUB_REPO, + }; + } + return { name: "flux", version: VERSION, @@ -41,44 +44,52 @@ export const configCache = { // 获取缓存的配置 get: (key: string): string | null => { const cacheKey = CACHE_PREFIX + key; - return localStorage.getItem(cacheKey); + + return localStorage.getItem(cacheKey); }, // 设置缓存的配置 set: (key: string, value: string): void => { const cacheKey = CACHE_PREFIX + key; - localStorage.setItem(cacheKey, value); + + localStorage.setItem(cacheKey, value); }, // 删除指定配置的缓存 remove: (key: string): void => { const cacheKey = CACHE_PREFIX + key; + localStorage.removeItem(cacheKey); }, // 清空所有配置缓存 clear: (): void => { - // 获取所有localStorage的key - const keys = Object.keys(localStorage); - keys.forEach(key => { - if (key.startsWith(CACHE_PREFIX)) { - localStorage.removeItem(key); - } - }); - } + // 获取所有localStorage的key + const keys = Object.keys(localStorage); + + keys.forEach((key) => { + if (key.startsWith(CACHE_PREFIX)) { + localStorage.removeItem(key); + } + }); + }, }; // 获取单个配置(优先从缓存) export const getCachedConfig = async (key: string): Promise => { const cachedValue = configCache.get(key); + if (cachedValue !== null) { return cachedValue; } const response = await getConfigByName(key); + if (response.code === 0 && response.data?.value) { const value = response.data.value; + configCache.set(key, value); + return value; } @@ -88,29 +99,31 @@ export const getCachedConfig = async (key: string): Promise => { // 获取所有配置(优先从缓存) export const getCachedConfigs = async (): Promise> => { // 尝试从缓存获取所有配置 - const configKeys = ['app_name']; + const configKeys = ["app_name"]; const cachedConfigs: Record = {}; let hasCachedData = false; - configKeys.forEach(key => { + configKeys.forEach((key) => { const cachedValue = configCache.get(key); + if (cachedValue !== null) { cachedConfigs[key] = cachedValue; hasCachedData = true; } }); - - // 从API获取最新配置 try { const response = await getConfigs(); + if (response.code === 0 && response.data) { const configs = response.data; + // 将所有配置存入缓存 Object.entries(configs).forEach(([key, value]) => { configCache.set(key, value as string); }); + return configs; } } catch (error) { @@ -125,12 +138,13 @@ export const getCachedConfigs = async (): Promise> => { // 动态更新网站配置 export const updateSiteConfig = async () => { - const appName = await getCachedConfig('app_name'); - if (appName && appName !== siteConfig.name) { - siteConfig.name = appName; - // 更新页面标题 - document.title = appName; - } + const appName = await getCachedConfig("app_name"); + + if (appName && appName !== siteConfig.name) { + siteConfig.name = appName; + // 更新页面标题 + document.title = appName; + } }; // 清除配置缓存的工具函数 @@ -140,7 +154,7 @@ export const updateSiteConfig = async () => { export const clearConfigCache = (keys?: string[]) => { if (keys && keys.length > 0) { // 删除指定的配置缓存 - keys.forEach(key => configCache.remove(key)); + keys.forEach((key) => configCache.remove(key)); } else { // 清空所有配置缓存 configCache.clear(); @@ -148,7 +162,7 @@ export const clearConfigCache = (keys?: string[]) => { }; // 在页面加载时异步更新配置(如果有更新的话) -if (typeof window !== 'undefined') { +if (typeof window !== "undefined") { // 延迟执行,避免阻塞初始渲染 setTimeout(() => { updateSiteConfig(); diff --git a/vite-frontend/src/layouts/admin.tsx b/vite-frontend/src/layouts/admin.tsx index 2409875..2bf18e8 100644 --- a/vite-frontend/src/layouts/admin.tsx +++ b/vite-frontend/src/layouts/admin.tsx @@ -1,15 +1,27 @@ -import React, { useState, useEffect } from 'react'; -import { useNavigate, useLocation } from 'react-router-dom'; +import React, { useState, useEffect } from "react"; +import { useNavigate, useLocation } from "react-router-dom"; import { Button } from "@heroui/button"; -import { Dropdown, DropdownTrigger, DropdownMenu, DropdownItem } from "@heroui/dropdown"; -import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, useDisclosure } from "@heroui/modal"; +import { + Dropdown, + DropdownTrigger, + DropdownMenu, + DropdownItem, +} from "@heroui/dropdown"; +import { + Modal, + ModalContent, + ModalHeader, + ModalBody, + ModalFooter, + useDisclosure, +} from "@heroui/modal"; import { Input } from "@heroui/input"; -import { toast } from 'react-hot-toast'; +import { toast } from "react-hot-toast"; -import { Logo } from '@/components/icons'; -import { updatePassword } from '@/api'; -import { safeLogout } from '@/utils/logout'; -import { siteConfig } from '@/config/site'; +import { Logo } from "@/components/icons"; +import { updatePassword } from "@/api"; +import { safeLogout } from "@/utils/logout"; +import { siteConfig } from "@/config/site"; interface MenuItem { path: string; @@ -36,86 +48,106 @@ export default function AdminLayout({ const [isMobile, setIsMobile] = useState(false); const [mobileMenuVisible, setMobileMenuVisible] = useState(false); - const [username, setUsername] = useState(''); + const [username, setUsername] = useState(""); const [isAdmin, setIsAdmin] = useState(false); const [passwordLoading, setPasswordLoading] = useState(false); const [passwordForm, setPasswordForm] = useState({ - newUsername: '', - currentPassword: '', - newPassword: '', - confirmPassword: '' + newUsername: "", + currentPassword: "", + newPassword: "", + confirmPassword: "", }); // 菜单项配置 const menuItems: MenuItem[] = [ { - path: '/dashboard', - label: '仪表板', + path: "/dashboard", + label: "仪表板", icon: ( - ) + ), }, { - path: '/forward', - label: '转发管理', + path: "/forward", + label: "转发管理", icon: ( - - - ) - }, - { - path: '/tunnel', - label: '隧道管理', - icon: ( - - + ), - adminOnly: true }, { - path: '/node', - label: '节点监控', + path: "/tunnel", + label: "隧道管理", icon: ( - + ), - adminOnly: true + adminOnly: true, }, { - path: '/limit', - label: '限速管理', + path: "/node", + label: "节点监控", icon: ( - + ), - adminOnly: true + adminOnly: true, }, { - path: '/user', - label: '用户管理', + path: "/limit", + label: "限速管理", + icon: ( + + + + ), + adminOnly: true, + }, + { + path: "/user", + label: "用户管理", icon: ( ), - adminOnly: true + adminOnly: true, }, { - path: '/config', - label: '网站配置', + path: "/config", + label: "网站配置", icon: ( - + ), - adminOnly: true - } + adminOnly: true, + }, ]; // 检查移动端 @@ -128,33 +160,35 @@ export default function AdminLayout({ useEffect(() => { // 获取用户信息 - const name = localStorage.getItem('name') || 'Admin'; - + const name = localStorage.getItem("name") || "Admin"; + // 兼容处理:如果没有admin字段,根据role_id判断(0为管理员) - let adminFlag = localStorage.getItem('admin') === 'true'; - if (localStorage.getItem('admin') === null) { - const roleId = parseInt(localStorage.getItem('role_id') || '1', 10); + let adminFlag = localStorage.getItem("admin") === "true"; + + if (localStorage.getItem("admin") === null) { + const roleId = parseInt(localStorage.getItem("role_id") || "1", 10); + adminFlag = roleId === 0; // 补充设置admin字段,避免下次再次判断 - localStorage.setItem('admin', adminFlag.toString()); + localStorage.setItem("admin", adminFlag.toString()); } - + setUsername(name); setIsAdmin(adminFlag); // 响应式检查 checkMobile(); - window.addEventListener('resize', checkMobile); + window.addEventListener("resize", checkMobile); return () => { - window.removeEventListener('resize', checkMobile); + window.removeEventListener("resize", checkMobile); }; }, []); // 退出登录 const handleLogout = () => { safeLogout(); - navigate('/'); + navigate("/"); }; // 切换移动端菜单 @@ -178,29 +212,36 @@ export default function AdminLayout({ // 密码表单验证 const validatePasswordForm = (): boolean => { if (!passwordForm.newUsername.trim()) { - toast.error('请输入新用户名'); + toast.error("请输入新用户名"); + return false; } if (passwordForm.newUsername.length < 3) { - toast.error('用户名长度至少3位'); + toast.error("用户名长度至少3位"); + return false; } if (!passwordForm.currentPassword) { - toast.error('请输入当前密码'); + toast.error("请输入当前密码"); + return false; } if (!passwordForm.newPassword) { - toast.error('请输入新密码'); + toast.error("请输入新密码"); + return false; } if (passwordForm.newPassword.length < 6) { - toast.error('新密码长度不能少于6位'); + toast.error("新密码长度不能少于6位"); + return false; } if (passwordForm.newPassword !== passwordForm.confirmPassword) { - toast.error('两次输入密码不一致'); + toast.error("两次输入密码不一致"); + return false; } + return true; }; @@ -211,16 +252,17 @@ export default function AdminLayout({ setPasswordLoading(true); try { const response = await updatePassword(passwordForm); + if (response.code === 0) { - toast.success('密码修改成功,请重新登录'); + toast.success("密码修改成功,请重新登录"); onOpenChange(); handleLogout(); } else { - toast.error(response.msg || '密码修改失败'); + toast.error(response.msg || "密码修改失败"); } } catch (error) { - toast.error('修改密码时发生错误'); - console.error('修改密码错误:', error); + toast.error("修改密码时发生错误"); + console.error("修改密码错误:", error); } finally { setPasswordLoading(false); } @@ -229,92 +271,98 @@ export default function AdminLayout({ // 重置密码表单 const resetPasswordForm = () => { setPasswordForm({ - newUsername: '', - currentPassword: '', - newPassword: '', - confirmPassword: '' + newUsername: "", + currentPassword: "", + newPassword: "", + confirmPassword: "", }); }; // 过滤菜单项(根据权限) - const filteredMenuItems = menuItems.filter(item => - !item.adminOnly || isAdmin + const filteredMenuItems = menuItems.filter( + (item) => !item.adminOnly || isAdmin, ); return ( -
+
{/* 移动端遮罩层 */} {isMobile && mobileMenuVisible && ( -
)} {/* 左侧菜单栏 */} - {/* 主内容区域 */} -
- {/* 顶部导航栏 */} -
+
+ {/* 顶部导航栏 */} +
{/* 移动端菜单按钮 */} {isMobile && ( )} @@ -345,21 +405,40 @@ export default function AdminLayout({
{/* 用户菜单 */} - - - - + + + + - + + } onPress={onOpen} @@ -368,13 +447,21 @@ export default function AdminLayout({ - - - } className="text-danger" color="danger" + startContent={ + + + + } onPress={handleLogout} > 退出登录 @@ -385,70 +472,94 @@ export default function AdminLayout({
{/* 主内容 */} -
+
{children}
{/* 修改密码弹窗 */} - { onOpenChange(); resetPasswordForm(); }} - size="2xl" - scrollBehavior="outside" - backdrop="blur" - placement="center" > - - {(onClose: () => void) => ( + + {(onClose: () => void) => ( <> - 修改密码 + + 修改密码 + -
- ) => setPasswordForm(prev => ({ ...prev, newUsername: e.target.value }))} - variant="bordered" - /> - ) => setPasswordForm(prev => ({ ...prev, currentPassword: e.target.value }))} - variant="bordered" - /> - ) => setPasswordForm(prev => ({ ...prev, newPassword: e.target.value }))} - variant="bordered" - /> - ) => setPasswordForm(prev => ({ ...prev, confirmPassword: e.target.value }))} - variant="bordered" - /> -
+
+ ) => + setPasswordForm((prev) => ({ + ...prev, + newUsername: e.target.value, + })) + } + /> + ) => + setPasswordForm((prev) => ({ + ...prev, + currentPassword: e.target.value, + })) + } + /> + ) => + setPasswordForm((prev) => ({ + ...prev, + newPassword: e.target.value, + })) + } + /> + ) => + setPasswordForm((prev) => ({ + ...prev, + confirmPassword: e.target.value, + })) + } + /> +
- @@ -459,4 +570,4 @@ export default function AdminLayout({
); -} \ No newline at end of file +} diff --git a/vite-frontend/src/layouts/h5-simple.tsx b/vite-frontend/src/layouts/h5-simple.tsx index 7a12efa..e4d1b29 100644 --- a/vite-frontend/src/layouts/h5-simple.tsx +++ b/vite-frontend/src/layouts/h5-simple.tsx @@ -1,9 +1,9 @@ -import React from 'react'; -import { useNavigate, useLocation } from 'react-router-dom'; +import React from "react"; +import { useNavigate, useLocation } from "react-router-dom"; import { Button } from "@heroui/button"; -import { Logo } from '@/components/icons'; -import { siteConfig } from '@/config/site'; +import { Logo } from "@/components/icons"; +import { siteConfig } from "@/config/site"; export default function H5SimpleLayout({ children, @@ -12,11 +12,11 @@ export default function H5SimpleLayout({ }) { const navigate = useNavigate(); const location = useLocation(); - + // 路由切换时回到顶部,避免上一页滚动位置保留 React.useEffect(() => { try { - window.scrollTo({ top: 0, left: 0, behavior: 'auto' }); + window.scrollTo({ top: 0, left: 0, behavior: "auto" }); } catch (e) { window.scrollTo(0, 0); } @@ -25,7 +25,7 @@ export default function H5SimpleLayout({ }, [location.pathname]); const handleBack = () => { - navigate('/profile'); + navigate("/profile"); }; return ( @@ -33,28 +33,26 @@ export default function H5SimpleLayout({ {/* 顶部导航栏 */}
- -

{siteConfig.name}

+

+ {siteConfig.name} +

-
-
+
{/* 主内容区域 */} -
- {children} -
+
{children}
); } diff --git a/vite-frontend/src/layouts/h5.tsx b/vite-frontend/src/layouts/h5.tsx index 3642e3c..533e1e5 100644 --- a/vite-frontend/src/layouts/h5.tsx +++ b/vite-frontend/src/layouts/h5.tsx @@ -1,8 +1,8 @@ -import React, { useState, useEffect } from 'react'; -import { useNavigate, useLocation } from 'react-router-dom'; +import React, { useState, useEffect } from "react"; +import { useNavigate, useLocation } from "react-router-dom"; -import { Logo } from '@/components/icons'; -import { siteConfig } from '@/config/site'; +import { Logo } from "@/components/icons"; +import { siteConfig } from "@/config/site"; interface TabItem { path: string; @@ -11,13 +11,7 @@ interface TabItem { adminOnly?: boolean; } - - -export default function H5Layout({ - children, -}: { - children: React.ReactNode; -}) { +export default function H5Layout({ children }: { children: React.ReactNode }) { const navigate = useNavigate(); const location = useLocation(); const [isAdmin, setIsAdmin] = useState(false); @@ -25,65 +19,77 @@ export default function H5Layout({ // Tabbar配置 const tabItems: TabItem[] = [ { - path: '/dashboard', - label: '首页', + path: "/dashboard", + label: "首页", icon: ( - ) + ), }, { - path: '/forward', - label: '转发', + path: "/forward", + label: "转发", icon: ( - - - ) - }, - { - path: '/tunnel', - label: '隧道', - icon: ( - - + ), - adminOnly: true }, { - path: '/node', - label: '节点', + path: "/tunnel", + label: "隧道", icon: ( - + ), - adminOnly: true + adminOnly: true, }, { - path: '/profile', - label: '我的', + path: "/node", + label: "节点", + icon: ( + + + + ), + adminOnly: true, + }, + { + path: "/profile", + label: "我的", icon: ( - ) - } + ), + }, ]; useEffect(() => { - // 兼容处理:如果没有admin字段,根据role_id判断(0为管理员) - let adminFlag = localStorage.getItem('admin') === 'true'; - if (localStorage.getItem('admin') === null) { - const roleId = parseInt(localStorage.getItem('role_id') || '1', 10); + let adminFlag = localStorage.getItem("admin") === "true"; + + if (localStorage.getItem("admin") === null) { + const roleId = parseInt(localStorage.getItem("role_id") || "1", 10); + adminFlag = roleId === 0; // 补充设置admin字段,避免下次再次判断 - localStorage.setItem('admin', adminFlag.toString()); + localStorage.setItem("admin", adminFlag.toString()); } - setIsAdmin(adminFlag); }, []); @@ -94,14 +100,14 @@ export default function H5Layout({ }; // 过滤tab项(根据权限) - const filteredTabItems = tabItems.filter(item => - !item.adminOnly || isAdmin + const filteredTabItems = tabItems.filter( + (item) => !item.adminOnly || isAdmin, ); // 路由切换时回到页面顶部,避免上一页的滚动位置遗留 useEffect(() => { try { - window.scrollTo({ top: 0, left: 0, behavior: 'auto' }); + window.scrollTo({ top: 0, left: 0, behavior: "auto" }); } catch (e) { window.scrollTo(0, 0); } @@ -115,17 +121,16 @@ export default function H5Layout({
-

{siteConfig.name}

+

+ {siteConfig.name} +

-
-
+
{/* 主内容区域 */} -
- {children} -
+
{children}
{/* 用于给固定 Tabbar 腾出空间的占位元素 */}
@@ -134,28 +139,27 @@ export default function H5Layout({ -
); } diff --git a/vite-frontend/src/main.tsx b/vite-frontend/src/main.tsx index 2f5a15e..961f872 100644 --- a/vite-frontend/src/main.tsx +++ b/vite-frontend/src/main.tsx @@ -1,4 +1,3 @@ - import ReactDOM from "react-dom/client"; import { BrowserRouter } from "react-router-dom"; @@ -11,6 +10,5 @@ ReactDOM.createRoot(document.getElementById("root")!).render( - + , ); - diff --git a/vite-frontend/src/pages/change-password.tsx b/vite-frontend/src/pages/change-password.tsx index 0557250..4a470f8 100644 --- a/vite-frontend/src/pages/change-password.tsx +++ b/vite-frontend/src/pages/change-password.tsx @@ -3,7 +3,7 @@ import { Input } from "@heroui/input"; import { Card, CardBody, CardHeader } from "@heroui/card"; import { useState } from "react"; import { useNavigate } from "react-router-dom"; -import toast from 'react-hot-toast'; +import toast from "react-hot-toast"; import { title } from "@/components/primitives"; import { updatePassword } from "@/api"; @@ -19,10 +19,10 @@ interface PasswordForm { export default function ChangePasswordPage() { const [form, setForm] = useState({ - newUsername: '', - currentPassword: '', - newPassword: '', - confirmPassword: '' + newUsername: "", + currentPassword: "", + newPassword: "", + confirmPassword: "", }); const [loading, setLoading] = useState(false); const [errors, setErrors] = useState>({}); @@ -32,39 +32,40 @@ export default function ChangePasswordPage() { const newErrors: Partial = {}; if (!form.newUsername.trim()) { - newErrors.newUsername = '请输入新用户名'; + newErrors.newUsername = "请输入新用户名"; } else if (form.newUsername.length < 3) { - newErrors.newUsername = '用户名长度至少3位'; + newErrors.newUsername = "用户名长度至少3位"; } else if (form.newUsername.length > 20) { - newErrors.newUsername = '用户名长度不能超过20位'; + newErrors.newUsername = "用户名长度不能超过20位"; } if (!form.currentPassword.trim()) { - newErrors.currentPassword = '请输入当前密码'; + newErrors.currentPassword = "请输入当前密码"; } if (!form.newPassword.trim()) { - newErrors.newPassword = '请输入新密码'; + newErrors.newPassword = "请输入新密码"; } else if (form.newPassword.length < 6) { - newErrors.newPassword = '新密码长度不能少于6位'; + newErrors.newPassword = "新密码长度不能少于6位"; } else if (form.newPassword.length > 20) { - newErrors.newPassword = '新密码长度不能超过20位'; + newErrors.newPassword = "新密码长度不能超过20位"; } if (!form.confirmPassword.trim()) { - newErrors.confirmPassword = '请再次输入新密码'; + newErrors.confirmPassword = "请再次输入新密码"; } else if (form.confirmPassword !== form.newPassword) { - newErrors.confirmPassword = '两次输入密码不一致'; + newErrors.confirmPassword = "两次输入密码不一致"; } setErrors(newErrors); + return Object.keys(newErrors).length === 0; }; const handleInputChange = (field: keyof PasswordForm, value: string) => { - setForm(prev => ({ ...prev, [field]: value })); + setForm((prev) => ({ ...prev, [field]: value })); if (errors[field]) { - setErrors(prev => ({ ...prev, [field]: undefined })); + setErrors((prev) => ({ ...prev, [field]: undefined })); } }; @@ -74,23 +75,23 @@ export default function ChangePasswordPage() { setLoading(true); try { const response = await updatePassword(form); - + if (response.code === 0) { - toast.success(response.msg || '账号密码修改成功'); - + toast.success(response.msg || "账号密码修改成功"); + // 使用 toast 确认对话框的替代方案 setTimeout(() => { - toast.success('即将跳转到登陆页面,请重新登录'); + toast.success("即将跳转到登陆页面,请重新登录"); setTimeout(() => { logout(); }, 1000); }, 1000); } else { - toast.error(response.msg || '账号密码修改失败'); + toast.error(response.msg || "账号密码修改失败"); } } catch (error) { - console.error('修改账号密码错误:', error); - toast.error('修改账号密码时发生错误'); + console.error("修改账号密码错误:", error); + toast.error("修改账号密码时发生错误"); } finally { setLoading(false); } @@ -98,11 +99,11 @@ export default function ChangePasswordPage() { const logout = () => { safeLogout(); - navigate('/'); + navigate("/"); }; const handleKeyPress = (e: React.KeyboardEvent) => { - if (e.key === 'Enter' && !loading) { + if (e.key === "Enter" && !loading) { handleSubmit(); } }; @@ -114,79 +115,97 @@ export default function ChangePasswordPage() {
- - + +
- +

安全提醒

-

检测到您使用的是默认账号密码,为了您的账户安全,请立即修改

+

+ 检测到您使用的是默认账号密码,为了您的账户安全,请立即修改 +

- +
handleInputChange('newUsername', e.target.value)} - onKeyDown={handleKeyPress} variant="bordered" - isDisabled={loading} - isInvalid={!!errors.newUsername} - errorMessage={errors.newUsername} + onChange={(e) => + handleInputChange("newUsername", e.target.value) + } + onKeyDown={handleKeyPress} /> - + handleInputChange('currentPassword', e.target.value)} - onKeyDown={handleKeyPress} variant="bordered" - isDisabled={loading} - isInvalid={!!errors.currentPassword} - errorMessage={errors.currentPassword} + onChange={(e) => + handleInputChange("currentPassword", e.target.value) + } + onKeyDown={handleKeyPress} /> - + handleInputChange('newPassword', e.target.value)} - onKeyDown={handleKeyPress} variant="bordered" - isDisabled={loading} - isInvalid={!!errors.newPassword} - errorMessage={errors.newPassword} + onChange={(e) => + handleInputChange("newPassword", e.target.value) + } + onKeyDown={handleKeyPress} /> - + handleInputChange('confirmPassword', e.target.value)} - onKeyDown={handleKeyPress} variant="bordered" - isDisabled={loading} - isInvalid={!!errors.confirmPassword} - errorMessage={errors.confirmPassword} + onChange={(e) => + handleInputChange("confirmPassword", e.target.value) + } + onKeyDown={handleKeyPress} /> - + - +
⚠️ 注意:修改账号密码后需要重新登录
@@ -197,4 +216,4 @@ export default function ChangePasswordPage() { ); -} \ No newline at end of file +} diff --git a/vite-frontend/src/pages/config.tsx b/vite-frontend/src/pages/config.tsx index a60e4e3..c46ad5f 100644 --- a/vite-frontend/src/pages/config.tsx +++ b/vite-frontend/src/pages/config.tsx @@ -1,5 +1,5 @@ -import { useState, useEffect } from 'react'; -import { useNavigate } from 'react-router-dom'; +import { useState, useEffect } from "react"; +import { useNavigate } from "react-router-dom"; import { Button } from "@heroui/button"; import { Card, CardBody, CardHeader } from "@heroui/card"; import { Input } from "@heroui/input"; @@ -7,23 +7,27 @@ import { Spinner } from "@heroui/spinner"; import { Divider } from "@heroui/divider"; import { Switch } from "@heroui/switch"; import { Select, SelectItem } from "@heroui/select"; -import toast from 'react-hot-toast'; -import { updateConfigs } from '@/api'; -import { SettingsIcon } from '@/components/icons'; +import toast from "react-hot-toast"; -import { isAdmin } from '@/utils/auth'; -import { getCachedConfigs, clearConfigCache, updateSiteConfig } from '@/config/site'; +import { updateConfigs } from "@/api"; +import { SettingsIcon } from "@/components/icons"; +import { isAdmin } from "@/utils/auth"; +import { + getCachedConfigs, + clearConfigCache, + updateSiteConfig, +} from "@/config/site"; // 简单的保存图标组件 const SaveIcon = ({ className }: { className?: string }) => ( @@ -36,7 +40,7 @@ interface ConfigItem { label: string; placeholder?: string; description?: string; - type: 'input' | 'switch' | 'select'; + type: "input" | "switch" | "select"; options?: { label: string; value: string; description?: string }[]; dependsOn?: string; // 依赖的配置项key dependsValue?: string; // 依赖的配置项值 @@ -45,96 +49,102 @@ interface ConfigItem { // 网站配置项定义 const CONFIG_ITEMS: ConfigItem[] = [ { - key: 'ip', - label: '面板后端地址', - placeholder: '请输入面板后端IP:PORT', - description: '格式“ip:port”,用于对接节点时使用,ip是你安装面板服务器的公网ip,端口是安装脚本内输入的后端端口。不要套CDN,不支持https,通讯数据有加密', - type: 'input' + key: "ip", + label: "面板后端地址", + placeholder: "请输入面板后端IP:PORT", + description: + "格式“ip:port”,用于对接节点时使用,ip是你安装面板服务器的公网ip,端口是安装脚本内输入的后端端口。不要套CDN,不支持https,通讯数据有加密", + type: "input", }, { - key: 'app_name', - label: '应用名称', - placeholder: '请输入应用名称', - description: '在浏览器标签页和导航栏显示的应用名称', - type: 'input' + key: "app_name", + label: "应用名称", + placeholder: "请输入应用名称", + description: "在浏览器标签页和导航栏显示的应用名称", + type: "input", }, { - key: 'captcha_enabled', - label: '启用验证码', - description: '开启后,用户登录时需要完成验证码验证', - type: 'switch' + key: "captcha_enabled", + label: "启用验证码", + description: "开启后,用户登录时需要完成验证码验证", + type: "switch", }, { - key: 'captcha_type', - label: '验证码类型', - description: '选择验证码的显示类型,不同类型有不同的安全级别', - type: 'select', - dependsOn: 'captcha_enabled', - dependsValue: 'true', + key: "captcha_type", + label: "验证码类型", + description: "选择验证码的显示类型,不同类型有不同的安全级别", + type: "select", + dependsOn: "captcha_enabled", + dependsValue: "true", options: [ - { - label: '随机类型', - value: 'RANDOM', - description: '系统随机选择验证码类型' + { + label: "随机类型", + value: "RANDOM", + description: "系统随机选择验证码类型", }, - { - label: '滑块验证码', - value: 'SLIDER', - description: '拖动滑块完成拼图验证' + { + label: "滑块验证码", + value: "SLIDER", + description: "拖动滑块完成拼图验证", }, - { - label: '文字点选验证码', - value: 'WORD_IMAGE_CLICK', - description: '按顺序点击指定文字' + { + label: "文字点选验证码", + value: "WORD_IMAGE_CLICK", + description: "按顺序点击指定文字", }, - { - label: '旋转验证码', - value: 'ROTATE', - description: '旋转图片到正确角度' + { + label: "旋转验证码", + value: "ROTATE", + description: "旋转图片到正确角度", }, - { - label: '拼图验证码', - value: 'CONCAT', - description: '拖动滑块完成图片拼接' - } - ] - } + { + label: "拼图验证码", + value: "CONCAT", + description: "拖动滑块完成图片拼接", + }, + ], + }, ]; // 初始化时从缓存读取配置,避免闪烁 const getInitialConfigs = (): Record => { - if (typeof window === 'undefined') return {}; - - const configKeys = ['app_name', 'captcha_enabled', 'captcha_type', 'ip']; + if (typeof window === "undefined") return {}; + + const configKeys = ["app_name", "captcha_enabled", "captcha_type", "ip"]; const initialConfigs: Record = {}; - + try { - configKeys.forEach(key => { - const cachedValue = localStorage.getItem('vite_config_' + key); + configKeys.forEach((key) => { + const cachedValue = localStorage.getItem("vite_config_" + key); + if (cachedValue) { initialConfigs[key] = cachedValue; } }); - } catch (error) { - } - + } catch (error) {} + return initialConfigs; }; export default function ConfigPage() { const navigate = useNavigate(); const initialConfigs = getInitialConfigs(); - const [configs, setConfigs] = useState>(initialConfigs); - const [loading, setLoading] = useState(Object.keys(initialConfigs).length === 0); // 如果有缓存数据,不显示loading + const [configs, setConfigs] = + useState>(initialConfigs); + const [loading, setLoading] = useState( + Object.keys(initialConfigs).length === 0, + ); // 如果有缓存数据,不显示loading const [saving, setSaving] = useState(false); const [hasChanges, setHasChanges] = useState(false); - const [originalConfigs, setOriginalConfigs] = useState>(initialConfigs); + const [originalConfigs, setOriginalConfigs] = + useState>(initialConfigs); // 权限检查 useEffect(() => { if (!isAdmin()) { - toast.error('权限不足,只有管理员可以访问此页面'); - navigate('/dashboard', { replace: true }); + toast.error("权限不足,只有管理员可以访问此页面"); + navigate("/dashboard", { replace: true }); + return; } }, [navigate]); @@ -143,17 +153,19 @@ export default function ConfigPage() { const loadConfigs = async (currentConfigs?: Record) => { const configsToCompare = currentConfigs || configs; const hasInitialData = Object.keys(configsToCompare).length > 0; - + // 如果已有缓存数据,不显示loading,静默更新 if (!hasInitialData) { setLoading(true); } - + try { const configData = await getCachedConfigs(); - + // 只有在数据有变化时才更新 - const hasDataChanged = JSON.stringify(configData) !== JSON.stringify(configsToCompare); + const hasDataChanged = + JSON.stringify(configData) !== JSON.stringify(configsToCompare); + if (hasDataChanged) { setConfigs(configData); setOriginalConfigs({ ...configData }); @@ -163,7 +175,7 @@ export default function ConfigPage() { } catch (error) { // 只有在没有缓存数据时才显示错误 if (!hasInitialData) { - toast.error('加载配置出错,请重试'); + toast.error("加载配置出错,请重试"); } } finally { setLoading(false); @@ -182,22 +194,25 @@ export default function ConfigPage() { // 处理配置项变更 const handleConfigChange = (key: string, value: string) => { let newConfigs = { ...configs, [key]: value }; - + // 特殊处理:启用验证码时,如果验证码类型未设置,默认为随机 - if (key === 'captcha_enabled' && value === 'true') { + if (key === "captcha_enabled" && value === "true") { if (!newConfigs.captcha_type) { - newConfigs.captcha_type = 'RANDOM'; + newConfigs.captcha_type = "RANDOM"; } } - + setConfigs(newConfigs); - + // 检查是否有变更 - const hasChangesNow = Object.keys(newConfigs).some( - k => newConfigs[k] !== originalConfigs[k] - ) || Object.keys(originalConfigs).some( - k => originalConfigs[k] !== newConfigs[k] - ); + const hasChangesNow = + Object.keys(newConfigs).some( + (k) => newConfigs[k] !== originalConfigs[k], + ) || + Object.keys(originalConfigs).some( + (k) => originalConfigs[k] !== newConfigs[k], + ); + setHasChanges(hasChangesNow); }; @@ -206,112 +221,115 @@ export default function ConfigPage() { setSaving(true); try { const response = await updateConfigs(configs); + if (response.code === 0) { - toast.success('配置保存成功'); - + toast.success("配置保存成功"); + // 清除所有配置缓存,强制下次重新获取 clearConfigCache(); - + // 获取变更的配置项 const changedKeys = Object.keys(configs).filter( - key => configs[key] !== originalConfigs[key] + (key) => configs[key] !== originalConfigs[key], ); - + setOriginalConfigs({ ...configs }); setHasChanges(false); - + // 如果应用名称发生变化,立即更新网站配置 - if (changedKeys.includes('app_name')) { + if (changedKeys.includes("app_name")) { await updateSiteConfig(); } - + // 触发配置更新事件,通知其他组件 - window.dispatchEvent(new CustomEvent('configUpdated', { - detail: { changedKeys } - })); + window.dispatchEvent( + new CustomEvent("configUpdated", { + detail: { changedKeys }, + }), + ); } else { - toast.error('保存配置失败: ' + response.msg); + toast.error("保存配置失败: " + response.msg); } } catch (error) { - toast.error('保存配置出错,请重试'); + toast.error("保存配置出错,请重试"); } finally { setSaving(false); } }; - - // 检查配置项是否应该显示(依赖检查) const shouldShowItem = (item: ConfigItem): boolean => { if (!item.dependsOn || !item.dependsValue) { return true; } + return configs[item.dependsOn] === item.dependsValue; }; // 渲染不同类型的配置项 const renderConfigItem = (item: ConfigItem) => { - const isChanged = hasChanges && configs[item.key] !== originalConfigs[item.key]; - + const isChanged = + hasChanges && configs[item.key] !== originalConfigs[item.key]; + switch (item.type) { - case 'input': + case "input": return ( - {configs[item.key] === 'true' ? '已启用' : '已禁用'} + {configs[item.key] === "true" ? "已启用" : "已禁用"} ); - case 'select': + case "select": return ( +
+ {sortedForwards.map((forward) => + forward && forward.id ? ( + + ) : null, + )} +
+ + + ) : ( + /* 空状态 */ + + +
+
+ + + +
+
+

+ 暂无转发配置 +

+

+ 还没有创建任何转发配置,点击上方按钮开始创建 +

+
+
+
+
+ )} + + {/* 新增/编辑模态框 */} + + + {(onClose) => ( + <> + +

+ {isEdit ? "编辑转发" : "新增转发"} +

+

+ {isEdit ? "修改现有转发配置的信息" : "创建新的转发配置"} +

+
+ +
+ setForm(prev => ({ ...prev, name: e.target.value }))} - isInvalid={!!errors.name} - errorMessage={errors.name} variant="bordered" - /> - - { const selectedKey = Array.from(keys)[0] as string; if (selectedKey) { handleTunnelChange(selectedKey); } }} - isInvalid={!!errors.tunnelId} - errorMessage={errors.tunnelId} - variant="bordered" - description={isEdit ? "更改隧道将释放原端口并在新隧道分配端口" : undefined} - > - {tunnels.map((tunnel) => ( - - {tunnel.name} - - ))} - + } + > + {tunnels.map((tunnel) => ( + {tunnel.name} + ))} + - { const value = e.target.value; setForm(prev => ({ @@ -1643,771 +1926,972 @@ export default function ForwardPage() { inPort: value ? parseInt(value) : null })); }} - isInvalid={!!errors.inPort} - errorMessage={errors.inPort} - variant="bordered" - description="指定入口端口,留空则从节点可用端口中自动分配" - /> - -