diff --git a/vite-frontend/package.json b/vite-frontend/package.json index aea3441..e695e36 100644 --- a/vite-frontend/package.json +++ b/vite-frontend/package.json @@ -13,45 +13,31 @@ "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", - "@heroui/accordion": "^2.2.21", - "@heroui/alert": "^2.2.24", - "@heroui/autocomplete": "^2.3.25", - "@heroui/avatar": "^2.2.19", - "@heroui/badge": "^2.2.15", - "@heroui/button": "2.2.23", - "@heroui/card": "^2.2.22", - "@heroui/chip": "^2.2.19", - "@heroui/code": "2.2.17", - "@heroui/date-picker": "^2.3.22", - "@heroui/dropdown": "2.3.23", - "@heroui/image": "^2.2.15", - "@heroui/input": "2.4.24", - "@heroui/kbd": "2.2.18", - "@heroui/link": "2.2.20", - "@heroui/modal": "^2.2.21", - "@heroui/navbar": "2.2.21", - "@heroui/pagination": "^2.2.21", - "@heroui/progress": "^2.2.20", - "@heroui/radio": "^2.3.24", - "@heroui/select": "^2.4.22", - "@heroui/snippet": "2.2.24", - "@heroui/spinner": "^2.2.20", - "@heroui/switch": "^2.2.21", - "@heroui/system": "2.4.19", - "@heroui/table": "^2.2.24", - "@heroui/tabs": "^2.2.27", - "@heroui/theme": "2.4.24", - "@heroui/use-theme": "2.1.10", + "@radix-ui/react-accordion": "^1.2.0", + "@radix-ui/react-checkbox": "^1.1.0", + "@radix-ui/react-dialog": "^1.1.0", + "@radix-ui/react-dropdown-menu": "^2.1.0", + "@radix-ui/react-label": "^2.1.0", + "@radix-ui/react-progress": "^1.1.0", + "@radix-ui/react-radio-group": "^1.2.0", + "@radix-ui/react-select": "^2.1.0", + "@radix-ui/react-separator": "^1.1.0", + "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-switch": "^1.1.0", + "@radix-ui/react-tabs": "^1.1.0", "@marsidev/react-turnstile": "^1.1.0", - "@nextui-org/system": "^2.4.6", + "@internationalized/date": "^3.10.0", + "@react-aria/i18n": "^3.12.12", "@react-aria/visually-hidden": "3.8.25", "@react-types/shared": "3.30.0", "@tailwindcss/postcss": "4.1.11", "@tailwindcss/vite": "^4.1.18", "@types/react-beautiful-dnd": "^13.1.8", "axios": "^1.11.0", + "class-variance-authority": "^0.7.1", "clsx": "2.1.1", "framer-motion": "11.18.2", + "lucide-react": "^0.542.0", "react": "18.3.1", "react-beautiful-dnd": "^13.1.1", "react-dom": "18.3.1", @@ -60,8 +46,10 @@ "react-router-dom": "6.30.3", "recharts": "^3.1.1", "sonner": "^2.0.6", + "tailwind-merge": "^2.5.5", "tailwind-variants": "1.0.0", - "tailwindcss": "4.1.11" + "tailwindcss": "4.1.11", + "tw-animate-css": "^1.3.0" }, "devDependencies": { "@eslint/compat": "1.2.8", diff --git a/vite-frontend/src/App.tsx b/vite-frontend/src/App.tsx index d7c1528..a882f13 100644 --- a/vite-frontend/src/App.tsx +++ b/vite-frontend/src/App.tsx @@ -1,5 +1,5 @@ import { Route, Routes, useNavigate } from "react-router-dom"; -import { useEffect, useState } from "react"; +import { useEffect } from "react"; import IndexPage from "@/pages/index"; import ChangePasswordPage from "@/pages/change-password"; @@ -19,50 +19,7 @@ import H5Layout from "@/layouts/h5"; import H5SimpleLayout from "@/layouts/h5-simple"; import { isLoggedIn } from "@/utils/auth"; import { siteConfig } from "@/config/site"; - -// 检测是否为H5模式 -const useH5Mode = () => { - // 立即检测H5模式,避免初始渲染时的闪屏 - const getInitialH5Mode = () => { - // 检测移动设备或小屏幕 - const isMobile = window.innerWidth <= 768; - // 检测是否为移动端浏览器 - 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"; - - return isMobile || isMobileBrowser || isH5Param; - }; - - const [isH5, setIsH5] = useState(getInitialH5Mode); - - useEffect(() => { - const checkH5Mode = () => { - // 检测移动设备或小屏幕 - const isMobile = window.innerWidth <= 768; - // 检测是否为移动端浏览器 - 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"; - - setIsH5(isMobile || isMobileBrowser || isH5Param); - }; - - window.addEventListener("resize", checkH5Mode); - - return () => window.removeEventListener("resize", checkH5Mode); - }, []); - - return isH5; -}; +import { useH5Mode } from "@/hooks/useH5Mode"; // 简化的路由保护组件 - 使用 React Router 导航避免循环 const ProtectedRoute = ({ @@ -99,15 +56,8 @@ const ProtectedRoute = ({ } // 根据模式和页面类型选择布局 - let Layout; - - if (isH5 && useSimpleLayout) { - Layout = H5SimpleLayout; - } else if (isH5) { - Layout = H5Layout; - } else { - Layout = AdminLayout; - } + const Layout = + isH5 && useSimpleLayout ? H5SimpleLayout : isH5 ? H5Layout : AdminLayout; return {children}; }; diff --git a/vite-frontend/src/api/index.ts b/vite-frontend/src/api/index.ts index 5cdba72..bea1164 100644 --- a/vite-frontend/src/api/index.ts +++ b/vite-frontend/src/api/index.ts @@ -1,3 +1,32 @@ +import type { + BatchOperationResult, + ForwardDiagnosisApiData, + ForwardApiItem, + GroupPermissionApiItem, + NodeReleaseApiItem, + NodeApiItem, + SpeedLimitApiItem, + TunnelDiagnosisApiData, + TunnelGroupApiItem, + UserApiItem, + UserGroupApiItem, + UserListQuery, + UserPackageInfoApiData, + UserTunnelPermissionApiItem, + TunnelApiItem, + UserTunnelApiItem, + UserMutationPayload, + NodeMutationPayload, + TunnelMutationPayload, + UserTunnelAssignPayload, + UserTunnelListQuery, + UserTunnelRemovePayload, + ForwardMutationPayload, + SpeedLimitMutationPayload, + UpdatePasswordPayload, + BackupImportPayload, +} from "./types"; + import axios from "axios"; import Network from "./network"; @@ -20,20 +49,25 @@ 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 updateUser = (data: any) => Network.post("/user/update", data); +export const createUser = (data: UserMutationPayload) => + Network.post("/user/create", data); +export const getAllUsers = (pageData: UserListQuery = {}) => + Network.post("/user/list", pageData); +export const updateUser = (data: UserMutationPayload) => + Network.post("/user/update", data); export const deleteUser = (id: number) => Network.post("/user/delete", { id }); -export const getUserPackageInfo = () => Network.post("/user/package"); +export const getUserPackageInfo = () => + Network.post("/user/package"); // 节点CRUD操作 - 全部使用POST请求 -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 createNode = (data: NodeMutationPayload) => + Network.post("/node/create", data); +export const getNodeList = () => Network.post("/node/list"); +export const updateNode = (data: NodeMutationPayload) => + 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 }); + Network.post("/node/install", { id }); export const updateNodeOrder = (data: { nodes: Array<{ id: number; inx: number }>; }) => Network.post("/node/update-order", data); @@ -55,44 +89,50 @@ export const batchUpgradeNodes = (ids: number[], version?: string) => { ids, version: version || "" }, { timeout: 15 * 60 * 1000 }, ); -export const getNodeReleases = () => Network.post("/node/releases"); +export const getNodeReleases = () => + Network.post("/node/releases"); export const rollbackNode = (id: number) => Network.post("/node/rollback", { id }); // 隧道CRUD操作 - 全部使用POST请求 -export const createTunnel = (data: any) => Network.post("/tunnel/create", data); -export const getTunnelList = () => Network.post("/tunnel/list"); +export const createTunnel = (data: TunnelMutationPayload) => + Network.post("/tunnel/create", data); +export const getTunnelList = () => + Network.post("/tunnel/list"); export const getTunnelById = (id: number) => - Network.post("/tunnel/get", { id }); -export const updateTunnel = (data: any) => Network.post("/tunnel/update", data); + Network.post("/tunnel/get", { id }); +export const updateTunnel = (data: TunnelMutationPayload) => + 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 }); + 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) => +export const assignUserTunnel = (data: UserTunnelAssignPayload) => 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) => +export const getUserTunnelList = (queryData: UserTunnelListQuery = {}) => + Network.post("/tunnel/user/list", queryData); +export const removeUserTunnel = (params: UserTunnelRemovePayload) => Network.post("/tunnel/user/remove", params); -export const updateUserTunnel = (data: any) => +export const updateUserTunnel = (data: UserTunnelAssignPayload) => Network.post("/tunnel/user/update", data); -export const userTunnel = () => Network.post("/tunnel/user/tunnel"); +export const userTunnel = () => + Network.post("/tunnel/user/tunnel"); // 转发CRUD操作 - 全部使用POST请求 -export const createForward = (data: any) => +export const createForward = (data: ForwardMutationPayload) => Network.post("/forward/create", data); -export const getForwardList = () => Network.post("/forward/list"); -export const updateForward = (data: any) => +export const getForwardList = () => + Network.post("/forward/list"); +export const updateForward = (data: ForwardMutationPayload) => Network.post("/forward/update", data); export const deleteForward = (id: number) => Network.post("/forward/delete", { id }); @@ -107,7 +147,7 @@ export const resumeForwardService = (forwardId: number) => // 转发诊断操作 export const diagnoseForward = (forwardId: number) => - Network.post("/forward/diagnose", { forwardId }); + Network.post("/forward/diagnose", { forwardId }); // 转发排序操作 export const updateForwardOrder = (data: { @@ -115,16 +155,17 @@ export const updateForwardOrder = (data: { }) => Network.post("/forward/update-order", data); // 限速规则CRUD操作 - 全部使用POST请求 -export const createSpeedLimit = (data: any) => +export const createSpeedLimit = (data: SpeedLimitMutationPayload) => Network.post("/speed-limit/create", data); -export const getSpeedLimitList = () => Network.post("/speed-limit/list"); -export const updateSpeedLimit = (data: any) => +export const getSpeedLimitList = () => + Network.post("/speed-limit/list"); +export const updateSpeedLimit = (data: SpeedLimitMutationPayload) => Network.post("/speed-limit/update", data); export const deleteSpeedLimit = (id: number) => Network.post("/speed-limit/delete", { id }); // 修改密码接口 -export const updatePassword = (data: any) => +export const updatePassword = (data: UpdatePasswordPayload) => Network.post("/user/updatePassword", data); // 重置流量接口 @@ -135,18 +176,19 @@ export const getUserGroups = (id: number) => Network.post("/user/groups", { id }); // 网站配置相关接口 -export const getConfigs = () => Network.post("/config/list"); +export const getConfigs = () => + Network.post>("/config/list"); export const getConfigByName = (name: string) => - Network.post("/config/get", { name }); + Network.post<{ name: string; value: string }>("/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 exportBackupData = () => Network.post("/backup/export"); -export const importBackupData = (data: any) => +export const importBackupData = (data: BackupImportPayload) => Network.post("/backup/import", data); -export const restoreBackupData = (data: any) => +export const restoreBackupData = (data: BackupImportPayload) => Network.post("/backup/restore", data); // 验证码相关接口 @@ -157,26 +199,27 @@ export const verifyCaptcha = (data: { captchaId: string; trackData: string }) => // 批量操作接口 export const batchDeleteForwards = (ids: number[]) => - Network.post("/forward/batch-delete", { ids }); + Network.post("/forward/batch-delete", { ids }); export const batchPauseForwards = (ids: number[]) => - Network.post("/forward/batch-pause", { ids }); + Network.post("/forward/batch-pause", { ids }); export const batchResumeForwards = (ids: number[]) => - Network.post("/forward/batch-resume", { ids }); + Network.post("/forward/batch-resume", { ids }); export const batchDeleteTunnels = (ids: number[]) => - Network.post("/tunnel/batch-delete", { ids }); + Network.post("/tunnel/batch-delete", { ids }); export const batchDeleteNodes = (ids: number[]) => - Network.post("/node/batch-delete", { ids }); + Network.post("/node/batch-delete", { ids }); export const batchRedeployForwards = (ids: number[]) => - Network.post("/forward/batch-redeploy", { ids }); + Network.post("/forward/batch-redeploy", { ids }); export const batchRedeployTunnels = (ids: number[]) => - Network.post("/tunnel/batch-redeploy", { ids }); + Network.post("/tunnel/batch-redeploy", { ids }); export const batchChangeTunnel = (data: { forwardIds: number[]; targetTunnelId: number; -}) => Network.post("/forward/batch-change-tunnel", data); +}) => Network.post("/forward/batch-change-tunnel", data); // 分组与权限分配接口 -export const getTunnelGroupList = () => Network.post("/group/tunnel/list"); +export const getTunnelGroupList = () => + Network.post("/group/tunnel/list"); export const createTunnelGroup = (data: { name: string; status?: number }) => Network.post("/group/tunnel/create", data); export const updateTunnelGroup = (data: { @@ -191,7 +234,8 @@ export const assignTunnelsToGroup = (data: { tunnelIds: number[]; }) => Network.post("/group/tunnel/assign", data); -export const getUserGroupList = () => Network.post("/group/user/list"); +export const getUserGroupList = () => + Network.post("/group/user/list"); export const createUserGroup = (data: { name: string; status?: number }) => Network.post("/group/user/create", data); export const updateUserGroup = (data: { @@ -207,7 +251,7 @@ export const assignUsersToGroup = (data: { }) => Network.post("/group/user/assign", data); export const getGroupPermissionList = () => - Network.post("/group/permission/list"); + Network.post("/group/permission/list"); export const assignGroupPermission = (data: { userGroupId: number; tunnelGroupId: number; @@ -216,7 +260,8 @@ export const removeGroupPermission = (id: number) => Network.post("/group/permission/remove", { id }); // 面板共享 (Federation) 接口 -export const getPeerShareList = () => Network.post("/federation/share/list"); +export const getPeerShareList = () => + Network.post>>("/federation/share/list"); export const createPeerShare = (data: { name: string; nodeId: number; @@ -242,7 +287,9 @@ export const deletePeerShare = (id: number) => export const resetPeerShareFlow = (id: number) => Network.post("/federation/share/reset-flow", { id }); export const getPeerRemoteUsageList = () => - Network.post("/federation/share/remote-usage/list"); + Network.post>>( + "/federation/share/remote-usage/list", + ); export const importRemoteNode = (data: { remoteUrl: string; token: string }) => Network.post("/federation/node/import", data); @@ -288,7 +335,7 @@ export const exportBackup = async (types: string[] = []) => { window.URL.revokeObjectURL(url); }; -export const importBackup = (data: { types: string[]; [key: string]: any }) => +export const importBackup = (data: BackupImportPayload) => Network.post("/backup/import", data); export interface AnnouncementData { diff --git a/vite-frontend/src/api/network.ts b/vite-frontend/src/api/network.ts index d7af375..0c00dc5 100644 --- a/vite-frontend/src/api/network.ts +++ b/vite-frontend/src/api/network.ts @@ -1,6 +1,11 @@ import axios, { AxiosResponse } from "axios"; +import { + extractApiErrorMessage, + isUnauthorizedError, +} from "@/api/error-message"; import { getPanelAddresses, isWebViewFunc } from "@/utils/panel"; +import { clearSession, getToken } from "@/utils/session"; interface PanelAddress { name: string; @@ -37,7 +42,7 @@ export const reinitializeBaseURL = () => { reinitializeBaseURL(); -interface ApiResponse { +interface ApiResponse { code: number; msg: string; data: T; @@ -49,10 +54,7 @@ interface RequestOptions { // 处理token失效的逻辑 function handleTokenExpired() { - // 清除localStorage中的token - window.localStorage.removeItem("token"); - window.localStorage.removeItem("role_id"); - window.localStorage.removeItem("name"); + clearSession(); // 跳转到登录页面 if (window.location.pathname !== "/") { @@ -61,7 +63,7 @@ function handleTokenExpired() { } // 检查响应是否为token失效 -function isTokenExpired(response: ApiResponse) { +function isTokenExpired(response: ApiResponse) { return ( response && response.code === 401 && @@ -72,9 +74,9 @@ function isTokenExpired(response: ApiResponse) { } const Network = { - get: function ( + get: function ( path: string = "", - data: any = {}, + data: unknown = {}, options: RequestOptions = {}, ): Promise> { return new Promise(function (resolve) { @@ -90,38 +92,45 @@ const Network = { params: data, timeout: options.timeout ?? 30000, headers: { - Authorization: window.localStorage.getItem("token"), + Authorization: getToken(), }, }) .then(function (response: AxiosResponse>) { // 检查是否token失效 if (isTokenExpired(response.data)) { handleTokenExpired(); - - return; } + resolve(response.data); }) - .catch(function (error: any) { + .catch(function (error: unknown) { + const errorMessage = extractApiErrorMessage(error); + // 检查是否是401错误(token失效) - if (error.response && error.response.status === 401) { + if (isUnauthorizedError(error)) { handleTokenExpired(); + resolve({ + code: 401, + msg: "未登录或token已过期", + data: null as T, + }); + return; } resolve({ code: -1, - msg: error.message || "网络请求失败", + msg: errorMessage, data: null as T, }); }); }); }, - post: function ( + post: function ( path: string = "", - data: any = {}, + data: unknown = {}, options: RequestOptions = {}, ): Promise> { return new Promise(function (resolve) { @@ -136,7 +145,7 @@ const Network = { .post(path, data, { timeout: options.timeout ?? 30000, headers: { - Authorization: window.localStorage.getItem("token"), + Authorization: getToken(), "Content-Type": "application/json", }, }) @@ -144,22 +153,29 @@ const Network = { // 检查是否token失效 if (isTokenExpired(response.data)) { handleTokenExpired(); - - return; } + resolve(response.data); }) - .catch(function (error: any) { + .catch(function (error: unknown) { + const errorMessage = extractApiErrorMessage(error); + // 检查是否是401错误(token失效) - if (error.response && error.response.status === 401) { + if (isUnauthorizedError(error)) { handleTokenExpired(); + resolve({ + code: 401, + msg: "未登录或token已过期", + data: null as T, + }); + return; } resolve({ code: -1, - msg: error.message || "网络请求失败", + msg: errorMessage, data: null as T, }); }); diff --git a/vite-frontend/src/components/navbar.tsx b/vite-frontend/src/components/navbar.tsx index b7b0c02..78d7c7a 100644 --- a/vite-frontend/src/components/navbar.tsx +++ b/vite-frontend/src/components/navbar.tsx @@ -1,26 +1,21 @@ -import { useState, useEffect } from "react"; -import { Link } from "@heroui/link"; +import { useEffect, useState } from "react"; +import { Link } from "@/shadcn-bridge/heroui/link"; import { Navbar as HeroUINavbar, NavbarBrand, NavbarContent, -} from "@heroui/navbar"; +} from "@/shadcn-bridge/heroui/navbar"; import { useNavigate } from "react-router-dom"; -import { isWebViewFunc } from "@/utils/panel"; import { Logo } from "@/components/icons"; import { siteConfig, getCachedConfig } from "@/config/site"; +import { useWebViewMode } from "@/hooks/useWebViewMode"; export const Navbar = () => { const navigate = useNavigate(); // 初始状态使用siteConfig中已经从缓存读取的值,避免闪烁 const [appName, setAppName] = useState(siteConfig.name); - const [isWebView, setIsWebView] = useState(false); - - // 检测是否在WebView中运行 - useEffect(() => { - setIsWebView(isWebViewFunc()); - }, []); + const isWebView = useWebViewMode(); useEffect(() => { // 异步检查是否有更新的配置 diff --git a/vite-frontend/src/components/theme-provider.tsx b/vite-frontend/src/components/theme-provider.tsx index 66cdecf..72c60c6 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 { useTheme } from "@/shadcn-bridge/heroui/use-theme"; interface ThemeProviderProps { children: React.ReactNode; diff --git a/vite-frontend/src/layouts/admin.tsx b/vite-frontend/src/layouts/admin.tsx index 5147725..508b083 100644 --- a/vite-frontend/src/layouts/admin.tsx +++ b/vite-frontend/src/layouts/admin.tsx @@ -1,12 +1,12 @@ import React, { useState, useEffect } from "react"; import { useNavigate, useLocation } from "react-router-dom"; -import { Button } from "@heroui/button"; +import { Button } from "@/shadcn-bridge/heroui/button"; import { Dropdown, DropdownTrigger, DropdownMenu, DropdownItem, -} from "@heroui/dropdown"; +} from "@/shadcn-bridge/heroui/dropdown"; import { Modal, ModalContent, @@ -14,14 +14,16 @@ import { ModalBody, ModalFooter, useDisclosure, -} from "@heroui/modal"; -import { Input } from "@heroui/input"; +} from "@/shadcn-bridge/heroui/modal"; +import { Input } from "@/shadcn-bridge/heroui/input"; 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 { useMobileBreakpoint } from "@/hooks/useMobileBreakpoint"; +import { getAdminFlag, getSessionName } from "@/utils/session"; interface MenuItem { path: string; @@ -46,7 +48,6 @@ export default function AdminLayout({ const location = useLocation(); const { isOpen, onOpen, onOpenChange } = useDisclosure(); - const [isMobile, setIsMobile] = useState(false); const [mobileMenuVisible, setMobileMenuVisible] = useState(false); const [username, setUsername] = useState(""); const [isAdmin, setIsAdmin] = useState(false); @@ -57,6 +58,7 @@ export default function AdminLayout({ newPassword: "", confirmPassword: "", }); + const isMobile = useMobileBreakpoint(); // 菜单项配置 const menuItems: MenuItem[] = [ @@ -170,41 +172,21 @@ export default function AdminLayout({ }, ]; - // 检查移动端 - const checkMobile = () => { - setIsMobile(window.innerWidth <= 768); - if (window.innerWidth > 768) { - setMobileMenuVisible(false); - } - }; - useEffect(() => { // 获取用户信息 - 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); - - adminFlag = roleId === 0; - // 补充设置admin字段,避免下次再次判断 - localStorage.setItem("admin", adminFlag.toString()); - } + const name = getSessionName() || "Admin"; + const adminFlag = getAdminFlag(); setUsername(name); setIsAdmin(adminFlag); - - // 响应式检查 - checkMobile(); - window.addEventListener("resize", checkMobile); - - return () => { - window.removeEventListener("resize", checkMobile); - }; }, []); + useEffect(() => { + if (!isMobile) { + setMobileMenuVisible(false); + } + }, [isMobile]); + // 退出登录 const handleLogout = () => { safeLogout(); diff --git a/vite-frontend/src/layouts/h5-simple.tsx b/vite-frontend/src/layouts/h5-simple.tsx index 7f77735..9943ade 100644 --- a/vite-frontend/src/layouts/h5-simple.tsx +++ b/vite-frontend/src/layouts/h5-simple.tsx @@ -1,9 +1,10 @@ import React from "react"; -import { useNavigate, useLocation } from "react-router-dom"; -import { Button } from "@heroui/button"; +import { useNavigate } from "react-router-dom"; +import { Button } from "@/shadcn-bridge/heroui/button"; import { Logo } from "@/components/icons"; import { siteConfig } from "@/config/site"; +import { useScrollTopOnPathChange } from "@/hooks/useScrollTopOnPathChange"; export default function H5SimpleLayout({ children, @@ -11,18 +12,8 @@ export default function H5SimpleLayout({ children: React.ReactNode; }) { const navigate = useNavigate(); - const location = useLocation(); - // 路由切换时回到顶部,避免上一页滚动位置保留 - React.useEffect(() => { - try { - window.scrollTo({ top: 0, left: 0, behavior: "auto" }); - } catch { - window.scrollTo(0, 0); - } - document.body.scrollTop = 0; - document.documentElement.scrollTop = 0; - }, [location.pathname]); + useScrollTopOnPathChange(); const handleBack = () => { navigate("/profile"); diff --git a/vite-frontend/src/layouts/h5.tsx b/vite-frontend/src/layouts/h5.tsx index db3f202..468d5e3 100644 --- a/vite-frontend/src/layouts/h5.tsx +++ b/vite-frontend/src/layouts/h5.tsx @@ -3,6 +3,8 @@ import { useNavigate, useLocation } from "react-router-dom"; import { Logo } from "@/components/icons"; import { siteConfig } from "@/config/site"; +import { getAdminFlag } from "@/utils/session"; +import { useScrollTopOnPathChange } from "@/hooks/useScrollTopOnPathChange"; interface TabItem { path: string; @@ -16,6 +18,8 @@ export default function H5Layout({ children }: { children: React.ReactNode }) { const location = useLocation(); const [isAdmin, setIsAdmin] = useState(false); + useScrollTopOnPathChange(); + // Tabbar配置 const tabItems: TabItem[] = [ { @@ -80,18 +84,7 @@ export default function H5Layout({ children }: { children: React.ReactNode }) { ]; 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); - - adminFlag = roleId === 0; - // 补充设置admin字段,避免下次再次判断 - localStorage.setItem("admin", adminFlag.toString()); - } - - setIsAdmin(adminFlag); + setIsAdmin(getAdminFlag()); }, []); // Tab点击处理 @@ -104,17 +97,6 @@ export default function H5Layout({ children }: { children: React.ReactNode }) { (item) => !item.adminOnly || isAdmin, ); - // 路由切换时回到页面顶部,避免上一页的滚动位置遗留 - useEffect(() => { - try { - window.scrollTo({ top: 0, left: 0, behavior: "auto" }); - } catch { - window.scrollTo(0, 0); - } - document.body.scrollTop = 0; - document.documentElement.scrollTop = 0; - }, [location.pathname]); - return ( {/* 顶部导航栏 */} diff --git a/vite-frontend/src/pages/change-password.tsx b/vite-frontend/src/pages/change-password.tsx index 99d09cb..907ec1e 100644 --- a/vite-frontend/src/pages/change-password.tsx +++ b/vite-frontend/src/pages/change-password.tsx @@ -1,6 +1,6 @@ -import { Button } from "@heroui/button"; -import { Input } from "@heroui/input"; -import { Card, CardBody, CardHeader } from "@heroui/card"; +import { Button } from "@/shadcn-bridge/heroui/button"; +import { Input } from "@/shadcn-bridge/heroui/input"; +import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card"; import { useState } from "react"; import { useNavigate } from "react-router-dom"; import toast from "react-hot-toast"; @@ -115,6 +115,7 @@ export default function ChangePasswordPage() { {loading ? "修改中..." : "立即修改账号密码"} diff --git a/vite-frontend/src/pages/config.tsx b/vite-frontend/src/pages/config.tsx index 6979199..5ad2f68 100644 --- a/vite-frontend/src/pages/config.tsx +++ b/vite-frontend/src/pages/config.tsx @@ -1,21 +1,21 @@ import { useState, useEffect, useRef } 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"; -import { Textarea } from "@heroui/input"; -import { Spinner } from "@heroui/spinner"; -import { Divider } from "@heroui/divider"; -import { Switch } from "@heroui/switch"; -import { Select, SelectItem } from "@heroui/select"; -import { Checkbox } from "@heroui/checkbox"; +import { Button } from "@/shadcn-bridge/heroui/button"; +import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card"; +import { Input } from "@/shadcn-bridge/heroui/input"; +import { Textarea } from "@/shadcn-bridge/heroui/input"; +import { Spinner } from "@/shadcn-bridge/heroui/spinner"; +import { Divider } from "@/shadcn-bridge/heroui/divider"; +import { Switch } from "@/shadcn-bridge/heroui/switch"; +import { Select, SelectItem } from "@/shadcn-bridge/heroui/select"; +import { Checkbox } from "@/shadcn-bridge/heroui/checkbox"; import { Modal, ModalBody, ModalContent, ModalFooter, ModalHeader, -} from "@heroui/modal"; +} from "@/shadcn-bridge/heroui/modal"; import toast from "react-hot-toast"; import { @@ -599,7 +599,7 @@ export default function ConfigPage() { disabled={!hasChanges} isLoading={saving} startContent={} - onClick={handleSave} + onPress={handleSave} > {saving ? "保存中..." : "保存配置"} @@ -715,7 +715,7 @@ export default function ConfigPage() { color="primary" isLoading={announcementSaving} startContent={} - onClick={saveAnnouncement} + onPress={saveAnnouncement} > 保存公告 diff --git a/vite-frontend/src/pages/dashboard.tsx b/vite-frontend/src/pages/dashboard.tsx index 0864326..6a1fb45 100644 --- a/vite-frontend/src/pages/dashboard.tsx +++ b/vite-frontend/src/pages/dashboard.tsx @@ -1,57 +1,18 @@ -import { Card, CardBody, CardHeader } from "@heroui/card"; -import { Button } from "@heroui/button"; -import { Modal, ModalContent, ModalHeader, ModalBody } from "@heroui/modal"; -import { useState, useEffect } from "react"; +import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card"; +import { Button } from "@/shadcn-bridge/heroui/button"; +import { Modal, ModalContent, ModalHeader, ModalBody } from "@/shadcn-bridge/heroui/modal"; +import { useState } from "react"; import toast from "react-hot-toast"; + +import { PageEmptyState, PageLoadingState } from "@/components/page-state"; +import { AnnouncementBanner } from "@/pages/dashboard/components/announcement-banner"; +import { FlowChartCard } from "@/pages/dashboard/components/flow-chart-card"; +import { MetricCard } from "@/pages/dashboard/components/metric-card"; import { - LineChart, - Line, - XAxis, - YAxis, - CartesianGrid, - Tooltip, - ResponsiveContainer, -} from "recharts"; - -import { - getUserPackageInfo, - getAnnouncement, - type AnnouncementData, -} from "@/api"; - -interface UserInfo { - flow: number; - inFlow: number; - outFlow: number; - num: number; - expTime?: string; - flowResetTime?: number; -} - -interface UserTunnel { - id: number; - tunnelId: number; - tunnelName: string; - flow: number; - inFlow: number; - outFlow: number; - num: number; - expTime?: string; - flowResetTime?: number; - tunnelFlow: number; -} - -interface Forward { - id: number; - name: string; - tunnelId: number; - tunnelName: string; - inIp: string; - inPort: number; - remoteAddr: string; - inFlow: number; - outFlow: number; -} + useDashboardData, + type DashboardForward as Forward, + type DashboardUserTunnel as UserTunnel, +} from "@/pages/dashboard/use-dashboard-data"; interface AddressItem { id: number; @@ -60,178 +21,21 @@ interface AddressItem { copying: boolean; } -interface StatisticsFlow { - id: number; - userId: number; - flow: number; - totalFlow: number; - time: string; -} - export default function DashboardPage() { - const [loading, setLoading] = useState(true); - const [userInfo, setUserInfo] = useState({} as UserInfo); - const [userTunnels, setUserTunnels] = useState([]); - const [forwardList, setForwardList] = useState([]); - const [statisticsFlows, setStatisticsFlows] = useState([]); - const [isAdmin, setIsAdmin] = useState(false); - const [announcement, setAnnouncement] = useState( - null, - ); + const { + loading, + userInfo, + userTunnels, + forwardList, + statisticsFlows, + isAdmin, + announcement, + } = useDashboardData(); const [addressModalOpen, setAddressModalOpen] = useState(false); const [addressModalTitle, setAddressModalTitle] = useState(""); const [addressList, setAddressList] = useState([]); - // 检查有效期通知 - const checkExpirationNotifications = ( - userInfo: UserInfo, - tunnels: UserTunnel[], - ) => { - // 避免重复通知,检查是否已经显示过 - const notificationKey = `expiration-${userInfo.expTime}-${tunnels.map((t) => t.expTime).join(",")}`; - const lastNotified = localStorage.getItem("lastNotified"); - - if (lastNotified === notificationKey) { - return; // 已经通知过,不重复显示 - } - - let hasNotification = false; - - // 检查主账户有效期 - if (userInfo.expTime) { - const expDate = new Date(userInfo.expTime); - const now = new Date(); - - if (!isNaN(expDate.getTime()) && expDate > now) { - const diffTime = expDate.getTime() - now.getTime(); - const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); - - if (diffDays <= 7 && diffDays > 0) { - hasNotification = true; - if (diffDays === 1) { - toast("账户将于明天过期,请及时续费", { - icon: "⚠️", - duration: 6000, - style: { background: "#f59e0b", color: "#fff" }, - }); - } else { - toast(`账户将于${diffDays}天后过期,请及时续费`, { - icon: "⚠️", - duration: 6000, - style: { background: "#f59e0b", color: "#fff" }, - }); - } - } else if (diffDays <= 0) { - hasNotification = true; - toast("账户已过期,请立即续费", { - icon: "⚠️", - duration: 8000, - style: { background: "#ef4444", color: "#fff" }, - }); - } - } - } - - // 检查隧道有效期 - tunnels.forEach((tunnel) => { - if (tunnel.expTime) { - const expDate = new Date(tunnel.expTime); - const now = new Date(); - - if (!isNaN(expDate.getTime()) && expDate > now) { - const diffTime = expDate.getTime() - now.getTime(); - const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); - - if (diffDays <= 7 && diffDays > 0) { - hasNotification = true; - if (diffDays === 1) { - toast(`隧道"${tunnel.tunnelName}"将于明天过期`, { - icon: "⚠️", - duration: 5000, - style: { background: "#f59e0b", color: "#fff" }, - }); - } else { - toast(`隧道"${tunnel.tunnelName}"将于${diffDays}天后过期`, { - icon: "⚠️", - duration: 5000, - style: { background: "#f59e0b", color: "#fff" }, - }); - } - } else if (diffDays <= 0) { - hasNotification = true; - toast(`隧道"${tunnel.tunnelName}"已过期`, { - icon: "⚠️", - duration: 6000, - style: { background: "#ef4444", color: "#fff" }, - }); - } - } - } - }); - - // 如果显示了通知,记录防止重复 - if (hasNotification) { - localStorage.setItem("lastNotified", notificationKey); - } - }; - - useEffect(() => { - setLoading(true); - setUserInfo({} as UserInfo); - setUserTunnels([]); - setForwardList([]); - setStatisticsFlows([]); - - const adminStatus = localStorage.getItem("admin"); - - setIsAdmin(adminStatus === "true"); - - loadPackageData(); - loadAnnouncement(); - localStorage.setItem("e", "/dashboard"); - }, []); - - const loadAnnouncement = async () => { - try { - const res = await getAnnouncement(); - - if (res.code === 0 && res.data && res.data.enabled === 1) { - setAnnouncement(res.data); - } - } catch (error) { - console.error("Failed to load announcement:", error); - } - }; - - const loadPackageData = async () => { - setLoading(true); - try { - const res = await getUserPackageInfo(); - - if (res.code === 0) { - const data = res.data; - - setUserInfo(data.userInfo || {}); - setUserTunnels(data.tunnelPermissions || []); - setForwardList(data.forwards || []); - setStatisticsFlows(data.statisticsFlows || []); - - // 检查有效期并显示通知 - checkExpirationNotifications( - data.userInfo, - data.tunnelPermissions || [], - ); - } else { - toast.error(res.msg || "获取套餐信息失败"); - } - } catch { - toast.error("获取套餐信息失败"); - } finally { - setLoading(false); - } - }; - const formatFlow = (value: number, unit: string = "bytes"): string => { // 99999 表示无限制 if (value === 99999) { @@ -289,7 +93,7 @@ export default function DashboardPage() { })); }; - const getExpStatus = (expTime?: string) => { + const getExpStatus = (expTime?: string | number) => { if (!expTime) return { color: "text-green-600 dark:text-green-400", @@ -450,7 +254,7 @@ export default function DashboardPage() { const now = new Date(); const currentDay = now.getDate(); - let daysUntilReset; + let daysUntilReset: number; if (resetDay > currentDay) { daysUntilReset = resetDay - currentDay; @@ -533,7 +337,7 @@ export default function DashboardPage() { } const firstIp = items[0]; - let formattedFirstIp; + let formattedFirstIp: string; if (firstIp.includes(":") && !firstIp.startsWith("[")) { formattedFirstIp = `[${firstIp}]`; @@ -598,7 +402,7 @@ export default function DashboardPage() { // 检查是否已经包含端口 const hasPort = /:\d+$/.test(items[0]); - let formattedList; + let formattedList: AddressItem[]; if (hasPort) { // 已经包含完整的 IP:Port 组合,直接使用 @@ -611,7 +415,7 @@ export default function DashboardPage() { } else { // 只包含IP,需要添加端口 formattedList = items.map((ip, index) => { - let formattedAddress; + let formattedAddress: string; if (ip.includes(":") && !ip.startsWith("[")) { formattedAddress = `[${ip}]:${port}`; @@ -709,324 +513,147 @@ export default function DashboardPage() { if (loading) { return ( - - - - 正在加载数据... - - + ); } return ( - {announcement && announcement.content && ( - - - - - - - - - - - 公告 - - - {announcement.content} - - - - - - )} + {announcement && } - - - - - - 总流量 - - - - - - - - - {formatFlow(userInfo.flow, "gb")} - - - - - - - - - - - 已用流量 - - - - - - - - - {formatFlow(calculateUserTotalUsedFlow())} - - - {renderProgressBar( - calculateUsagePercentage("flow"), - "sm", - userInfo.flow === 99999, - )} - - - {userInfo.flow === 99999 - ? "无限制" - : `${calculateUsagePercentage("flow").toFixed(1)}%`} - - {userInfo.flowResetTime !== undefined && - userInfo.flowResetTime !== null && ( - - - - - - {formatResetTime(userInfo.flowResetTime)} - - - )} - - - - - - - - - - - - 转发配额 - - - - - - - - - {formatNumber(userInfo.num || 0)} - - - - - - - - - - - 已用转发 - - - - - - - - - {forwardList.length} - - - {renderProgressBar( - calculateUsagePercentage("forwards"), - "sm", - userInfo.num === 99999, - )} - - {userInfo.num === 99999 - ? "无限制" - : `${calculateUsagePercentage("forwards").toFixed(1)}%`} - - - - - - - - {/* 24小时流量统计图表 */} - - - + - - + - - 24小时流量统计 - - - - - {statisticsFlows.length === 0 ? ( - - - - - 暂无流量统计数据 - - ) : ( - - {/* 流量趋势图 */} - - - - - - { - const v = - typeof value === "number" ? value : Number(value); + } + iconClassName="bg-blue-100 dark:bg-blue-500/20" + title="总流量" + value={formatFlow(userInfo.flow, "gb")} + /> - if (!Number.isFinite(v)) return String(value); - if (v === 0) return "0"; - if (v < 1024) return `${v}B`; - if (v < 1024 * 1024) return `${(v / 1024).toFixed(1)}K`; - if (v < 1024 * 1024 * 1024) - return `${(v / (1024 * 1024)).toFixed(1)}M`; - - return `${(v / (1024 * 1024 * 1024)).toFixed(1)}G`; - }} - tickLine={false} - /> - ; - label?: string | number; - }) => { - if (active && payload && payload.length) { - const firstValue = payload[0]?.value; - const numericValue = - typeof firstValue === "number" - ? firstValue - : Number(firstValue); - const flowValue = Number.isFinite(numericValue) - ? numericValue - : 0; - - return ( - - {`时间: ${label ?? ""}`} - - {`流量: ${formatFlow(flowValue)}`} - - - ); - } - - return null; - }} - /> - - - + + {renderProgressBar( + calculateUsagePercentage("flow"), + "sm", + userInfo.flow === 99999, + )} + + + {userInfo.flow === 99999 + ? "无限制" + : `${calculateUsagePercentage("flow").toFixed(1)}%`} + + {userInfo.flowResetTime !== undefined && + userInfo.flowResetTime !== null && ( + + + + + + {formatResetTime(userInfo.flowResetTime)} + + + )} - )} - - + } + icon={ + + + + } + iconClassName="bg-green-100 dark:bg-green-500/20" + title="已用流量" + value={formatFlow(calculateUserTotalUsedFlow())} + /> + + + + + } + iconClassName="bg-purple-100 dark:bg-purple-500/20" + title="转发配额" + value={formatNumber(userInfo.num || 0)} + /> + + + {renderProgressBar( + calculateUsagePercentage("forwards"), + "sm", + userInfo.num === 99999, + )} + + {userInfo.num === 99999 + ? "无限制" + : `${calculateUsagePercentage("forwards").toFixed(1)}%`} + + + } + icon={ + + + + } + iconClassName="bg-orange-100 dark:bg-orange-500/20" + title="已用转发" + value={forwardList.length} + /> + + + {/* 隧道权限 - 管理员不显示 */} {!isAdmin && ( @@ -1034,6 +661,7 @@ export default function DashboardPage() { {userTunnels.length === 0 ? ( - - - - - 暂无隧道权限 - + ) : ( {userTunnels.map((tunnel) => { @@ -1170,6 +783,7 @@ export default function DashboardPage() { {addressModalTitle} - + 复制全部 @@ -1348,7 +963,7 @@ export default function DashboardPage() { isLoading={item.copying} size="sm" variant="light" - onClick={() => copyAddress(item)} + onPress={() => copyAddress(item)} > 复制 diff --git a/vite-frontend/src/pages/forward.tsx b/vite-frontend/src/pages/forward.tsx index 4f3f846..12aa1e9 100644 --- a/vite-frontend/src/pages/forward.tsx +++ b/vite-frontend/src/pages/forward.tsx @@ -1,22 +1,22 @@ import { useState, useEffect, useMemo } from "react"; -import { Card, CardBody, CardHeader } from "@heroui/card"; -import { Button } from "@heroui/button"; -import { Input } from "@heroui/input"; -import { Textarea } from "@heroui/input"; -import { Select, SelectItem } from "@heroui/select"; +import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card"; +import { Button } from "@/shadcn-bridge/heroui/button"; +import { Input } from "@/shadcn-bridge/heroui/input"; +import { Textarea } from "@/shadcn-bridge/heroui/input"; +import { Select, SelectItem } from "@/shadcn-bridge/heroui/select"; import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, -} from "@heroui/modal"; -import { Chip } from "@heroui/chip"; -import { Spinner } from "@heroui/spinner"; -import { Switch } from "@heroui/switch"; -import { Alert } from "@heroui/alert"; -import { Accordion, AccordionItem } from "@heroui/accordion"; -import { Checkbox } from "@heroui/checkbox"; +} from "@/shadcn-bridge/heroui/modal"; +import { Chip } from "@/shadcn-bridge/heroui/chip"; +import { Spinner } from "@/shadcn-bridge/heroui/spinner"; +import { Switch } from "@/shadcn-bridge/heroui/switch"; +import { Alert } from "@/shadcn-bridge/heroui/alert"; +import { Accordion, AccordionItem } from "@/shadcn-bridge/heroui/accordion"; +import { Checkbox } from "@/shadcn-bridge/heroui/checkbox"; import toast from "react-hot-toast"; import { DndContext, @@ -48,12 +48,29 @@ import { resumeForwardService, diagnoseForward, updateForwardOrder, - batchDeleteForwards, - batchPauseForwards, - batchResumeForwards, - batchRedeployForwards, - batchChangeTunnel, } from "@/api"; +import { + type ForwardAddressItem, + formatInAddress, + formatRemoteAddress, + hasMultipleAddresses, + resolveForwardAddressAction, +} from "@/pages/forward/address"; +import { + buildForwardDiagnosisFallbackResult, + getForwardDiagnosisQualityDisplay, + type ForwardDiagnosisResult, +} from "@/pages/forward/diagnosis"; +import { + executeForwardBatchChangeTunnel, + executeForwardBatchDelete, + executeForwardBatchRedeploy, + executeForwardBatchToggleService, +} from "@/pages/forward/batch-actions"; +import { buildForwardOrder, FORWARD_ORDER_KEY } from "@/pages/forward/order"; +import { PageLoadingState } from "@/components/page-state"; +import { useMobileBreakpoint } from "@/hooks/useMobileBreakpoint"; +import { saveOrder } from "@/utils/order-storage"; import { JwtUtil } from "@/utils/jwt"; interface Forward { @@ -94,32 +111,6 @@ interface ForwardForm { strategy: string; } -interface AddressItem { - id: number; - address: string; - copying: boolean; -} - -interface DiagnosisResult { - forwardName: string; - timestamp: number; - results: Array<{ - success: boolean; - description: string; - nodeName: string; - nodeId: string; - targetIp: string; - targetPort?: number; - message?: string; - averageTime?: number; - packetLoss?: number; - fromChainType?: number; // 1: 入口, 2: 链, 3: 出口 - fromInx?: number; - toChainType?: number; - toInx?: number; - }>; -} - // 添加分组接口 interface UserGroup { userId: number | null; @@ -137,20 +128,7 @@ export default function ForwardPage() { const [loading, setLoading] = useState(true); const [forwards, setForwards] = useState([]); const [tunnels, setTunnels] = useState([]); - - // 检测是否为移动端 - const [isMobile, setIsMobile] = useState(false); - - useEffect(() => { - const checkMobile = () => { - setIsMobile(window.innerWidth < 768); - }; - - checkMobile(); - window.addEventListener("resize", checkMobile); - - return () => window.removeEventListener("resize", checkMobile); - }, []); + const isMobile = useMobileBreakpoint(); // 显示模式状态 - 从localStorage读取,默认为平铺显示 const [viewMode, setViewMode] = useState<"grouped" | "direct">(() => { @@ -179,9 +157,9 @@ export default function ForwardPage() { const [currentDiagnosisForward, setCurrentDiagnosisForward] = useState(null); const [diagnosisResult, setDiagnosisResult] = - useState(null); + useState(null); const [addressModalTitle, setAddressModalTitle] = useState(""); - const [addressList, setAddressList] = useState([]); + const [addressList, setAddressList] = useState([]); // 导出相关状态 const [exportModalOpen, setExportModalOpen] = useState(false); @@ -245,56 +223,16 @@ export default function ForwardPage() { // 切换到直接显示模式时,初始化拖拽排序顺序 if (newMode === "direct") { - // 在平铺模式下,只对当前用户的转发进行排序 const currentUserId = JwtUtil.getUserIdFromToken(); - let userForwards = forwards; - - if (currentUserId !== null) { - userForwards = forwards.filter( - (f: Forward) => f.userId === currentUserId, - ); - } - - // 检查数据库中是否有排序信息 - const hasDbOrdering = userForwards.some( - (f: Forward) => f.inx !== undefined && f.inx !== 0, + const { order, fromDatabase } = buildForwardOrder( + forwards, + currentUserId, ); - if (hasDbOrdering) { - // 使用数据库中的排序信息 - const dbOrder = userForwards - .sort((a: Forward, b: Forward) => (a.inx ?? 0) - (b.inx ?? 0)) - .map((f: Forward) => f.id); + setForwardOrder(order); - setForwardOrder(dbOrder); - - // 同步到localStorage - try { - localStorage.setItem("forward-order", JSON.stringify(dbOrder)); - } catch {} - } else { - // 使用本地存储的顺序 - const savedOrder = localStorage.getItem("forward-order"); - - if (savedOrder) { - try { - const orderIds = JSON.parse(savedOrder); - const validOrder = orderIds.filter((id: number) => - userForwards.some((f: Forward) => f.id === id), - ); - - userForwards.forEach((forward: Forward) => { - if (!validOrder.includes(forward.id)) { - validOrder.push(forward.id); - } - }); - setForwardOrder(validOrder); - } catch { - setForwardOrder(userForwards.map((f: Forward) => f.id)); - } - } else { - setForwardOrder(userForwards.map((f: Forward) => f.id)); - } + if (fromDatabase) { + saveOrder(FORWARD_ORDER_KEY, order); } } } catch {} @@ -320,58 +258,16 @@ export default function ForwardPage() { // 初始化拖拽排序顺序 if (viewMode === "direct") { - // 在平铺模式下,只对当前用户的转发进行排序 const currentUserId = JwtUtil.getUserIdFromToken(); - let userForwards = forwardsData; - - if (currentUserId !== null) { - userForwards = forwardsData.filter( - (f: Forward) => f.userId === currentUserId, - ); - } - - // 检查数据库中是否有排序信息 - const hasDbOrdering = userForwards.some( - (f: Forward) => f.inx !== undefined && f.inx !== 0, + const { order, fromDatabase } = buildForwardOrder( + forwardsData, + currentUserId, ); - if (hasDbOrdering) { - // 使用数据库中的排序信息 - const dbOrder = userForwards - .sort((a: Forward, b: Forward) => (a.inx ?? 0) - (b.inx ?? 0)) - .map((f: Forward) => f.id); + setForwardOrder(order); - setForwardOrder(dbOrder); - - // 同步到localStorage - try { - localStorage.setItem("forward-order", JSON.stringify(dbOrder)); - } catch {} - } else { - // 使用本地存储的顺序 - const savedOrder = localStorage.getItem("forward-order"); - - if (savedOrder) { - try { - const orderIds = JSON.parse(savedOrder); - // 验证保存的顺序是否仍然有效(只包含当前用户的转发) - const validOrder = orderIds.filter((id: number) => - userForwards.some((f: Forward) => f.id === id), - ); - - // 添加新的转发ID(如果存在) - userForwards.forEach((forward: Forward) => { - if (!validOrder.includes(forward.id)) { - validOrder.push(forward.id); - } - }); - setForwardOrder(validOrder); - } catch { - setForwardOrder(userForwards.map((f: Forward) => f.id)); - } - } else { - setForwardOrder(userForwards.map((f: Forward) => f.id)); - } + if (fromDatabase) { + saveOrder(FORWARD_ORDER_KEY, order); } } } else { @@ -587,7 +483,7 @@ export default function ForwardPage() { const addressCount = processedRemoteAddr.split(",").length; - let res; + let res: { code: number; msg: string }; if (isEdit) { // 更新时确保包含必要字段 @@ -647,7 +543,7 @@ export default function ForwardPage() { ), ); - let res; + let res: { code: number; msg: string }; if (targetState) { res = await resumeForwardService(forward.id); @@ -694,63 +590,33 @@ export default function ForwardPage() { const response = await diagnoseForward(forward.id); if (response.code === 0) { - setDiagnosisResult(response.data); + setDiagnosisResult(response.data as ForwardDiagnosisResult); } else { toast.error(response.msg || "诊断失败"); - setDiagnosisResult({ - forwardName: forward.name, - timestamp: Date.now(), - results: [ - { - success: false, - description: "诊断失败", - nodeName: "-", - nodeId: "-", - targetIp: forward.remoteAddr.split(",")[0] || "-", - message: response.msg || "诊断过程中发生错误", - }, - ], - }); + setDiagnosisResult( + buildForwardDiagnosisFallbackResult({ + forwardName: forward.name, + remoteAddr: forward.remoteAddr, + description: "诊断失败", + message: response.msg || "诊断过程中发生错误", + }), + ); } } catch { toast.error("网络错误,请重试"); - setDiagnosisResult({ - forwardName: forward.name, - timestamp: Date.now(), - results: [ - { - success: false, - description: "网络错误", - nodeName: "-", - nodeId: "-", - targetIp: forward.remoteAddr.split(",")[0] || "-", - message: "无法连接到服务器", - }, - ], - }); + setDiagnosisResult( + buildForwardDiagnosisFallbackResult({ + forwardName: forward.name, + remoteAddr: forward.remoteAddr, + description: "网络错误", + message: "无法连接到服务器", + }), + ); } finally { setDiagnosisLoading(false); } }; - // 获取连接质量 - const getQualityDisplay = (averageTime?: number, packetLoss?: number) => { - if (averageTime === undefined || packetLoss === undefined) return null; - - if (averageTime < 30 && packetLoss === 0) - return { text: "🚀 优秀", color: "success" }; - if (averageTime < 50 && packetLoss === 0) - return { text: "✨ 很好", color: "success" }; - if (averageTime < 100 && packetLoss < 1) - return { text: "👍 良好", color: "primary" }; - if (averageTime < 150 && packetLoss < 2) - return { text: "😐 一般", color: "warning" }; - if (averageTime < 200 && packetLoss < 5) - return { text: "😟 较差", color: "warning" }; - - return { text: "😵 很差", color: "danger" }; - }; - // 格式化流量 const formatFlow = (value: number): string => { if (value === 0) return "0 B"; @@ -762,141 +628,26 @@ export default function ForwardPage() { return (value / (1024 * 1024 * 1024)).toFixed(2) + " GB"; }; - // 格式化入口地址 - const formatInAddress = (ipString: string, port: number): string => { - if (!ipString) return ""; - - const items = ipString - .split(",") - .map((item) => item.trim()) - .filter((item) => item); - - if (items.length === 0) return ""; - - // 检查第一项是否已经包含端口(格式:IP:端口) - const firstItem = items[0]; - const hasPort = /:\d+$/.test(firstItem); - - if (hasPort) { - // inIp 已经包含完整的 IP:Port 组合 - if (items.length === 1) { - return items[0]; - } - - return `${items[0]} (+${items.length - 1}个)`; - } - - // inIp 只包含IP,需要添加端口(兼容旧数据) - if (!port) return ""; - - if (items.length === 1) { - const ip = items[0]; - - if (ip.includes(":") && !ip.startsWith("[")) { - return `[${ip}]:${port}`; - } else { - return `${ip}:${port}`; - } - } - - const firstIp = items[0]; - let formattedFirstIp; - - if (firstIp.includes(":") && !firstIp.startsWith("[")) { - formattedFirstIp = `[${firstIp}]`; - } else { - formattedFirstIp = firstIp; - } - - return `${formattedFirstIp}:${port} (+${items.length - 1}个)`; - }; - - // 格式化远程地址 - const formatRemoteAddress = (addressString: string): string => { - if (!addressString) return ""; - - const addresses = addressString - .split(",") - .map((addr) => addr.trim()) - .filter((addr) => addr); - - if (addresses.length === 0) return ""; - if (addresses.length === 1) return addresses[0]; - - return `${addresses[0]} (+${addresses.length - 1})`; - }; - - // 检查是否有多个地址 - const hasMultipleAddresses = (addressString: string): boolean => { - if (!addressString) return false; - const addresses = addressString - .split(",") - .map((addr) => addr.trim()) - .filter((addr) => addr); - - return addresses.length > 1; - }; - // 显示地址列表弹窗 const showAddressModal = ( addressString: string, port: number | null, title: string, ) => { - if (!addressString) return; + const action = resolveForwardAddressAction(addressString, port, title); - let addresses: string[]; - - if (port !== null) { - // 入口地址处理 - const items = addressString - .split(",") - .map((item) => item.trim()) - .filter((item) => item); - - if (items.length <= 1) { - copyToClipboard(formatInAddress(addressString, port), title); - - return; - } - - // 检查是否已经包含端口 - const hasPort = /:\d+$/.test(items[0]); - - if (hasPort) { - // 已经包含完整的 IP:Port 组合,直接使用 - addresses = items; - } else { - // 只包含IP,需要添加端口 - addresses = items.map((ip) => { - if (ip.includes(":") && !ip.startsWith("[")) { - return `[${ip}]:${port}`; - } else { - return `${ip}:${port}`; - } - }); - } - } else { - // 远程地址处理 - addresses = addressString - .split(",") - .map((addr) => addr.trim()) - .filter((addr) => addr); - if (addresses.length <= 1) { - copyToClipboard(addressString, title); - - return; - } + if (action.type === "none") { + return; } - setAddressList( - addresses.map((address, index) => ({ - id: index, - address, - copying: false, - })), - ); - setAddressModalTitle(`${title} (${addresses.length}个)`); + if (action.type === "copy") { + copyToClipboard(action.text, action.label); + + return; + } + + setAddressList(action.items); + setAddressModalTitle(action.title); setAddressModalOpen(true); }; @@ -911,7 +662,7 @@ export default function ForwardPage() { }; // 复制地址 - const copyAddress = async (addressItem: AddressItem) => { + const copyAddress = async (addressItem: ForwardAddressItem) => { try { setAddressList((prev) => prev.map((item) => @@ -1220,10 +971,7 @@ export default function ForwardPage() { setForwardOrder(newOrder); - // 保存到localStorage - try { - localStorage.setItem("forward-order", JSON.stringify(newOrder)); - } catch {} + saveOrder(FORWARD_ORDER_KEY, newOrder); // 持久化到数据库 try { @@ -1292,27 +1040,22 @@ export default function ForwardPage() { if (selectedIds.size === 0) return; setBatchLoading(true); try { - const res = await batchDeleteForwards(Array.from(selectedIds)); + const outcome = await executeForwardBatchDelete(Array.from(selectedIds)); - if (res.code === 0) { - const result = res.data; + if (outcome.toastVariant === "success") { + toast.success(outcome.toastMessage); + } else { + toast.error(outcome.toastMessage); + } - if (result.failCount === 0) { - toast.success(`成功删除 ${result.successCount} 项`); - } else { - toast.error( - `成功 ${result.successCount} 项,失败 ${result.failCount} 项`, - ); - } + if (outcome.shouldRefresh) { setSelectedIds(new Set()); setSelectMode(false); - setBatchDeleteModalOpen(false); + if (outcome.closeDeleteModal) { + setBatchDeleteModalOpen(false); + } loadData(false); - } else { - toast.error(res.msg || "删除失败"); } - } catch (e: any) { - toast.error(e.message || "删除失败"); } finally { setBatchLoading(false); } @@ -1322,33 +1065,22 @@ export default function ForwardPage() { if (selectedIds.size === 0) return; setBatchLoading(true); try { - const ids = Array.from(selectedIds); - const res = enable - ? await batchResumeForwards(ids) - : await batchPauseForwards(ids); + const outcome = await executeForwardBatchToggleService( + Array.from(selectedIds), + enable, + ); - if (res.code === 0) { - const result = res.data; + if (outcome.toastVariant === "success") { + toast.success(outcome.toastMessage); + } else { + toast.error(outcome.toastMessage); + } - if (result.failCount === 0) { - toast.success( - enable - ? `成功启用 ${result.successCount} 项` - : `成功停用 ${result.successCount} 项`, - ); - } else { - toast.error( - `成功 ${result.successCount} 项,失败 ${result.failCount} 项`, - ); - } + if (outcome.shouldRefresh) { setSelectedIds(new Set()); setSelectMode(false); loadData(false); - } else { - toast.error(res.msg || (enable ? "启用失败" : "停用失败")); } - } catch (e: any) { - toast.error(e.message || (enable ? "启用失败" : "停用失败")); } finally { setBatchLoading(false); } @@ -1358,26 +1090,21 @@ export default function ForwardPage() { if (selectedIds.size === 0) return; setBatchLoading(true); try { - const res = await batchRedeployForwards(Array.from(selectedIds)); + const outcome = await executeForwardBatchRedeploy( + Array.from(selectedIds), + ); - if (res.code === 0) { - const result = res.data; + if (outcome.toastVariant === "success") { + toast.success(outcome.toastMessage); + } else { + toast.error(outcome.toastMessage); + } - if (result.failCount === 0) { - toast.success(`成功重新下发 ${result.successCount} 项`); - } else { - toast.error( - `成功 ${result.successCount} 项,失败 ${result.failCount} 项`, - ); - } + if (outcome.shouldRefresh) { setSelectedIds(new Set()); setSelectMode(false); loadData(false); - } else { - toast.error(res.msg || "下发失败"); } - } catch (e: any) { - toast.error(e.message || "下发失败"); } finally { setBatchLoading(false); } @@ -1387,31 +1114,28 @@ export default function ForwardPage() { if (selectedIds.size === 0 || !batchTargetTunnelId) return; setBatchLoading(true); try { - const res = await batchChangeTunnel({ - forwardIds: Array.from(selectedIds), - targetTunnelId: batchTargetTunnelId, - }); + const outcome = await executeForwardBatchChangeTunnel( + Array.from(selectedIds), + batchTargetTunnelId, + ); - if (res.code === 0) { - const result = res.data; + if (outcome.toastVariant === "success") { + toast.success(outcome.toastMessage); + } else { + toast.error(outcome.toastMessage); + } - if (result.failCount === 0) { - toast.success(`成功换隧道 ${result.successCount} 项`); - } else { - toast.error( - `成功 ${result.successCount} 项,失败 ${result.failCount} 项`, - ); - } + if (outcome.shouldRefresh) { setSelectedIds(new Set()); setSelectMode(false); - setBatchChangeTunnelModalOpen(false); - setBatchTargetTunnelId(null); + if (outcome.closeChangeTunnelModal) { + setBatchChangeTunnelModalOpen(false); + } + if (outcome.resetTargetTunnel) { + setBatchTargetTunnelId(null); + } loadData(false); - } else { - toast.error(res.msg || "隧道失败"); } - } catch (e: any) { - toast.error(e.message || "隧道失败"); } finally { setBatchLoading(false); } @@ -1568,6 +1292,7 @@ export default function ForwardPage() { title={isMobile ? "长按拖拽排序" : "拖拽排序"} > {hasMultipleAddresses(forward.inIp) && ( {hasMultipleAddresses(forward.remoteAddr) && ( - - - 正在加载... - - - ); + return ; } const userGroups = groupForwardsByUserAndTunnel(); @@ -1804,6 +1527,9 @@ export default function ForwardPage() { {/* 显示模式切换按钮 */} {viewMode === "grouped" ? ( - + ) : ( - + )} @@ -1936,6 +1672,7 @@ export default function ForwardPage() { {addressModalTitle} - + 复制 @@ -2327,7 +2067,7 @@ export default function ForwardPage() { isLoading={item.copying} size="sm" variant="light" - onClick={() => copyAddress(item)} + onPress={() => copyAddress(item)} > 复制 @@ -2401,6 +2141,7 @@ export default function ForwardPage() { size="sm" startContent={ {result.success ? ( ) : ( {results.map((result, index) => { - const quality = getQualityDisplay( - result.averageTime, - result.packetLoss, - ); + const quality = + getForwardDiagnosisQualityDisplay( + result.averageTime, + result.packetLoss, + ); return ( {results.map((result, index) => { - const quality = getQualityDisplay( - result.averageTime, - result.packetLoss, - ); + const quality = + getForwardDiagnosisQualityDisplay( + result.averageTime, + result.packetLoss, + ); return ( { return new Date(timestamp).toLocaleString(); }; -const isAdminUser = () => { - let adminFlag = localStorage.getItem("admin") === "true"; - - if (localStorage.getItem("admin") === null) { - const roleId = parseInt(localStorage.getItem("role_id") || "1", 10); - - adminFlag = roleId === 0; - localStorage.setItem("admin", adminFlag.toString()); - } - - return adminFlag; -}; - export default function GroupPage() { const [loading, setLoading] = useState(true); - const [isAdmin] = useState(isAdminUser()); + const [isAdmin] = useState(getAdminFlag()); const [tunnelGroups, setTunnelGroups] = useState([]); const [userGroups, setUserGroups] = useState([]); @@ -198,19 +186,25 @@ export default function GroupPage() { ]); if (tunnelGroupRes.code === 0) { - setTunnelGroups(tunnelGroupRes.data || []); + setTunnelGroups( + Array.isArray(tunnelGroupRes.data) ? tunnelGroupRes.data : [], + ); } if (userGroupRes.code === 0) { - setUserGroups(userGroupRes.data || []); + setUserGroups( + Array.isArray(userGroupRes.data) ? userGroupRes.data : [], + ); } if (permissionRes.code === 0) { - setPermissions(permissionRes.data || []); + setPermissions( + Array.isArray(permissionRes.data) ? permissionRes.data : [], + ); } if (tunnelRes.code === 0) { - setTunnels(tunnelRes.data || []); + setTunnels(Array.isArray(tunnelRes.data) ? tunnelRes.data : []); } if (userRes.code === 0) { - setUsers(userRes.data || []); + setUsers(Array.isArray(userRes.data) ? userRes.data : []); } if ( diff --git a/vite-frontend/src/pages/index.tsx b/vite-frontend/src/pages/index.tsx index f7f02f5..6c1030e 100644 --- a/vite-frontend/src/pages/index.tsx +++ b/vite-frontend/src/pages/index.tsx @@ -1,16 +1,17 @@ -import { Button } from "@heroui/button"; -import { Input } from "@heroui/input"; -import { Card, CardBody, CardHeader } from "@heroui/card"; -import { useState, useEffect } from "react"; +import { Button } from "@/shadcn-bridge/heroui/button"; +import { Input } from "@/shadcn-bridge/heroui/input"; +import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card"; +import { useState } from "react"; import { useNavigate } from "react-router-dom"; import toast from "react-hot-toast"; import { Turnstile } from "@marsidev/react-turnstile"; -import { isWebViewFunc } from "@/utils/panel"; import { siteConfig } from "@/config/site"; import { title } from "@/components/primitives"; import DefaultLayout from "@/layouts/default"; import { login, LoginData, checkCaptcha, getConfigByName } from "@/api"; +import { writeLoginSession } from "@/utils/session"; +import { useWebViewMode } from "@/hooks/useWebViewMode"; interface LoginForm { username: string; @@ -29,12 +30,7 @@ export default function IndexPage() { const [showCaptcha, setShowCaptcha] = useState(false); const [siteKey, setSiteKey] = useState(""); const navigate = useNavigate(); - const [isWebView, setIsWebView] = useState(false); - - // 检测是否在WebView中运行 - useEffect(() => { - setIsWebView(isWebViewFunc()); - }, []); + const isWebView = useWebViewMode(); // 验证表单 const validateForm = (): boolean => { @@ -91,10 +87,7 @@ export default function IndexPage() { // 检查是否需要强制修改密码 if (response.data.requirePasswordChange) { - localStorage.setItem("token", response.data.token); - localStorage.setItem("role_id", response.data.role_id.toString()); - localStorage.setItem("name", response.data.name); - localStorage.setItem("admin", (response.data.role_id === 0).toString()); + writeLoginSession(response.data); toast.success("检测到默认密码,即将跳转到修改密码页面"); navigate("/change-password"); @@ -102,10 +95,7 @@ export default function IndexPage() { } // 保存登录信息 - localStorage.setItem("token", response.data.token); - localStorage.setItem("role_id", response.data.role_id.toString()); - localStorage.setItem("name", response.data.name); - localStorage.setItem("admin", (response.data.role_id === 0).toString()); + writeLoginSession(response.data); // 登录成功 toast.success("登录成功"); @@ -206,7 +196,7 @@ export default function IndexPage() { disabled={loading} isLoading={loading} size="lg" - onClick={handleLogin} + onPress={handleLogin} > {loading ? (showCaptcha ? "验证中..." : "登录中...") : "登录"} @@ -238,10 +228,9 @@ export default function IndexPage() { {showCaptcha && siteKey && ( {/* 背景遮罩层 - 模糊效果,暗黑模式下更深 */} - { setShowCaptcha(false); setLoading(false); diff --git a/vite-frontend/src/pages/limit.tsx b/vite-frontend/src/pages/limit.tsx index 3766d56..09b4fda 100644 --- a/vite-frontend/src/pages/limit.tsx +++ b/vite-frontend/src/pages/limit.tsx @@ -1,17 +1,16 @@ import { useState, useEffect } from "react"; -import { Card, CardBody, CardHeader } from "@heroui/card"; -import { Button } from "@heroui/button"; -import { Input } from "@heroui/input"; -import { Select, SelectItem } from "@heroui/select"; +import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card"; +import { Button } from "@/shadcn-bridge/heroui/button"; +import { Input } from "@/shadcn-bridge/heroui/input"; +import { Select, SelectItem } from "@/shadcn-bridge/heroui/select"; import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, -} from "@heroui/modal"; -import { Chip } from "@heroui/chip"; -import { Spinner } from "@heroui/spinner"; +} from "@/shadcn-bridge/heroui/modal"; +import { Chip } from "@/shadcn-bridge/heroui/chip"; import toast from "react-hot-toast"; import { @@ -21,6 +20,7 @@ import { deleteSpeedLimit, getTunnelList, } from "@/api"; +import { PageLoadingState } from "@/components/page-state"; interface SpeedLimitRule { id: number; @@ -188,7 +188,7 @@ export default function LimitPage() { setSubmitLoading(true); try { - let res; + let res: { code: number; msg: string }; if (isEdit) { res = await updateSpeedLimit(form); @@ -215,14 +215,7 @@ export default function LimitPage() { }; if (loading) { - return ( - - - - 正在加载... - - - ); + return ; } return ( @@ -293,6 +286,7 @@ export default function LimitPage() { size="sm" startContent={ ([]); const [nodeOrder, setNodeOrder] = useState([]); const [loading, setLoading] = useState(false); - const [wsConnected, setWsConnected] = useState(false); - const [wsConnecting, setWsConnecting] = useState(false); const [dialogVisible, setDialogVisible] = useState(false); const [dialogTitle, setDialogTitle] = useState(""); const [isEdit, setIsEdit] = useState(false); @@ -200,53 +207,26 @@ export default function NodePage() { Record >({}); - const websocketRef = useRef(null); - const reconnectTimerRef = useRef(null); - const reconnectAttemptsRef = useRef(0); - const maxReconnectAttempts = 5; - const offlineTimersRef = useRef>>( - new Map(), - ); - const offlineDelayMs = 3000; + const handleNodeOffline = useCallback((nodeId: number) => { + setNodeList((prev) => + prev.map((node) => { + if (node.id !== nodeId) return node; + if (node.connectionStatus === "offline" && node.systemInfo === null) { + return node; + } - const clearOfflineTimer = (nodeId: number) => { - const timer = offlineTimersRef.current.get(nodeId); - - if (timer) { - clearTimeout(timer); - offlineTimersRef.current.delete(nodeId); - } - }; - - const scheduleNodeOffline = (nodeId: number) => { - if (offlineTimersRef.current.has(nodeId)) return; - const timer = setTimeout(() => { - offlineTimersRef.current.delete(nodeId); - setNodeList((prev) => - prev.map((node) => { - if (node.id !== nodeId) return node; - if (node.connectionStatus === "offline" && node.systemInfo === null) - return node; - - return { ...node, connectionStatus: "offline", systemInfo: null }; - }), - ); - }, offlineDelayMs); - - offlineTimersRef.current.set(nodeId, timer); - }; - - useEffect(() => { - loadNodes(); - initWebSocket(); - - return () => { - closeWebSocket(); - }; + return { ...node, connectionStatus: "offline", systemInfo: null }; + }), + ); }, []); + const { clearOfflineTimer, scheduleNodeOffline } = useNodeOfflineTimers({ + delayMs: 3000, + onNodeOffline: handleNodeOffline, + }); + // 加载节点列表 - const loadNodes = async () => { + const loadNodes = useCallback(async () => { setLoading(true); try { const res = await getNodeList(); @@ -279,32 +259,12 @@ export default function NodePage() { setNodeOrder(dbOrder); } else { - try { - const stored = localStorage.getItem("node-order"); - - if (stored) { - const parsed = JSON.parse(stored); - - if (Array.isArray(parsed)) { - const existingIds = new Set(nodesData.map((n) => n.id)); - const validOrder = parsed - .map((id: any) => Number(id)) - .filter((id: number) => existingIds.has(id)); - - if (validOrder.length > 0) { - setNodeOrder(validOrder); - } else { - setNodeOrder(nodesData.map((n) => n.id)); - } - } else { - setNodeOrder(nodesData.map((n) => n.id)); - } - } else { - setNodeOrder(nodesData.map((n) => n.id)); - } - } catch { - setNodeOrder(nodesData.map((n) => n.id)); - } + setNodeOrder( + loadStoredOrder( + "node-order", + nodesData.map((n) => n.id), + ), + ); } } else { toast.error(res.msg || "加载节点列表失败"); @@ -314,68 +274,7 @@ export default function NodePage() { } finally { setLoading(false); } - }; - - // 初始化WebSocket连接 - const initWebSocket = () => { - if ( - websocketRef.current && - (websocketRef.current.readyState === WebSocket.OPEN || - websocketRef.current.readyState === WebSocket.CONNECTING) - ) { - return; - } - - if (websocketRef.current) { - closeWebSocket(); - } - - // 构建WebSocket URL,使用axios的baseURL - const baseUrl = - axios.defaults.baseURL || - (import.meta.env.VITE_API_BASE - ? `${import.meta.env.VITE_API_BASE}/api/v1/` - : "/api/v1/"); - const wsUrl = - baseUrl.replace(/^http/, "ws").replace(/\/api\/v1\/$/, "") + - `/system-info?type=0&secret=${localStorage.getItem("token")}`; - - try { - setWsConnecting(true); - websocketRef.current = new WebSocket(wsUrl); - - websocketRef.current.onopen = () => { - reconnectAttemptsRef.current = 0; - setWsConnected(true); - setWsConnecting(false); - }; - - websocketRef.current.onmessage = (event) => { - try { - const data = JSON.parse(event.data); - - handleWebSocketMessage(data); - } catch { - // 解析失败时不输出错误信息 - } - }; - - websocketRef.current.onerror = () => { - // WebSocket错误时不输出错误信息 - }; - - websocketRef.current.onclose = () => { - websocketRef.current = null; - setWsConnected(false); - setWsConnecting(false); - attemptReconnect(); - }; - } catch { - setWsConnected(false); - setWsConnecting(false); - attemptReconnect(); - } - }; + }, []); // 处理WebSocket消息 const handleWebSocketMessage = (data: any) => { @@ -404,61 +303,20 @@ export default function NodePage() { setNodeList((prev) => prev.map((node) => { if (node.id === nodeId) { - try { - let systemInfo; + const systemInfo = buildNodeSystemInfo( + messageData, + node.systemInfo, + ); - if (typeof messageData === "string") { - systemInfo = JSON.parse(messageData); - } else { - systemInfo = messageData; - } - - const currentUpload = parseInt(systemInfo.bytes_transmitted) || 0; - const currentDownload = parseInt(systemInfo.bytes_received) || 0; - const currentUptime = parseInt(systemInfo.uptime) || 0; - - let uploadSpeed = 0; - let downloadSpeed = 0; - - if (node.systemInfo && node.systemInfo.uptime) { - const timeDiff = currentUptime - node.systemInfo.uptime; - - if (timeDiff > 0 && timeDiff <= 10) { - const lastUpload = node.systemInfo.uploadTraffic || 0; - const lastDownload = node.systemInfo.downloadTraffic || 0; - - const uploadDiff = currentUpload - lastUpload; - const downloadDiff = currentDownload - lastDownload; - - const uploadReset = currentUpload < lastUpload; - const downloadReset = currentDownload < lastDownload; - - if (!uploadReset && uploadDiff >= 0) { - uploadSpeed = uploadDiff / timeDiff; - } - - if (!downloadReset && downloadDiff >= 0) { - downloadSpeed = downloadDiff / timeDiff; - } - } - } - - return { - ...node, - connectionStatus: "online", - systemInfo: { - cpuUsage: parseFloat(systemInfo.cpu_usage) || 0, - memoryUsage: parseFloat(systemInfo.memory_usage) || 0, - uploadTraffic: currentUpload, - downloadTraffic: currentDownload, - uploadSpeed: uploadSpeed, - downloadSpeed: downloadSpeed, - uptime: currentUptime, - }, - }; - } catch { + if (!systemInfo) { return node; } + + return { + ...node, + connectionStatus: "online", + systemInfo, + }; } return node; @@ -487,49 +345,13 @@ export default function NodePage() { } }; - // 尝试重新连接 - const attemptReconnect = () => { - if (reconnectTimerRef.current) return; - if (reconnectAttemptsRef.current < maxReconnectAttempts) { - reconnectAttemptsRef.current++; + const { wsConnected, wsConnecting } = useNodeRealtime({ + onMessage: handleWebSocketMessage, + }); - reconnectTimerRef.current = setTimeout(() => { - reconnectTimerRef.current = null; - initWebSocket(); - }, 3000 * reconnectAttemptsRef.current); - } - }; - - // 关闭WebSocket连接 - const closeWebSocket = () => { - if (reconnectTimerRef.current) { - clearTimeout(reconnectTimerRef.current); - reconnectTimerRef.current = null; - } - - offlineTimersRef.current.forEach((timer) => clearTimeout(timer)); - offlineTimersRef.current.clear(); - - reconnectAttemptsRef.current = 0; - setWsConnected(false); - setWsConnecting(false); - - if (websocketRef.current) { - websocketRef.current.onopen = null; - websocketRef.current.onmessage = null; - websocketRef.current.onerror = null; - websocketRef.current.onclose = null; - - if ( - websocketRef.current.readyState === WebSocket.OPEN || - websocketRef.current.readyState === WebSocket.CONNECTING - ) { - websocketRef.current.close(); - } - - websocketRef.current = null; - } - }; + useEffect(() => { + loadNodes(); + }, [loadNodes]); // 格式化速度 const formatSpeed = (bytesPerSecond: number): string => { @@ -797,11 +619,11 @@ export default function NodePage() { const res = await getNodeInstallCommand(node.id); if (res.code === 0 && res.data) { - try { - await navigator.clipboard.writeText(res.data); + const copied = await tryCopyInstallCommand(res.data); + + if (copied) { toast.success("安装命令已复制到剪贴板"); - } catch { - // 复制失败,显示安装命令模态框 + } else { setInstallCommand(res.data); setCurrentNodeName(node.name); setInstallCommandModal(true); @@ -1031,10 +853,7 @@ export default function NodePage() { setNodeOrder(newOrder); - // 保存到 localStorage - try { - localStorage.setItem("node-order", JSON.stringify(newOrder)); - } catch {} + saveOrder("node-order", newOrder); // 持久化到数据库 try { @@ -1241,42 +1060,12 @@ export default function NodePage() { {/* 节点列表 */} {loading ? ( - - - - 正在加载... - - + ) : nodeList.length === 0 ? ( - - - - - - - - - - - 暂无节点配置 - - - 还没有创建任何节点配置,点击上方按钮开始创建 - - - - - + ) : ( )} - - {node.connectionStatus === "online" - ? "在线" - : "离线"} - + {(() => { + const connectionStatusMeta = + getConnectionStatusMeta( + node.connectionStatus, + ); + + return ( + + {connectionStatusMeta.text} + + ); + })()} @@ -1353,13 +1146,7 @@ export default function NodePage() { {isRemoteNode && node.syncError && ( - {node.syncError === "provider_share_deleted" - ? "提供方已删除该分享" - : node.syncError === "provider_share_disabled" - ? "提供方已禁用该分享" - : node.syncError === "provider_share_expired" - ? "提供方分享已过期" - : `远程同步失败: ${node.syncError}`} + {getRemoteSyncErrorMessage(node.syncError)} )} {/* 基础信息 */} @@ -1816,6 +1603,7 @@ export default function NodePage() { (node?.isRemote ?? 0) !== 1, - ); + const localNodes: Node[] = ( + Array.isArray(res.data) ? (res.data as Node[]) : [] + ).filter((node: Node) => (node?.isRemote ?? 0) !== 1); setNodes(localNodes); setShareForm((prev) => { @@ -172,7 +174,11 @@ export default function PanelSharingPage() { const res = await getPeerRemoteUsageList(); if (res.code === 0) { - setRemoteUsageNodes(res.data || []); + setRemoteUsageNodes( + Array.isArray(res.data) + ? (res.data as unknown as RemoteUsageNode[]) + : [], + ); } else { toast.error(res.msg || "加载远程占用端口失败"); } diff --git a/vite-frontend/src/pages/profile.tsx b/vite-frontend/src/pages/profile.tsx index 01c5318..f888a50 100644 --- a/vite-frontend/src/pages/profile.tsx +++ b/vite-frontend/src/pages/profile.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect } from "react"; -import { Card, CardBody } from "@heroui/card"; -import { Button } from "@heroui/button"; +import { Card, CardBody } from "@/shadcn-bridge/heroui/card"; +import { Button } from "@/shadcn-bridge/heroui/button"; import { Modal, ModalContent, @@ -8,8 +8,8 @@ import { ModalBody, ModalFooter, useDisclosure, -} from "@heroui/modal"; -import { Input } from "@heroui/input"; +} from "@/shadcn-bridge/heroui/modal"; +import { Input } from "@/shadcn-bridge/heroui/input"; import { toast } from "react-hot-toast"; import { useNavigate } from "react-router-dom"; @@ -17,6 +17,7 @@ import { isWebViewFunc } from "@/utils/panel"; import { siteConfig } from "@/config/site"; import { updatePassword } from "@/api"; import { safeLogout } from "@/utils/logout"; +import { getAdminFlag, getSessionName } from "@/utils/session"; interface PasswordForm { newUsername: string; currentPassword: string; @@ -47,21 +48,8 @@ export default function ProfilePage() { useEffect(() => { // 获取用户信息 - 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); - - adminFlag = roleId === 0; - // 补充设置admin字段,避免下次再次判断 - localStorage.setItem("admin", adminFlag.toString()); - } - - setUsername(name); - setIsAdmin(adminFlag); + setUsername(getSessionName() || "Admin"); + setIsAdmin(getAdminFlag()); }, []); // 管理员菜单项 diff --git a/vite-frontend/src/pages/settings.tsx b/vite-frontend/src/pages/settings.tsx index 185ffa1..54eb648 100644 --- a/vite-frontend/src/pages/settings.tsx +++ b/vite-frontend/src/pages/settings.tsx @@ -1,7 +1,7 @@ import { useState, useEffect } from "react"; -import { Input } from "@heroui/input"; -import { Button } from "@heroui/button"; -import { Card, CardBody } from "@heroui/card"; +import { Input } from "@/shadcn-bridge/heroui/input"; +import { Button } from "@/shadcn-bridge/heroui/button"; +import { Card, CardBody } from "@/shadcn-bridge/heroui/card"; import { useNavigate } from "react-router-dom"; import toast from "react-hot-toast"; @@ -87,11 +87,13 @@ export const SettingsPage = () => { navigate(-1)} + onPress={() => navigate(-1)} > { onChange={(e) => setNewAddress(e.target.value)} /> - + 添加 @@ -182,7 +184,7 @@ export const SettingsPage = () => { color="primary" size="sm" variant="flat" - onClick={() => setCurrentPanel(panel.name)} + onPress={() => setCurrentPanel(panel.name)} > 设为当前 @@ -191,7 +193,7 @@ export const SettingsPage = () => { color="danger" size="sm" variant="light" - onClick={() => handleDeletePanelAddress(panel.name)} + onPress={() => handleDeletePanelAddress(panel.name)} > 删除 diff --git a/vite-frontend/src/pages/tunnel.tsx b/vite-frontend/src/pages/tunnel.tsx index dad3c9d..a5f02cf 100644 --- a/vite-frontend/src/pages/tunnel.tsx +++ b/vite-frontend/src/pages/tunnel.tsx @@ -1,20 +1,20 @@ import { useState, useEffect, useMemo } from "react"; -import { Card, CardBody, CardHeader } from "@heroui/card"; -import { Button } from "@heroui/button"; -import { Input, Textarea } from "@heroui/input"; -import { Select, SelectItem } from "@heroui/select"; +import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card"; +import { Button } from "@/shadcn-bridge/heroui/button"; +import { Input, Textarea } from "@/shadcn-bridge/heroui/input"; +import { Select, SelectItem } from "@/shadcn-bridge/heroui/select"; import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, -} from "@heroui/modal"; -import { Chip } from "@heroui/chip"; -import { Spinner } from "@heroui/spinner"; -import { Divider } from "@heroui/divider"; -import { Alert } from "@heroui/alert"; -import { Checkbox } from "@heroui/checkbox"; +} from "@/shadcn-bridge/heroui/modal"; +import { Chip } from "@/shadcn-bridge/heroui/chip"; +import { Spinner } from "@/shadcn-bridge/heroui/spinner"; +import { Divider } from "@/shadcn-bridge/heroui/divider"; +import { Alert } from "@/shadcn-bridge/heroui/alert"; +import { Checkbox } from "@/shadcn-bridge/heroui/checkbox"; import toast from "react-hot-toast"; import { DndContext, @@ -45,6 +45,20 @@ import { batchDeleteTunnels, batchRedeployTunnels, } from "@/api"; +import { PageLoadingState } from "@/components/page-state"; +import { + buildDiagnosisFallbackResult, + getDiagnosisQualityDisplay, + type DiagnosisResult, +} from "@/pages/tunnel/diagnosis"; +import { + createTunnelFormDefaults, + getTunnelFlowDisplay, + getTunnelTypeDisplay, + validateTunnelForm, +} from "@/pages/tunnel/form"; +import { loadStoredOrder, saveOrder } from "@/utils/order-storage"; +import { extractApiErrorMessage } from "@/api/error-message"; interface ChainTunnel { nodeId: number; @@ -92,27 +106,6 @@ interface TunnelForm { status: number; } -interface DiagnosisResult { - tunnelName: string; - tunnelType: string; - timestamp: number; - results: Array<{ - success: boolean; - description: string; - nodeName: string; - nodeId: string; - targetIp: string; - targetPort?: number; - message?: string; - averageTime?: number; - packetLoss?: number; - fromChainType?: number; // 1: 入口, 2: 链, 3: 出口 - fromInx?: number; - toChainType?: number; - toInx?: number; - }>; -} - export default function TunnelPage() { const [loading, setLoading] = useState(true); const [tunnels, setTunnels] = useState([]); @@ -134,18 +127,7 @@ export default function TunnelPage() { useState(null); // 表单状态 - const [form, setForm] = useState({ - name: "", - type: 1, - inNodeId: [], - outNodeId: [], - chainNodes: [], - flow: 1, - trafficRatio: 1.0, - inIp: "", - ipPreference: "", - status: 1, - }); + const [form, setForm] = useState(createTunnelFormDefaults()); // 表单验证错误 const [errors, setErrors] = useState<{ [key: string]: string }>({}); @@ -189,32 +171,12 @@ export default function TunnelPage() { setTunnelOrder(dbOrder); } else { - try { - const stored = localStorage.getItem("tunnel-order"); - - if (stored) { - const parsed = JSON.parse(stored); - - if (Array.isArray(parsed)) { - const existingIds = new Set(tunnelsData.map((t) => t.id)); - const validOrder = parsed - .map((id: any) => Number(id)) - .filter((id: number) => existingIds.has(id)); - - if (validOrder.length > 0) { - setTunnelOrder(validOrder); - } else { - setTunnelOrder(tunnelsData.map((t) => t.id)); - } - } else { - setTunnelOrder(tunnelsData.map((t) => t.id)); - } - } else { - setTunnelOrder(tunnelsData.map((t) => t.id)); - } - } catch { - setTunnelOrder(tunnelsData.map((t) => t.id)); - } + setTunnelOrder( + loadStoredOrder( + "tunnel-order", + tunnelsData.map((t) => t.id), + ), + ); } } else { toast.error(tunnelsRes.msg || "获取隧道列表失败"); @@ -233,59 +195,7 @@ export default function TunnelPage() { // 表单验证 const validateForm = (): boolean => { - const newErrors: { [key: string]: string } = {}; - - if (!form.name.trim()) { - newErrors.name = "请输入隧道名称"; - } else if (form.name.length < 2 || form.name.length > 50) { - newErrors.name = "隧道名称长度应在2-50个字符之间"; - } - - if (!form.inNodeId || form.inNodeId.length === 0) { - newErrors.inNodeId = "请至少选择一个入口节点"; - } else { - // 验证所有选择的节点都在线 - const offlineNodes = form.inNodeId.filter((item) => { - const node = nodes.find((n) => n.id === item.nodeId); - - return node && node.status !== 1; - }); - - if (offlineNodes.length > 0) { - newErrors.inNodeId = "所有入口节点必须在线"; - } - } - - if (form.trafficRatio <= 0 || form.trafficRatio > 100.0) { - newErrors.trafficRatio = "流量倍率须大于0,支持小数(如 0.5)"; - } - - // 隧道转发时的验证 - if (form.type === 2) { - if (!form.outNodeId || form.outNodeId.length === 0) { - newErrors.outNodeId = "请至少选择一个出口节点"; - } else { - // 验证所有选择的节点都在线 - const offlineNodes = form.outNodeId.filter((item) => { - const node = nodes.find((n) => n.id === item.nodeId); - - return node && node.status !== 1; - }); - - if (offlineNodes.length > 0) { - newErrors.outNodeId = "所有出口节点必须在线"; - } - - // 检查是否有重复节点 - const inNodeIds = form.inNodeId.map((item) => item.nodeId); - const outNodeIds = form.outNodeId.map((item) => item.nodeId); - const overlap = inNodeIds.filter((id) => outNodeIds.includes(id)); - - if (overlap.length > 0) { - newErrors.outNodeId = "隧道转发模式下,入口和出口不能有相同节点"; - } - } - } + const newErrors = validateTunnelForm(form, nodes); setErrors(newErrors); @@ -295,18 +205,7 @@ export default function TunnelPage() { // 新增隧道 const handleAdd = () => { setIsEdit(false); - setForm({ - name: "", - type: 1, - inNodeId: [], - outNodeId: [], - chainNodes: [], - flow: 1, - trafficRatio: 1.0, - inIp: "", - ipPreference: "", - status: 1, - }); + setForm(createTunnelFormDefaults()); setErrors({}); setModalOpen(true); }; @@ -518,91 +417,33 @@ export default function TunnelPage() { const response = await diagnoseTunnel(tunnel.id); if (response.code === 0) { - setDiagnosisResult(response.data); + setDiagnosisResult(response.data as DiagnosisResult); } else { toast.error(response.msg || "诊断失败"); - setDiagnosisResult({ - tunnelName: tunnel.name, - tunnelType: tunnel.type === 1 ? "端口转发" : "隧道转发", - timestamp: Date.now(), - results: [ - { - success: false, - description: "诊断失败", - nodeName: "-", - nodeId: "-", - targetIp: "-", - targetPort: 443, - message: response.msg || "诊断过程中发生错误", - }, - ], - }); + setDiagnosisResult( + buildDiagnosisFallbackResult({ + tunnelName: tunnel.name, + tunnelType: tunnel.type, + description: "诊断失败", + message: response.msg || "诊断过程中发生错误", + }), + ); } } catch { toast.error("网络错误,请重试"); - setDiagnosisResult({ - tunnelName: tunnel.name, - tunnelType: tunnel.type === 1 ? "端口转发" : "隧道转发", - timestamp: Date.now(), - results: [ - { - success: false, - description: "网络错误", - nodeName: "-", - nodeId: "-", - targetIp: "-", - targetPort: 443, - message: "无法连接到服务器", - }, - ], - }); + setDiagnosisResult( + buildDiagnosisFallbackResult({ + tunnelName: tunnel.name, + tunnelType: tunnel.type, + description: "网络错误", + message: "无法连接到服务器", + }), + ); } finally { setDiagnosisLoading(false); } }; - // 获取类型显示 - const getTypeDisplay = (type: number) => { - switch (type) { - case 1: - return { text: "端口转发", color: "primary" }; - case 2: - return { text: "隧道转发", color: "secondary" }; - default: - return { text: "未知", color: "default" }; - } - }; - - // 获取流量计算显示 - const getFlowDisplay = (flow: number) => { - switch (flow) { - case 1: - return "单向计算"; - case 2: - return "双向计算"; - default: - return "未知"; - } - }; - - // 获取连接质量 - const getQualityDisplay = (averageTime?: number, packetLoss?: number) => { - if (averageTime === undefined || packetLoss === undefined) return null; - - if (averageTime < 30 && packetLoss === 0) - return { text: "🚀 优秀", color: "success" }; - if (averageTime < 50 && packetLoss === 0) - return { text: "✨ 很好", color: "success" }; - if (averageTime < 100 && packetLoss < 1) - return { text: "👍 良好", color: "primary" }; - if (averageTime < 150 && packetLoss < 2) - return { text: "😐 一般", color: "warning" }; - if (averageTime < 200 && packetLoss < 5) - return { text: "😟 较差", color: "warning" }; - - return { text: "😵 很差", color: "danger" }; - }; - // 处理拖拽结束 const handleDragEnd = async (event: DragEndEvent) => { const { active, over } = event; @@ -624,10 +465,7 @@ export default function TunnelPage() { setTunnelOrder(newOrder); - // 保存到 localStorage - try { - localStorage.setItem("tunnel-order", JSON.stringify(newOrder)); - } catch {} + saveOrder("tunnel-order", newOrder); // 持久化到数据库 try { @@ -701,8 +539,8 @@ export default function TunnelPage() { } else { toast.error(res.msg || "删除失败"); } - } catch (e: any) { - toast.error(e.message || "删除失败"); + } catch (error) { + toast.error(extractApiErrorMessage(error, "删除失败")); } finally { setBatchLoading(false); } @@ -730,8 +568,8 @@ export default function TunnelPage() { } else { toast.error(res.msg || "下发失败"); } - } catch (e: any) { - toast.error(e.message || "下发失败"); + } catch (error) { + toast.error(extractApiErrorMessage(error, "下发失败")); } finally { setBatchLoading(false); } @@ -829,14 +667,7 @@ export default function TunnelPage() { }; if (loading) { - return ( - - - - 正在加载... - - - ); + return ; } return ( @@ -902,7 +733,7 @@ export default function TunnelPage() { > {sortedTunnels.map((tunnel) => { - const typeDisplay = getTypeDisplay(tunnel.type); + const typeDisplay = getTunnelTypeDisplay(tunnel.type); return ( @@ -942,6 +773,7 @@ export default function TunnelPage() { title="拖拽排序" > - {getFlowDisplay(tunnel.flow)} + {getTunnelFlowDisplay(tunnel.flow)} @@ -1091,6 +928,7 @@ export default function TunnelPage() { size="sm" startContent={ removeChainNode(groupIndex)} > - addNodeToChain(groupIndex, nodeId), - ); + addedIds.forEach((nodeId) => { + addNodeToChain(groupIndex, nodeId); + }); // 删除取消选择的节点 - removedIds.forEach((nodeId) => + removedIds.forEach((nodeId) => { removeNodeFromChain( groupIndex, nodeId, - ), - ); + ); + }); }} > {nodes.map((node) => ( @@ -2193,7 +2037,7 @@ export default function TunnelPage() { {results.map((result, index) => { - const quality = getQualityDisplay( + const quality = getDiagnosisQualityDisplay( result.averageTime, result.packetLoss, ); @@ -2366,7 +2210,7 @@ export default function TunnelPage() { {results.map((result, index) => { - const quality = getQualityDisplay( + const quality = getDiagnosisQualityDisplay( result.averageTime, result.packetLoss, ); @@ -2522,6 +2366,7 @@ export default function TunnelPage() { { @@ -228,9 +229,7 @@ export default function UserPage() { }); if (response.code === 0) { - const data = response.data || {}; - - setUsers(data || []); + setUsers(Array.isArray(response.data) ? response.data : []); } else { toast.error(response.msg || "获取用户列表失败"); } @@ -246,7 +245,7 @@ export default function UserPage() { const response = await getTunnelList(); if (response.code === 0) { - setTunnels(response.data || []); + setTunnels(Array.isArray(response.data) ? response.data : []); } } catch {} }; @@ -256,7 +255,15 @@ export default function UserPage() { const response = await getSpeedLimitList(); if (response.code === 0) { - setSpeedLimits(response.data || []); + const speedLimitList = Array.isArray(response.data) + ? response.data.map((item) => ({ + ...item, + uploadSpeed: item.uploadSpeed ?? item.speed ?? 0, + downloadSpeed: item.downloadSpeed ?? item.speed ?? 0, + })) + : []; + + setSpeedLimits(speedLimitList); } } catch {} }; @@ -266,7 +273,7 @@ export default function UserPage() { const response = await getUserGroupList(); if (response.code === 0) { - setUserGroups(response.data || []); + setUserGroups(Array.isArray(response.data) ? response.data : []); } } catch {} }; @@ -277,7 +284,7 @@ export default function UserPage() { const response = await getUserTunnelList({ userId }); if (response.code === 0) { - setUserTunnels(response.data || []); + setUserTunnels(Array.isArray(response.data) ? response.data : []); } else { toast.error(response.msg || "获取隧道权限列表失败"); } @@ -626,10 +633,11 @@ export default function UserPage() { /> @@ -643,12 +651,7 @@ export default function UserPage() { {/* 用户列表 */} {loading ? ( - - - - 正在加载... - - + ) : users.length === 0 ? ( @@ -799,6 +802,7 @@ export default function UserPage() { size="sm" startContent={ handleEditTunnel(userTunnel)} + onPress={() => handleEditTunnel(userTunnel)} > handleResetTunnelFlow(userTunnel)} + onPress={() => handleResetTunnelFlow(userTunnel)} > handleRemoveTunnel(userTunnel)} + onPress={() => handleRemoveTunnel(userTunnel)} > @@ -1596,6 +1604,7 @@ export default function UserPage() { { - localStorage.clear(); + clearSession(); }; diff --git a/vite-frontend/tailwind.config.js b/vite-frontend/tailwind.config.js index db470c4..d370467 100644 --- a/vite-frontend/tailwind.config.js +++ b/vite-frontend/tailwind.config.js @@ -1,5 +1,3 @@ -import {heroui} from "@heroui/theme" - /** @type {import('tailwindcss').Config} */ export default { content: [ @@ -7,11 +5,100 @@ export default { './src/layouts/**/*.{js,ts,jsx,tsx,mdx}', './src/pages/**/*.{js,ts,jsx,tsx,mdx}', './src/components/**/*.{js,ts,jsx,tsx,mdx}', - "./node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}", + './src/shadcn-bridge/**/*.{js,ts,jsx,tsx,mdx}', ], theme: { - extend: {}, + extend: { + colors: { + background: "var(--background)", + foreground: "var(--foreground)", + border: "var(--border)", + input: "var(--input)", + ring: "var(--ring)", + content1: "var(--content1)", + divider: "var(--divider)", + default: { + 50: "var(--default-50)", + 100: "var(--default-100)", + 200: "var(--default-200)", + 300: "var(--default-300)", + 400: "var(--default-400)", + 500: "var(--default-500)", + 600: "var(--default-600)", + 700: "var(--default-700)", + 800: "var(--default-800)", + 900: "var(--default-900)", + }, + primary: { + DEFAULT: "var(--primary)", + foreground: "var(--primary-foreground)", + 50: "var(--primary-50)", + 100: "var(--primary-100)", + 200: "var(--primary-200)", + 300: "var(--primary-300)", + 400: "var(--primary-400)", + 500: "var(--primary-500)", + 600: "var(--primary-600)", + 700: "var(--primary-700)", + 800: "var(--primary-800)", + 900: "var(--primary-900)", + }, + secondary: { + DEFAULT: "var(--secondary)", + foreground: "var(--secondary-foreground)", + 50: "var(--secondary-50)", + 100: "var(--secondary-100)", + 200: "var(--secondary-200)", + 300: "var(--secondary-300)", + 400: "var(--secondary-400)", + 500: "var(--secondary-500)", + 600: "var(--secondary-600)", + 700: "var(--secondary-700)", + 800: "var(--secondary-800)", + 900: "var(--secondary-900)", + }, + danger: { + DEFAULT: "var(--danger)", + 50: "var(--danger-50)", + 100: "var(--danger-100)", + 200: "var(--danger-200)", + 300: "var(--danger-300)", + 400: "var(--danger-400)", + 500: "var(--danger-500)", + 600: "var(--danger-600)", + 700: "var(--danger-700)", + 800: "var(--danger-800)", + 900: "var(--danger-900)", + }, + success: { + DEFAULT: "var(--success)", + 50: "var(--success-50)", + 100: "var(--success-100)", + 200: "var(--success-200)", + 300: "var(--success-300)", + 400: "var(--success-400)", + 500: "var(--success-500)", + 600: "var(--success-600)", + 700: "var(--success-700)", + 800: "var(--success-800)", + 900: "var(--success-900)", + }, + warning: { + DEFAULT: "var(--warning)", + 50: "var(--warning-50)", + 100: "var(--warning-100)", + 200: "var(--warning-200)", + 300: "var(--warning-300)", + 400: "var(--warning-400)", + 500: "var(--warning-500)", + 600: "var(--warning-600)", + 700: "var(--warning-700)", + 800: "var(--warning-800)", + 900: "var(--warning-900)", + }, + }, + }, }, darkMode: "class", - plugins: [heroui()], + plugins: [], } diff --git a/vite-frontend/tsconfig.json b/vite-frontend/tsconfig.json index 456359a..9fbb106 100644 --- a/vite-frontend/tsconfig.json +++ b/vite-frontend/tsconfig.json @@ -8,16 +8,12 @@ "paths": { "@/*": ["./src/*"] }, - - /* Bundler mode */ "moduleResolution": "bundler", "allowImportingTsExtensions": true, "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", - - /* Linting */ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true,
- {announcement.content} -
- 总流量 -
- {formatFlow(userInfo.flow, "gb")} -
- 已用流量 -
- {formatFlow(calculateUserTotalUsedFlow())} -
- {userInfo.flow === 99999 - ? "无限制" - : `${calculateUsagePercentage("flow").toFixed(1)}%`} -
- 转发配额 -
- {formatNumber(userInfo.num || 0)} -
- 已用转发 -
- {forwardList.length} -
- {userInfo.num === 99999 - ? "无限制" - : `${calculateUsagePercentage("forwards").toFixed(1)}%`} -
暂无流量统计数据
{`时间: ${label ?? ""}`}
- {`流量: ${formatFlow(flowValue)}`} -
+ {userInfo.flow === 99999 + ? "无限制" + : `${calculateUsagePercentage("flow").toFixed(1)}%`} +
+ {userInfo.num === 99999 + ? "无限制" + : `${calculateUsagePercentage("forwards").toFixed(1)}%`} +
暂无隧道权限
- 还没有创建任何节点配置,点击上方按钮开始创建 -