mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-10-07 02:06:38 +08:00
style: 美化用户隧道权限分配表单UI并统一代码格式
- 使用HeroUI Checkbox组件替换原生checkbox - 重构隧道列表为card-based tile风格 - 添加选中/未选中/已分配状态视觉区分 - 应用eslint --fix代码格式统一 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
+68
-58
@@ -12,11 +12,9 @@ import ProfilePage from "@/pages/profile";
|
|||||||
import LimitPage from "@/pages/limit";
|
import LimitPage from "@/pages/limit";
|
||||||
import ConfigPage from "@/pages/config";
|
import ConfigPage from "@/pages/config";
|
||||||
import { SettingsPage } from "@/pages/settings";
|
import { SettingsPage } from "@/pages/settings";
|
||||||
|
|
||||||
import AdminLayout from "@/layouts/admin";
|
import AdminLayout from "@/layouts/admin";
|
||||||
import H5Layout from "@/layouts/h5";
|
import H5Layout from "@/layouts/h5";
|
||||||
import H5SimpleLayout from "@/layouts/h5-simple";
|
import H5SimpleLayout from "@/layouts/h5-simple";
|
||||||
|
|
||||||
import { isLoggedIn } from "@/utils/auth";
|
import { isLoggedIn } from "@/utils/auth";
|
||||||
import { siteConfig } from "@/config/site";
|
import { siteConfig } from "@/config/site";
|
||||||
|
|
||||||
@@ -27,11 +25,14 @@ const useH5Mode = () => {
|
|||||||
// 检测移动设备或小屏幕
|
// 检测移动设备或小屏幕
|
||||||
const isMobile = window.innerWidth <= 768;
|
const isMobile = window.innerWidth <= 768;
|
||||||
// 检测是否为移动端浏览器
|
// 检测是否为移动端浏览器
|
||||||
const isMobileBrowser = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
const isMobileBrowser =
|
||||||
|
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
|
||||||
|
navigator.userAgent,
|
||||||
|
);
|
||||||
// 检测URL参数是否包含h5模式
|
// 检测URL参数是否包含h5模式
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
const isH5Param = urlParams.get('h5') === 'true';
|
const isH5Param = urlParams.get("h5") === "true";
|
||||||
|
|
||||||
return isMobile || isMobileBrowser || isH5Param;
|
return isMobile || isMobileBrowser || isH5Param;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -42,39 +43,50 @@ const useH5Mode = () => {
|
|||||||
// 检测移动设备或小屏幕
|
// 检测移动设备或小屏幕
|
||||||
const isMobile = window.innerWidth <= 768;
|
const isMobile = window.innerWidth <= 768;
|
||||||
// 检测是否为移动端浏览器
|
// 检测是否为移动端浏览器
|
||||||
const isMobileBrowser = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
const isMobileBrowser =
|
||||||
|
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
|
||||||
|
navigator.userAgent,
|
||||||
|
);
|
||||||
// 检测URL参数是否包含h5模式
|
// 检测URL参数是否包含h5模式
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
const isH5Param = urlParams.get('h5') === 'true';
|
const isH5Param = urlParams.get("h5") === "true";
|
||||||
|
|
||||||
setIsH5(isMobile || isMobileBrowser || isH5Param);
|
setIsH5(isMobile || isMobileBrowser || isH5Param);
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('resize', checkH5Mode);
|
window.addEventListener("resize", checkH5Mode);
|
||||||
|
|
||||||
return () => window.removeEventListener('resize', checkH5Mode);
|
return () => window.removeEventListener("resize", checkH5Mode);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return isH5;
|
return isH5;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 简化的路由保护组件 - 使用 React Router 导航避免循环
|
// 简化的路由保护组件 - 使用 React Router 导航避免循环
|
||||||
const ProtectedRoute = ({ children, useSimpleLayout = false, skipLayout = false }: { children: React.ReactNode, useSimpleLayout?: boolean, skipLayout?: boolean }) => {
|
const ProtectedRoute = ({
|
||||||
|
children,
|
||||||
|
useSimpleLayout = false,
|
||||||
|
skipLayout = false,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
useSimpleLayout?: boolean;
|
||||||
|
skipLayout?: boolean;
|
||||||
|
}) => {
|
||||||
const authenticated = isLoggedIn();
|
const authenticated = isLoggedIn();
|
||||||
const isH5 = useH5Mode();
|
const isH5 = useH5Mode();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!authenticated) {
|
if (!authenticated) {
|
||||||
// 使用 React Router 导航,避免无限跳转
|
// 使用 React Router 导航,避免无限跳转
|
||||||
navigate('/', { replace: true });
|
navigate("/", { replace: true });
|
||||||
}
|
}
|
||||||
}, [authenticated, navigate]);
|
}, [authenticated, navigate]);
|
||||||
|
|
||||||
if (!authenticated) {
|
if (!authenticated) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center min-h-screen bg-white dark:bg-black">
|
<div className="flex items-center justify-center min-h-screen bg-white dark:bg-black">
|
||||||
<div className="text-lg text-gray-700 dark:text-gray-200"></div>
|
<div className="text-lg text-gray-700 dark:text-gray-200" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -86,6 +98,7 @@ const ProtectedRoute = ({ children, useSimpleLayout = false, skipLayout = false
|
|||||||
|
|
||||||
// 根据模式和页面类型选择布局
|
// 根据模式和页面类型选择布局
|
||||||
let Layout;
|
let Layout;
|
||||||
|
|
||||||
if (isH5 && useSimpleLayout) {
|
if (isH5 && useSimpleLayout) {
|
||||||
Layout = H5SimpleLayout;
|
Layout = H5SimpleLayout;
|
||||||
} else if (isH5) {
|
} else if (isH5) {
|
||||||
@@ -93,31 +106,30 @@ const ProtectedRoute = ({ children, useSimpleLayout = false, skipLayout = false
|
|||||||
} else {
|
} else {
|
||||||
Layout = AdminLayout;
|
Layout = AdminLayout;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Layout>{children}</Layout>;
|
return <Layout>{children}</Layout>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// 登录页面路由组件 - 已登录则重定向到dashboard
|
// 登录页面路由组件 - 已登录则重定向到dashboard
|
||||||
const LoginRoute = () => {
|
const LoginRoute = () => {
|
||||||
const authenticated = isLoggedIn();
|
const authenticated = isLoggedIn();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (authenticated) {
|
if (authenticated) {
|
||||||
// 使用 React Router 导航,避免无限跳转
|
// 使用 React Router 导航,避免无限跳转
|
||||||
navigate('/dashboard', { replace: true });
|
navigate("/dashboard", { replace: true });
|
||||||
}
|
}
|
||||||
}, [authenticated, navigate]);
|
}, [authenticated, navigate]);
|
||||||
|
|
||||||
if (authenticated) {
|
if (authenticated) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center min-h-screen bg-gray-100 dark:bg-black">
|
<div className="flex items-center justify-center min-h-screen bg-gray-100 dark:bg-black">
|
||||||
<div className="text-lg text-gray-700 dark:text-gray-200"></div>
|
<div className="text-lg text-gray-700 dark:text-gray-200" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <IndexPage />;
|
return <IndexPage />;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -125,18 +137,19 @@ function App() {
|
|||||||
// 立即设置页面标题(使用已从缓存读取的配置)
|
// 立即设置页面标题(使用已从缓存读取的配置)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.title = siteConfig.name;
|
document.title = siteConfig.name;
|
||||||
|
|
||||||
// 异步检查是否有配置更新
|
// 异步检查是否有配置更新
|
||||||
const checkTitleUpdate = async () => {
|
const checkTitleUpdate = async () => {
|
||||||
try {
|
try {
|
||||||
// 引入必要的函数
|
// 引入必要的函数
|
||||||
const { getCachedConfig } = await import('@/config/site');
|
const { getCachedConfig } = await import("@/config/site");
|
||||||
const cachedAppName = await getCachedConfig('app_name');
|
const cachedAppName = await getCachedConfig("app_name");
|
||||||
|
|
||||||
if (cachedAppName && cachedAppName !== document.title) {
|
if (cachedAppName && cachedAppName !== document.title) {
|
||||||
document.title = cachedAppName;
|
document.title = cachedAppName;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('检查标题更新失败:', error);
|
console.warn("检查标题更新失败:", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -148,83 +161,80 @@ function App() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<LoginRoute />} />
|
<Route element={<LoginRoute />} path="/" />
|
||||||
<Route
|
<Route
|
||||||
path="/change-password"
|
|
||||||
element={
|
element={
|
||||||
<ProtectedRoute skipLayout={true}>
|
<ProtectedRoute skipLayout={true}>
|
||||||
<ChangePasswordPage />
|
<ChangePasswordPage />
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
}
|
}
|
||||||
|
path="/change-password"
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/dashboard"
|
|
||||||
element={
|
element={
|
||||||
<ProtectedRoute>
|
<ProtectedRoute>
|
||||||
<DashboardPage />
|
<DashboardPage />
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
}
|
}
|
||||||
|
path="/dashboard"
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/forward"
|
|
||||||
element={
|
element={
|
||||||
<ProtectedRoute>
|
<ProtectedRoute>
|
||||||
<ForwardPage />
|
<ForwardPage />
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
}
|
}
|
||||||
|
path="/forward"
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/tunnel"
|
|
||||||
element={
|
element={
|
||||||
<ProtectedRoute>
|
<ProtectedRoute>
|
||||||
<TunnelPage />
|
<TunnelPage />
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
}
|
}
|
||||||
|
path="/tunnel"
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/node"
|
|
||||||
element={
|
element={
|
||||||
<ProtectedRoute>
|
<ProtectedRoute>
|
||||||
<NodePage />
|
<NodePage />
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
}
|
}
|
||||||
|
path="/node"
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/user"
|
|
||||||
element={
|
element={
|
||||||
<ProtectedRoute useSimpleLayout={true}>
|
<ProtectedRoute useSimpleLayout={true}>
|
||||||
<UserPage />
|
<UserPage />
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
}
|
}
|
||||||
|
path="/user"
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/profile"
|
|
||||||
element={
|
element={
|
||||||
<ProtectedRoute>
|
<ProtectedRoute>
|
||||||
<ProfilePage />
|
<ProfilePage />
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
}
|
}
|
||||||
|
path="/profile"
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/limit"
|
|
||||||
element={
|
element={
|
||||||
<ProtectedRoute useSimpleLayout={true}>
|
<ProtectedRoute useSimpleLayout={true}>
|
||||||
<LimitPage />
|
<LimitPage />
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
}
|
}
|
||||||
|
path="/limit"
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/config"
|
|
||||||
element={
|
element={
|
||||||
<ProtectedRoute useSimpleLayout={true}>
|
<ProtectedRoute useSimpleLayout={true}>
|
||||||
<ConfigPage />
|
<ConfigPage />
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
}
|
}
|
||||||
/>
|
path="/config"
|
||||||
<Route
|
|
||||||
path="/settings"
|
|
||||||
element={<SettingsPage />}
|
|
||||||
/>
|
/>
|
||||||
|
<Route element={<SettingsPage />} path="/settings" />
|
||||||
</Routes>
|
</Routes>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import Network from './network';
|
import Network from "./network";
|
||||||
|
|
||||||
// 登陆相关接口
|
// 登陆相关接口
|
||||||
export interface LoginData {
|
export interface LoginData {
|
||||||
@@ -14,11 +14,13 @@ export interface LoginResponse {
|
|||||||
requirePasswordChange?: boolean;
|
requirePasswordChange?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const login = (data: LoginData) => Network.post<LoginResponse>("/user/login", data);
|
export const login = (data: LoginData) =>
|
||||||
|
Network.post<LoginResponse>("/user/login", data);
|
||||||
|
|
||||||
// 用户CRUD操作 - 全部使用POST请求
|
// 用户CRUD操作 - 全部使用POST请求
|
||||||
export const createUser = (data: any) => Network.post("/user/create", data);
|
export const createUser = (data: any) => Network.post("/user/create", data);
|
||||||
export const getAllUsers = (pageData: any = {}) => Network.post("/user/list", pageData);
|
export const getAllUsers = (pageData: any = {}) =>
|
||||||
|
Network.post("/user/list", pageData);
|
||||||
export const updateUser = (data: any) => Network.post("/user/update", data);
|
export const updateUser = (data: any) => Network.post("/user/update", data);
|
||||||
export const deleteUser = (id: number) => Network.post("/user/delete", { id });
|
export const deleteUser = (id: number) => Network.post("/user/delete", { id });
|
||||||
export const getUserPackageInfo = () => Network.post("/user/package");
|
export const getUserPackageInfo = () => Network.post("/user/package");
|
||||||
@@ -28,68 +30,100 @@ export const createNode = (data: any) => Network.post("/node/create", data);
|
|||||||
export const getNodeList = () => Network.post("/node/list");
|
export const getNodeList = () => Network.post("/node/list");
|
||||||
export const updateNode = (data: any) => Network.post("/node/update", data);
|
export const updateNode = (data: any) => Network.post("/node/update", data);
|
||||||
export const deleteNode = (id: number) => Network.post("/node/delete", { id });
|
export const deleteNode = (id: number) => Network.post("/node/delete", { id });
|
||||||
export const getNodeInstallCommand = (id: number) => Network.post("/node/install", { id });
|
export const getNodeInstallCommand = (id: number) =>
|
||||||
export const updateNodeOrder = (data: { nodes: Array<{ id: number; inx: number }> }) => Network.post("/node/update-order", data);
|
Network.post("/node/install", { id });
|
||||||
|
export const updateNodeOrder = (data: {
|
||||||
|
nodes: Array<{ id: number; inx: number }>;
|
||||||
|
}) => Network.post("/node/update-order", data);
|
||||||
export const checkNodeStatus = (nodeId?: number) => {
|
export const checkNodeStatus = (nodeId?: number) => {
|
||||||
const params = nodeId ? { nodeId } : {};
|
const params = nodeId ? { nodeId } : {};
|
||||||
|
|
||||||
return Network.post("/node/check-status", params);
|
return Network.post("/node/check-status", params);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 隧道CRUD操作 - 全部使用POST请求
|
// 隧道CRUD操作 - 全部使用POST请求
|
||||||
export const createTunnel = (data: any) => Network.post("/tunnel/create", data);
|
export const createTunnel = (data: any) => Network.post("/tunnel/create", data);
|
||||||
export const getTunnelList = () => Network.post("/tunnel/list");
|
export const getTunnelList = () => Network.post("/tunnel/list");
|
||||||
export const getTunnelById = (id: number) => Network.post("/tunnel/get", { id });
|
export const getTunnelById = (id: number) =>
|
||||||
|
Network.post("/tunnel/get", { id });
|
||||||
export const updateTunnel = (data: any) => Network.post("/tunnel/update", data);
|
export const updateTunnel = (data: any) => Network.post("/tunnel/update", data);
|
||||||
export const deleteTunnel = (id: number) => Network.post("/tunnel/delete", { id });
|
export const deleteTunnel = (id: number) =>
|
||||||
export const diagnoseTunnel = (tunnelId: number) => Network.post("/tunnel/diagnose", { tunnelId });
|
Network.post("/tunnel/delete", { id });
|
||||||
export const updateTunnelOrder = (data: { tunnels: Array<{ id: number; inx: number }> }) => Network.post("/tunnel/update-order", data);
|
export const diagnoseTunnel = (tunnelId: number) =>
|
||||||
|
Network.post("/tunnel/diagnose", { tunnelId });
|
||||||
|
export const updateTunnelOrder = (data: {
|
||||||
|
tunnels: Array<{ id: number; inx: number }>;
|
||||||
|
}) => Network.post("/tunnel/update-order", data);
|
||||||
|
|
||||||
// 用户隧道权限管理操作 - 全部使用POST请求
|
// 用户隧道权限管理操作 - 全部使用POST请求
|
||||||
export const assignUserTunnel = (data: any) => Network.post("/tunnel/user/assign", data);
|
export const assignUserTunnel = (data: any) =>
|
||||||
export const batchAssignUserTunnel = (data: { userId: number; tunnels: Array<{ tunnelId: number; speedId?: number | null }> }) =>
|
Network.post("/tunnel/user/assign", data);
|
||||||
Network.post("/tunnel/user/batch-assign", data);
|
export const batchAssignUserTunnel = (data: {
|
||||||
export const getUserTunnelList = (queryData: any = {}) => Network.post("/tunnel/user/list", queryData);
|
userId: number;
|
||||||
export const removeUserTunnel = (params: any) => Network.post("/tunnel/user/remove", params);
|
tunnels: Array<{ tunnelId: number; speedId?: number | null }>;
|
||||||
export const updateUserTunnel = (data: any) => Network.post("/tunnel/user/update", data);
|
}) => Network.post("/tunnel/user/batch-assign", data);
|
||||||
|
export const getUserTunnelList = (queryData: any = {}) =>
|
||||||
|
Network.post("/tunnel/user/list", queryData);
|
||||||
|
export const removeUserTunnel = (params: any) =>
|
||||||
|
Network.post("/tunnel/user/remove", params);
|
||||||
|
export const updateUserTunnel = (data: any) =>
|
||||||
|
Network.post("/tunnel/user/update", data);
|
||||||
export const userTunnel = () => Network.post("/tunnel/user/tunnel");
|
export const userTunnel = () => Network.post("/tunnel/user/tunnel");
|
||||||
|
|
||||||
// 转发CRUD操作 - 全部使用POST请求
|
// 转发CRUD操作 - 全部使用POST请求
|
||||||
export const createForward = (data: any) => Network.post("/forward/create", data);
|
export const createForward = (data: any) =>
|
||||||
|
Network.post("/forward/create", data);
|
||||||
export const getForwardList = () => Network.post("/forward/list");
|
export const getForwardList = () => Network.post("/forward/list");
|
||||||
export const updateForward = (data: any) => Network.post("/forward/update", data);
|
export const updateForward = (data: any) =>
|
||||||
export const deleteForward = (id: number) => Network.post("/forward/delete", { id });
|
Network.post("/forward/update", data);
|
||||||
export const forceDeleteForward = (id: number) => Network.post("/forward/force-delete", { id });
|
export const deleteForward = (id: number) =>
|
||||||
|
Network.post("/forward/delete", { id });
|
||||||
|
export const forceDeleteForward = (id: number) =>
|
||||||
|
Network.post("/forward/force-delete", { id });
|
||||||
|
|
||||||
// 转发服务控制操作 - 通过Java后端接口
|
// 转发服务控制操作 - 通过Java后端接口
|
||||||
export const pauseForwardService = (forwardId: number) => Network.post("/forward/pause", { id: forwardId });
|
export const pauseForwardService = (forwardId: number) =>
|
||||||
export const resumeForwardService = (forwardId: number) => Network.post("/forward/resume", { id: forwardId });
|
Network.post("/forward/pause", { id: forwardId });
|
||||||
|
export const resumeForwardService = (forwardId: number) =>
|
||||||
|
Network.post("/forward/resume", { id: forwardId });
|
||||||
|
|
||||||
// 转发诊断操作
|
// 转发诊断操作
|
||||||
export const diagnoseForward = (forwardId: number) => Network.post("/forward/diagnose", { forwardId });
|
export const diagnoseForward = (forwardId: number) =>
|
||||||
|
Network.post("/forward/diagnose", { forwardId });
|
||||||
|
|
||||||
// 转发排序操作
|
// 转发排序操作
|
||||||
export const updateForwardOrder = (data: { forwards: Array<{ id: number; inx: number }> }) => Network.post("/forward/update-order", data);
|
export const updateForwardOrder = (data: {
|
||||||
|
forwards: Array<{ id: number; inx: number }>;
|
||||||
|
}) => Network.post("/forward/update-order", data);
|
||||||
|
|
||||||
// 限速规则CRUD操作 - 全部使用POST请求
|
// 限速规则CRUD操作 - 全部使用POST请求
|
||||||
export const createSpeedLimit = (data: any) => Network.post("/speed-limit/create", data);
|
export const createSpeedLimit = (data: any) =>
|
||||||
|
Network.post("/speed-limit/create", data);
|
||||||
export const getSpeedLimitList = () => Network.post("/speed-limit/list");
|
export const getSpeedLimitList = () => Network.post("/speed-limit/list");
|
||||||
export const updateSpeedLimit = (data: any) => Network.post("/speed-limit/update", data);
|
export const updateSpeedLimit = (data: any) =>
|
||||||
export const deleteSpeedLimit = (id: number) => Network.post("/speed-limit/delete", { id });
|
Network.post("/speed-limit/update", data);
|
||||||
|
export const deleteSpeedLimit = (id: number) =>
|
||||||
|
Network.post("/speed-limit/delete", { id });
|
||||||
|
|
||||||
// 修改密码接口
|
// 修改密码接口
|
||||||
export const updatePassword = (data: any) => Network.post("/user/updatePassword", data);
|
export const updatePassword = (data: any) =>
|
||||||
|
Network.post("/user/updatePassword", data);
|
||||||
|
|
||||||
// 重置流量接口
|
// 重置流量接口
|
||||||
export const resetUserFlow = (data: { id: number; type: number }) => Network.post("/user/reset", data);
|
export const resetUserFlow = (data: { id: number; type: number }) =>
|
||||||
|
Network.post("/user/reset", data);
|
||||||
|
|
||||||
// 网站配置相关接口
|
// 网站配置相关接口
|
||||||
export const getConfigs = () => Network.post("/config/list");
|
export const getConfigs = () => Network.post("/config/list");
|
||||||
export const getConfigByName = (name: string) => Network.post("/config/get", { name });
|
export const getConfigByName = (name: string) =>
|
||||||
export const updateConfigs = (configMap: Record<string, string>) => Network.post("/config/update", configMap);
|
Network.post("/config/get", { name });
|
||||||
export const updateConfig = (name: string, value: string) => Network.post("/config/update-single", { name, value });
|
export const updateConfigs = (configMap: Record<string, string>) =>
|
||||||
|
Network.post("/config/update", configMap);
|
||||||
|
export const updateConfig = (name: string, value: string) =>
|
||||||
|
Network.post("/config/update-single", { name, value });
|
||||||
|
|
||||||
// 验证码相关接口
|
// 验证码相关接口
|
||||||
export const checkCaptcha = () => Network.post("/captcha/check");
|
export const checkCaptcha = () => Network.post("/captcha/check");
|
||||||
export const generateCaptcha = () => Network.post(`/captcha/generate`);
|
export const generateCaptcha = () => Network.post(`/captcha/generate`);
|
||||||
export const verifyCaptcha = (data: { captchaId: string; trackData: string }) => Network.post("/captcha/verify", data);
|
export const verifyCaptcha = (data: { captchaId: string; trackData: string }) =>
|
||||||
|
Network.post("/captcha/verify", data);
|
||||||
|
|||||||
@@ -1,41 +1,42 @@
|
|||||||
import axios, { AxiosResponse } from 'axios';
|
import axios, { AxiosResponse } from "axios";
|
||||||
import { getPanelAddresses, isWebViewFunc} from '@/utils/panel';
|
|
||||||
|
|
||||||
|
import { getPanelAddresses, isWebViewFunc } from "@/utils/panel";
|
||||||
|
|
||||||
interface PanelAddress {
|
interface PanelAddress {
|
||||||
name: string;
|
name: string;
|
||||||
address: string;
|
address: string;
|
||||||
inx: boolean;
|
inx: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const setPanelAddressesFunc = (newAddress: PanelAddress[]) => {
|
const setPanelAddressesFunc = (newAddress: PanelAddress[]) => {
|
||||||
newAddress.forEach(item => {
|
newAddress.forEach((item) => {
|
||||||
if (item.inx) {
|
if (item.inx) {
|
||||||
baseURL = `${item.address}/api/v1/`;
|
baseURL = `${item.address}/api/v1/`;
|
||||||
axios.defaults.baseURL = baseURL;
|
axios.defaults.baseURL = baseURL;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
function getWebViewPanelAddress() {
|
|
||||||
(window as any).setAddresses = setPanelAddressesFunc
|
|
||||||
getPanelAddresses("setAddresses");
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let baseURL: string = '';
|
function getWebViewPanelAddress() {
|
||||||
|
(window as any).setAddresses = setPanelAddressesFunc;
|
||||||
|
getPanelAddresses("setAddresses");
|
||||||
|
}
|
||||||
|
|
||||||
|
let baseURL: string = "";
|
||||||
|
|
||||||
export const reinitializeBaseURL = () => {
|
export const reinitializeBaseURL = () => {
|
||||||
if (isWebViewFunc()) {
|
if (isWebViewFunc()) {
|
||||||
getWebViewPanelAddress();
|
getWebViewPanelAddress();
|
||||||
} else {
|
} else {
|
||||||
baseURL = import.meta.env.VITE_API_BASE ? `${import.meta.env.VITE_API_BASE}/api/v1/` : '/api/v1/';
|
baseURL = import.meta.env.VITE_API_BASE
|
||||||
|
? `${import.meta.env.VITE_API_BASE}/api/v1/`
|
||||||
|
: "/api/v1/";
|
||||||
axios.defaults.baseURL = baseURL;
|
axios.defaults.baseURL = baseURL;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
reinitializeBaseURL();
|
reinitializeBaseURL();
|
||||||
|
|
||||||
|
|
||||||
interface ApiResponse<T = any> {
|
interface ApiResponse<T = any> {
|
||||||
code: number;
|
code: number;
|
||||||
msg: string;
|
msg: string;
|
||||||
@@ -45,98 +46,123 @@ interface ApiResponse<T = any> {
|
|||||||
// 处理token失效的逻辑
|
// 处理token失效的逻辑
|
||||||
function handleTokenExpired() {
|
function handleTokenExpired() {
|
||||||
// 清除localStorage中的token
|
// 清除localStorage中的token
|
||||||
window.localStorage.removeItem('token');
|
window.localStorage.removeItem("token");
|
||||||
window.localStorage.removeItem('role_id');
|
window.localStorage.removeItem("role_id");
|
||||||
window.localStorage.removeItem('name');
|
window.localStorage.removeItem("name");
|
||||||
|
|
||||||
// 跳转到登录页面
|
// 跳转到登录页面
|
||||||
if (window.location.pathname !== '/') {
|
if (window.location.pathname !== "/") {
|
||||||
window.location.href = '/';
|
window.location.href = "/";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查响应是否为token失效
|
// 检查响应是否为token失效
|
||||||
function isTokenExpired(response: ApiResponse) {
|
function isTokenExpired(response: ApiResponse) {
|
||||||
return response && response.code === 401 &&
|
return (
|
||||||
(response.msg === '未登录或token已过期' ||
|
response &&
|
||||||
response.msg === '无效的token或token已过期' ||
|
response.code === 401 &&
|
||||||
response.msg === '无法获取用户权限信息');
|
(response.msg === "未登录或token已过期" ||
|
||||||
|
response.msg === "无效的token或token已过期" ||
|
||||||
|
response.msg === "无法获取用户权限信息")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const Network = {
|
const Network = {
|
||||||
get: function<T = any>(path: string = '', data: any = {}): Promise<ApiResponse<T>> {
|
get: function <T = any>(
|
||||||
return new Promise(function(resolve) {
|
path: string = "",
|
||||||
|
data: any = {},
|
||||||
|
): Promise<ApiResponse<T>> {
|
||||||
|
return new Promise(function (resolve) {
|
||||||
// 如果baseURL是默认值且是WebView环境,说明没有设置面板地址
|
// 如果baseURL是默认值且是WebView环境,说明没有设置面板地址
|
||||||
if (baseURL === '') {
|
if (baseURL === "") {
|
||||||
resolve({"code": -1, "msg": " - 请先设置面板地址", "data": null as T});
|
resolve({ code: -1, msg: " - 请先设置面板地址", data: null as T });
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
axios.get(path, {
|
axios
|
||||||
params: data,
|
.get(path, {
|
||||||
timeout: 30000,
|
params: data,
|
||||||
headers: {
|
timeout: 30000,
|
||||||
"Authorization": window.localStorage.getItem('token')
|
headers: {
|
||||||
}
|
Authorization: window.localStorage.getItem("token"),
|
||||||
})
|
},
|
||||||
.then(function(response: AxiosResponse<ApiResponse<T>>) {
|
})
|
||||||
|
.then(function (response: AxiosResponse<ApiResponse<T>>) {
|
||||||
// 检查是否token失效
|
// 检查是否token失效
|
||||||
if (isTokenExpired(response.data)) {
|
if (isTokenExpired(response.data)) {
|
||||||
handleTokenExpired();
|
handleTokenExpired();
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
resolve(response.data);
|
resolve(response.data);
|
||||||
})
|
})
|
||||||
.catch(function(error: any) {
|
.catch(function (error: any) {
|
||||||
console.error('GET请求错误:', error);
|
console.error("GET请求错误:", error);
|
||||||
|
|
||||||
// 检查是否是401错误(token失效)
|
// 检查是否是401错误(token失效)
|
||||||
if (error.response && error.response.status === 401) {
|
if (error.response && error.response.status === 401) {
|
||||||
handleTokenExpired();
|
handleTokenExpired();
|
||||||
return;
|
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
resolve({"code": -1, "msg": error.message || "网络请求失败", "data": null as T});
|
|
||||||
});
|
resolve({
|
||||||
|
code: -1,
|
||||||
|
msg: error.message || "网络请求失败",
|
||||||
|
data: null as T,
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
post: function<T = any>(path: string = '', data: any = {}): Promise<ApiResponse<T>> {
|
post: function <T = any>(
|
||||||
return new Promise(function(resolve) {
|
path: string = "",
|
||||||
|
data: any = {},
|
||||||
|
): Promise<ApiResponse<T>> {
|
||||||
|
return new Promise(function (resolve) {
|
||||||
// 如果baseURL是默认值且是WebView环境,说明没有设置面板地址
|
// 如果baseURL是默认值且是WebView环境,说明没有设置面板地址
|
||||||
if (baseURL === '') {
|
if (baseURL === "") {
|
||||||
resolve({"code": -1, "msg": " - 请先设置面板地址", "data": null as T});
|
resolve({ code: -1, msg: " - 请先设置面板地址", data: null as T });
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
axios.post(path, data, {
|
axios
|
||||||
timeout: 30000,
|
.post(path, data, {
|
||||||
headers: {
|
timeout: 30000,
|
||||||
"Authorization": window.localStorage.getItem('token'),
|
headers: {
|
||||||
"Content-Type": "application/json"
|
Authorization: window.localStorage.getItem("token"),
|
||||||
}
|
"Content-Type": "application/json",
|
||||||
})
|
},
|
||||||
.then(function(response: AxiosResponse<ApiResponse<T>>) {
|
})
|
||||||
|
.then(function (response: AxiosResponse<ApiResponse<T>>) {
|
||||||
// 检查是否token失效
|
// 检查是否token失效
|
||||||
if (isTokenExpired(response.data)) {
|
if (isTokenExpired(response.data)) {
|
||||||
handleTokenExpired();
|
handleTokenExpired();
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
resolve(response.data);
|
resolve(response.data);
|
||||||
})
|
})
|
||||||
.catch(function(error: any) {
|
.catch(function (error: any) {
|
||||||
console.error('POST请求错误:', error);
|
console.error("POST请求错误:", error);
|
||||||
|
|
||||||
// 检查是否是401错误(token失效)
|
// 检查是否是401错误(token失效)
|
||||||
if (error.response && error.response.status === 401) {
|
if (error.response && error.response.status === 401) {
|
||||||
handleTokenExpired();
|
handleTokenExpired();
|
||||||
return;
|
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
resolve({"code": -1, "msg": error.message || "网络请求失败", "data": null as T});
|
|
||||||
});
|
resolve({
|
||||||
|
code: -1,
|
||||||
|
msg: error.message || "网络请求失败",
|
||||||
|
data: null as T,
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Network;
|
export default Network;
|
||||||
|
|||||||
@@ -84,11 +84,11 @@ export const SearchIcon = ({
|
|||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M11.5 21C16.7467 21 21 16.7467 21 11.5C21 6.25329 16.7467 2 11.5 2C6.25329 2 2 6.25329 2 11.5C2 16.7467 6.25329 21 11.5 21Z"
|
d="M11.5 21C16.7467 21 21 16.7467 21 11.5C21 6.25329 16.7467 2 11.5 2C6.25329 2 2 6.25329 2 11.5C2 16.7467 6.25329 21 11.5 21Z"
|
||||||
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
strokeWidth="2"
|
strokeWidth="2"
|
||||||
fill="none"
|
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
d="M22 22L20.5 20.5"
|
d="M22 22L20.5 20.5"
|
||||||
@@ -142,19 +142,19 @@ export const EditIcon = ({
|
|||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M11 4H4C3.46957 4 2.96086 4.21071 2.58579 4.58579C2.21071 4.96086 2 5.46957 2 6V20C2 20.5304 2.21071 21.0391 2.58579 21.4142C2.96086 21.7893 3.46957 22 4 22H18C18.5304 22 19.0391 21.7893 19.4142 21.4142C19.7893 21.0391 20 20.5304 20 20V13"
|
d="M11 4H4C3.46957 4 2.96086 4.21071 2.58579 4.58579C2.21071 4.96086 2 5.46957 2 6V20C2 20.5304 2.21071 21.0391 2.58579 21.4142C2.96086 21.7893 3.46957 22 4 22H18C18.5304 22 19.0391 21.7893 19.4142 21.4142C19.7893 21.0391 20 20.5304 20 20V13"
|
||||||
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
strokeWidth="2"
|
strokeWidth="2"
|
||||||
fill="none"
|
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
d="M18.5 2.49998C18.8978 2.10216 19.4374 1.87866 20 1.87866C20.5626 1.87866 21.1022 2.10216 21.5 2.49998C21.8978 2.89781 22.1213 3.43737 22.1213 3.99998C22.1213 4.56259 21.8978 5.10216 21.5 5.49998L12 15L8 16L9 12L18.5 2.49998Z"
|
d="M18.5 2.49998C18.8978 2.10216 19.4374 1.87866 20 1.87866C20.5626 1.87866 21.1022 2.10216 21.5 2.49998C21.8978 2.89781 22.1213 3.43737 22.1213 3.99998C22.1213 4.56259 21.8978 5.10216 21.5 5.49998L12 15L8 16L9 12L18.5 2.49998Z"
|
||||||
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
strokeWidth="2"
|
strokeWidth="2"
|
||||||
fill="none"
|
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
@@ -183,11 +183,11 @@ export const DeleteIcon = ({
|
|||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
d="M8 6V4C8 3.46957 8.21071 2.96086 8.58579 2.58579C8.96086 2.21071 9.46957 2 10 2H14C14.5304 2 15.0391 2.21071 15.4142 2.58579C15.7893 2.96086 16 3.46957 16 4V6M19 6V20C19 20.5304 18.7893 21.0391 18.4142 21.4142C18.0391 21.7893 17.5304 22 17 22H7C6.46957 22 5.96086 21.7893 5.58579 21.4142C5.21071 21.0391 5 20.5304 5 20V6H19Z"
|
d="M8 6V4C8 3.46957 8.21071 2.96086 8.58579 2.58579C8.96086 2.21071 9.46957 2 10 2H14C14.5304 2 15.0391 2.21071 15.4142 2.58579C15.7893 2.96086 16 3.46957 16 4V6M19 6V20C19 20.5304 18.7893 21.0391 18.4142 21.4142C18.0391 21.7893 17.5304 22 17 22H7C6.46957 22 5.96086 21.7893 5.58579 21.4142C5.21071 21.0391 5 20.5304 5 20V6H19Z"
|
||||||
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
strokeWidth="2"
|
strokeWidth="2"
|
||||||
fill="none"
|
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
@@ -209,19 +209,19 @@ export const UserIcon = ({
|
|||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M20 21V19C20 17.9391 19.5786 16.9217 18.8284 16.1716C18.0783 15.4214 17.0609 15 16 15H8C6.93913 15 5.92172 15.4214 5.17157 16.1716C4.42143 16.9217 4 17.9391 4 19V21"
|
d="M20 21V19C20 17.9391 19.5786 16.9217 18.8284 16.1716C18.0783 15.4214 17.0609 15 16 15H8C6.93913 15 5.92172 15.4214 5.17157 16.1716C4.42143 16.9217 4 17.9391 4 19V21"
|
||||||
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
strokeWidth="2"
|
strokeWidth="2"
|
||||||
fill="none"
|
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
d="M12 11C14.2091 11 16 9.20914 16 7C16 4.79086 14.2091 3 12 3C9.79086 3 8 4.79086 8 7C8 9.20914 9.79086 11 12 11Z"
|
d="M12 11C14.2091 11 16 9.20914 16 7C16 4.79086 14.2091 3 12 3C9.79086 3 8 4.79086 8 7C8 9.20914 9.79086 11 12 11Z"
|
||||||
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
strokeWidth="2"
|
strokeWidth="2"
|
||||||
fill="none"
|
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
@@ -243,19 +243,19 @@ export const SettingsIcon = ({
|
|||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M12.22 2H11.78C11.2496 2 10.7409 2.21071 10.3658 2.58579C9.99072 2.96086 9.78 3.46957 9.78 4C9.78 4.53043 9.99072 5.03914 10.3658 5.41421C10.7409 5.78929 11.2496 6 11.78 6H12.22C12.7504 6 13.2591 5.78929 13.6342 5.41421C14.0093 5.03914 14.22 4.53043 14.22 4C14.22 3.46957 14.0093 2.96086 13.6342 2.58579C13.2591 2.21071 12.7504 2 12.22 2V2Z"
|
d="M12.22 2H11.78C11.2496 2 10.7409 2.21071 10.3658 2.58579C9.99072 2.96086 9.78 3.46957 9.78 4C9.78 4.53043 9.99072 5.03914 10.3658 5.41421C10.7409 5.78929 11.2496 6 11.78 6H12.22C12.7504 6 13.2591 5.78929 13.6342 5.41421C14.0093 5.03914 14.22 4.53043 14.22 4C14.22 3.46957 14.0093 2.96086 13.6342 2.58579C13.2591 2.21071 12.7504 2 12.22 2V2Z"
|
||||||
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
strokeWidth="2"
|
strokeWidth="2"
|
||||||
fill="none"
|
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
d="M19.5 8.5C19.8978 8.10218 20.2374 7.63877 20.5 7.12132C20.7626 6.60387 20.8478 6.03677 20.8478 5.464C20.8478 4.89123 20.7626 4.32413 20.5 3.80668C20.2374 3.28923 19.8978 2.82582 19.5 2.428C19.1022 2.03018 18.6388 1.69063 18.1213 1.42801C17.6039 1.16539 17.0368 1.08023 16.464 1.08023C15.8912 1.08023 15.3241 1.16539 14.8067 1.42801C14.2892 1.69063 13.8258 2.03018 13.428 2.428L12 3.856L10.572 2.428C9.78889 1.64486 8.73968 1.21408 7.64800 1.21408C6.55632 1.21408 5.50711 1.64486 4.72400 2.428C3.94086 3.21111 3.51008 4.26032 3.51008 5.352C3.51008 6.44368 3.94086 7.49289 4.72400 8.276L6.15200 9.704L12 15.552L17.848 9.704L19.276 8.276C19.5 8.052 19.5 8.276 19.5 8.5Z"
|
d="M19.5 8.5C19.8978 8.10218 20.2374 7.63877 20.5 7.12132C20.7626 6.60387 20.8478 6.03677 20.8478 5.464C20.8478 4.89123 20.7626 4.32413 20.5 3.80668C20.2374 3.28923 19.8978 2.82582 19.5 2.428C19.1022 2.03018 18.6388 1.69063 18.1213 1.42801C17.6039 1.16539 17.0368 1.08023 16.464 1.08023C15.8912 1.08023 15.3241 1.16539 14.8067 1.42801C14.2892 1.69063 13.8258 2.03018 13.428 2.428L12 3.856L10.572 2.428C9.78889 1.64486 8.73968 1.21408 7.64800 1.21408C6.55632 1.21408 5.50711 1.64486 4.72400 2.428C3.94086 3.21111 3.51008 4.26032 3.51008 5.352C3.51008 6.44368 3.94086 7.49289 4.72400 8.276L6.15200 9.704L12 15.552L17.848 9.704L19.276 8.276C19.5 8.052 19.5 8.276 19.5 8.5Z"
|
||||||
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
strokeWidth="2"
|
strokeWidth="2"
|
||||||
fill="none"
|
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import {
|
|||||||
NavbarBrand,
|
NavbarBrand,
|
||||||
NavbarContent,
|
NavbarContent,
|
||||||
} from "@heroui/navbar";
|
} from "@heroui/navbar";
|
||||||
import { isWebViewFunc } from '@/utils/panel';
|
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
import { isWebViewFunc } from "@/utils/panel";
|
||||||
import { Logo } from "@/components/icons";
|
import { Logo } from "@/components/icons";
|
||||||
import { siteConfig, getCachedConfig } from "@/config/site";
|
import { siteConfig, getCachedConfig } from "@/config/site";
|
||||||
|
|
||||||
@@ -20,21 +20,21 @@ export const Navbar = () => {
|
|||||||
// 检测是否在WebView中运行
|
// 检测是否在WebView中运行
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsWebView(isWebViewFunc());
|
setIsWebView(isWebViewFunc());
|
||||||
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 异步检查是否有更新的配置
|
// 异步检查是否有更新的配置
|
||||||
const checkForUpdates = async () => {
|
const checkForUpdates = async () => {
|
||||||
try {
|
try {
|
||||||
const cachedAppName = await getCachedConfig('app_name');
|
const cachedAppName = await getCachedConfig("app_name");
|
||||||
|
|
||||||
if (cachedAppName && cachedAppName !== appName) {
|
if (cachedAppName && cachedAppName !== appName) {
|
||||||
setAppName(cachedAppName);
|
setAppName(cachedAppName);
|
||||||
// 同步更新siteConfig
|
// 同步更新siteConfig
|
||||||
siteConfig.name = cachedAppName;
|
siteConfig.name = cachedAppName;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('检查配置更新失败:', error);
|
console.warn("检查配置更新失败:", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -44,27 +44,33 @@ export const Navbar = () => {
|
|||||||
// 监听配置更新事件
|
// 监听配置更新事件
|
||||||
const handleConfigUpdate = async () => {
|
const handleConfigUpdate = async () => {
|
||||||
try {
|
try {
|
||||||
const cachedAppName = await getCachedConfig('app_name');
|
const cachedAppName = await getCachedConfig("app_name");
|
||||||
|
|
||||||
if (cachedAppName) {
|
if (cachedAppName) {
|
||||||
setAppName(cachedAppName);
|
setAppName(cachedAppName);
|
||||||
siteConfig.name = cachedAppName;
|
siteConfig.name = cachedAppName;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('更新配置失败:', error);
|
console.warn("更新配置失败:", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('configUpdated', handleConfigUpdate);
|
window.addEventListener("configUpdated", handleConfigUpdate);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
window.removeEventListener('configUpdated', handleConfigUpdate);
|
window.removeEventListener("configUpdated", handleConfigUpdate);
|
||||||
};
|
};
|
||||||
}, [appName]);
|
}, [appName]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<HeroUINavbar maxWidth="xl" position="sticky" height="60px" className="shrink-0">
|
<HeroUINavbar
|
||||||
|
className="shrink-0"
|
||||||
|
height="60px"
|
||||||
|
maxWidth="xl"
|
||||||
|
position="sticky"
|
||||||
|
>
|
||||||
<NavbarContent className="basis-1/5 sm:basis-full" justify="start">
|
<NavbarContent className="basis-1/5 sm:basis-full" justify="start">
|
||||||
<NavbarBrand className="gap-2 max-w-fit">
|
<NavbarBrand className="gap-2 max-w-fit">
|
||||||
<Link
|
<Link
|
||||||
@@ -83,12 +89,27 @@ export const Navbar = () => {
|
|||||||
{isWebView && (
|
{isWebView && (
|
||||||
<button
|
<button
|
||||||
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 transition-colors"
|
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 transition-colors"
|
||||||
onClick={() => navigate('/settings')}
|
|
||||||
title="面板设置"
|
title="面板设置"
|
||||||
|
onClick={() => navigate("/settings")}
|
||||||
>
|
>
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
className="w-5 h-5"
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from "react";
|
||||||
import AdminLayout from '@/layouts/admin';
|
|
||||||
|
import AdminLayout from "@/layouts/admin";
|
||||||
|
|
||||||
interface PageWrapperProps {
|
interface PageWrapperProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -8,11 +9,11 @@ interface PageWrapperProps {
|
|||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PageWrapper({
|
export default function PageWrapper({
|
||||||
children,
|
children,
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
className = "container mx-auto max-w-7xl px-3 lg:px-6 py-8"
|
className = "container mx-auto max-w-7xl px-3 lg:px-6 py-8",
|
||||||
}: PageWrapperProps) {
|
}: PageWrapperProps) {
|
||||||
const [isReady, setIsReady] = useState(false);
|
const [isReady, setIsReady] = useState(false);
|
||||||
|
|
||||||
@@ -31,8 +32,8 @@ export default function PageWrapper({
|
|||||||
<div className="container mx-auto max-w-7xl px-3 lg:px-6 py-8">
|
<div className="container mx-auto max-w-7xl px-3 lg:px-6 py-8">
|
||||||
<div className="flex items-center justify-center h-64">
|
<div className="flex items-center justify-center h-64">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="animate-spin h-5 w-5 border-2 border-gray-200 dark:border-gray-700 border-t-gray-600 dark:border-t-gray-300 rounded-full"></div>
|
<div className="animate-spin h-5 w-5 border-2 border-gray-200 dark:border-gray-700 border-t-gray-600 dark:border-t-gray-300 rounded-full" />
|
||||||
<span className="text-default-600"></span>
|
<span className="text-default-600" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -45,12 +46,10 @@ export default function PageWrapper({
|
|||||||
<div className={className}>
|
<div className={className}>
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<h1 className="text-2xl font-bold mb-2 text-foreground">{title}</h1>
|
<h1 className="text-2xl font-bold mb-2 text-foreground">{title}</h1>
|
||||||
{description && (
|
{description && <p className="text-default-600">{description}</p>}
|
||||||
<p className="text-default-600">{description}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</AdminLayout>
|
</AdminLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from "react";
|
||||||
import { useTheme } from '@heroui/use-theme';
|
import { useTheme } from "@heroui/use-theme";
|
||||||
|
|
||||||
interface ThemeProviderProps {
|
interface ThemeProviderProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -11,17 +11,21 @@ export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 确保主题与HTML class同步
|
// 确保主题与HTML class同步
|
||||||
const updateThemeClass = (currentTheme: string) => {
|
const updateThemeClass = (currentTheme: string) => {
|
||||||
if (currentTheme === 'dark') {
|
if (currentTheme === "dark") {
|
||||||
document.documentElement.classList.add('dark');
|
document.documentElement.classList.add("dark");
|
||||||
document.documentElement.style.colorScheme = 'dark';
|
document.documentElement.style.colorScheme = "dark";
|
||||||
} else {
|
} else {
|
||||||
document.documentElement.classList.remove('dark');
|
document.documentElement.classList.remove("dark");
|
||||||
document.documentElement.style.colorScheme = 'light';
|
document.documentElement.style.colorScheme = "light";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 始终跟随系统主题
|
// 始终跟随系统主题
|
||||||
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
|
||||||
|
.matches
|
||||||
|
? "dark"
|
||||||
|
: "light";
|
||||||
|
|
||||||
if (systemTheme !== theme) {
|
if (systemTheme !== theme) {
|
||||||
setTheme(systemTheme);
|
setTheme(systemTheme);
|
||||||
}
|
}
|
||||||
@@ -30,15 +34,17 @@ export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
|
|||||||
updateThemeClass(theme);
|
updateThemeClass(theme);
|
||||||
|
|
||||||
// 监听系统主题变化
|
// 监听系统主题变化
|
||||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
||||||
const handleThemeChange = (e: MediaQueryListEvent) => {
|
const handleThemeChange = (e: MediaQueryListEvent) => {
|
||||||
const newTheme = e.matches ? 'dark' : 'light';
|
const newTheme = e.matches ? "dark" : "light";
|
||||||
|
|
||||||
setTheme(newTheme);
|
setTheme(newTheme);
|
||||||
};
|
};
|
||||||
|
|
||||||
mediaQuery.addEventListener('change', handleThemeChange);
|
mediaQuery.addEventListener("change", handleThemeChange);
|
||||||
return () => mediaQuery.removeEventListener('change', handleThemeChange);
|
|
||||||
|
return () => mediaQuery.removeEventListener("change", handleThemeChange);
|
||||||
}, [theme, setTheme]);
|
}, [theme, setTheme]);
|
||||||
|
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import { getConfigByName, getConfigs } from '@/api';
|
import { getConfigByName, getConfigs } from "@/api";
|
||||||
|
|
||||||
export type SiteConfig = typeof siteConfig;
|
export type SiteConfig = typeof siteConfig;
|
||||||
|
|
||||||
// 缓存相关常量
|
// 缓存相关常量
|
||||||
const CACHE_PREFIX = 'vite_config_';
|
const CACHE_PREFIX = "vite_config_";
|
||||||
const VERSION = import.meta.env.VITE_APP_VERSION || "dev";
|
const VERSION = import.meta.env.VITE_APP_VERSION || "dev";
|
||||||
const APP_VERSION = "1.0.3";
|
const APP_VERSION = "1.0.3";
|
||||||
const GITHUB_REPO = import.meta.env.VITE_GITHUB_REPO || "https://github.com/Sagit-chu/flux-panel";
|
const GITHUB_REPO =
|
||||||
|
import.meta.env.VITE_GITHUB_REPO || "https://github.com/Sagit-chu/flux-panel";
|
||||||
|
|
||||||
const getInitialConfig = () => {
|
const getInitialConfig = () => {
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === "undefined") {
|
||||||
return {
|
return {
|
||||||
name: "flux",
|
name: "flux",
|
||||||
version: VERSION,
|
version: VERSION,
|
||||||
@@ -18,15 +19,17 @@ const getInitialConfig = () => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const cachedAppName = localStorage.getItem(CACHE_PREFIX + 'app_name');
|
const cachedAppName = localStorage.getItem(CACHE_PREFIX + "app_name");
|
||||||
if (cachedAppName) {
|
|
||||||
return {
|
if (cachedAppName) {
|
||||||
name: cachedAppName,
|
return {
|
||||||
version: VERSION,
|
name: cachedAppName,
|
||||||
app_version: APP_VERSION,
|
version: VERSION,
|
||||||
github_repo: GITHUB_REPO,
|
app_version: APP_VERSION,
|
||||||
};
|
github_repo: GITHUB_REPO,
|
||||||
}
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: "flux",
|
name: "flux",
|
||||||
version: VERSION,
|
version: VERSION,
|
||||||
@@ -41,44 +44,52 @@ export const configCache = {
|
|||||||
// 获取缓存的配置
|
// 获取缓存的配置
|
||||||
get: (key: string): string | null => {
|
get: (key: string): string | null => {
|
||||||
const cacheKey = CACHE_PREFIX + key;
|
const cacheKey = CACHE_PREFIX + key;
|
||||||
return localStorage.getItem(cacheKey);
|
|
||||||
|
return localStorage.getItem(cacheKey);
|
||||||
},
|
},
|
||||||
|
|
||||||
// 设置缓存的配置
|
// 设置缓存的配置
|
||||||
set: (key: string, value: string): void => {
|
set: (key: string, value: string): void => {
|
||||||
const cacheKey = CACHE_PREFIX + key;
|
const cacheKey = CACHE_PREFIX + key;
|
||||||
localStorage.setItem(cacheKey, value);
|
|
||||||
|
localStorage.setItem(cacheKey, value);
|
||||||
},
|
},
|
||||||
|
|
||||||
// 删除指定配置的缓存
|
// 删除指定配置的缓存
|
||||||
remove: (key: string): void => {
|
remove: (key: string): void => {
|
||||||
const cacheKey = CACHE_PREFIX + key;
|
const cacheKey = CACHE_PREFIX + key;
|
||||||
|
|
||||||
localStorage.removeItem(cacheKey);
|
localStorage.removeItem(cacheKey);
|
||||||
},
|
},
|
||||||
|
|
||||||
// 清空所有配置缓存
|
// 清空所有配置缓存
|
||||||
clear: (): void => {
|
clear: (): void => {
|
||||||
// 获取所有localStorage的key
|
// 获取所有localStorage的key
|
||||||
const keys = Object.keys(localStorage);
|
const keys = Object.keys(localStorage);
|
||||||
keys.forEach(key => {
|
|
||||||
if (key.startsWith(CACHE_PREFIX)) {
|
keys.forEach((key) => {
|
||||||
localStorage.removeItem(key);
|
if (key.startsWith(CACHE_PREFIX)) {
|
||||||
}
|
localStorage.removeItem(key);
|
||||||
});
|
}
|
||||||
}
|
});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取单个配置(优先从缓存)
|
// 获取单个配置(优先从缓存)
|
||||||
export const getCachedConfig = async (key: string): Promise<string | null> => {
|
export const getCachedConfig = async (key: string): Promise<string | null> => {
|
||||||
const cachedValue = configCache.get(key);
|
const cachedValue = configCache.get(key);
|
||||||
|
|
||||||
if (cachedValue !== null) {
|
if (cachedValue !== null) {
|
||||||
return cachedValue;
|
return cachedValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await getConfigByName(key);
|
const response = await getConfigByName(key);
|
||||||
|
|
||||||
if (response.code === 0 && response.data?.value) {
|
if (response.code === 0 && response.data?.value) {
|
||||||
const value = response.data.value;
|
const value = response.data.value;
|
||||||
|
|
||||||
configCache.set(key, value);
|
configCache.set(key, value);
|
||||||
|
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,29 +99,31 @@ export const getCachedConfig = async (key: string): Promise<string | null> => {
|
|||||||
// 获取所有配置(优先从缓存)
|
// 获取所有配置(优先从缓存)
|
||||||
export const getCachedConfigs = async (): Promise<Record<string, string>> => {
|
export const getCachedConfigs = async (): Promise<Record<string, string>> => {
|
||||||
// 尝试从缓存获取所有配置
|
// 尝试从缓存获取所有配置
|
||||||
const configKeys = ['app_name'];
|
const configKeys = ["app_name"];
|
||||||
const cachedConfigs: Record<string, string> = {};
|
const cachedConfigs: Record<string, string> = {};
|
||||||
let hasCachedData = false;
|
let hasCachedData = false;
|
||||||
|
|
||||||
configKeys.forEach(key => {
|
configKeys.forEach((key) => {
|
||||||
const cachedValue = configCache.get(key);
|
const cachedValue = configCache.get(key);
|
||||||
|
|
||||||
if (cachedValue !== null) {
|
if (cachedValue !== null) {
|
||||||
cachedConfigs[key] = cachedValue;
|
cachedConfigs[key] = cachedValue;
|
||||||
hasCachedData = true;
|
hasCachedData = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 从API获取最新配置
|
// 从API获取最新配置
|
||||||
try {
|
try {
|
||||||
const response = await getConfigs();
|
const response = await getConfigs();
|
||||||
|
|
||||||
if (response.code === 0 && response.data) {
|
if (response.code === 0 && response.data) {
|
||||||
const configs = response.data;
|
const configs = response.data;
|
||||||
|
|
||||||
// 将所有配置存入缓存
|
// 将所有配置存入缓存
|
||||||
Object.entries(configs).forEach(([key, value]) => {
|
Object.entries(configs).forEach(([key, value]) => {
|
||||||
configCache.set(key, value as string);
|
configCache.set(key, value as string);
|
||||||
});
|
});
|
||||||
|
|
||||||
return configs;
|
return configs;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -125,12 +138,13 @@ export const getCachedConfigs = async (): Promise<Record<string, string>> => {
|
|||||||
|
|
||||||
// 动态更新网站配置
|
// 动态更新网站配置
|
||||||
export const updateSiteConfig = async () => {
|
export const updateSiteConfig = async () => {
|
||||||
const appName = await getCachedConfig('app_name');
|
const appName = await getCachedConfig("app_name");
|
||||||
if (appName && appName !== siteConfig.name) {
|
|
||||||
siteConfig.name = appName;
|
if (appName && appName !== siteConfig.name) {
|
||||||
// 更新页面标题
|
siteConfig.name = appName;
|
||||||
document.title = appName;
|
// 更新页面标题
|
||||||
}
|
document.title = appName;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 清除配置缓存的工具函数
|
// 清除配置缓存的工具函数
|
||||||
@@ -140,7 +154,7 @@ export const updateSiteConfig = async () => {
|
|||||||
export const clearConfigCache = (keys?: string[]) => {
|
export const clearConfigCache = (keys?: string[]) => {
|
||||||
if (keys && keys.length > 0) {
|
if (keys && keys.length > 0) {
|
||||||
// 删除指定的配置缓存
|
// 删除指定的配置缓存
|
||||||
keys.forEach(key => configCache.remove(key));
|
keys.forEach((key) => configCache.remove(key));
|
||||||
} else {
|
} else {
|
||||||
// 清空所有配置缓存
|
// 清空所有配置缓存
|
||||||
configCache.clear();
|
configCache.clear();
|
||||||
@@ -148,7 +162,7 @@ export const clearConfigCache = (keys?: string[]) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 在页面加载时异步更新配置(如果有更新的话)
|
// 在页面加载时异步更新配置(如果有更新的话)
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== "undefined") {
|
||||||
// 延迟执行,避免阻塞初始渲染
|
// 延迟执行,避免阻塞初始渲染
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
updateSiteConfig();
|
updateSiteConfig();
|
||||||
|
|||||||
+292
-181
@@ -1,15 +1,27 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from "react";
|
||||||
import { useNavigate, useLocation } from 'react-router-dom';
|
import { useNavigate, useLocation } from "react-router-dom";
|
||||||
import { Button } from "@heroui/button";
|
import { Button } from "@heroui/button";
|
||||||
import { Dropdown, DropdownTrigger, DropdownMenu, DropdownItem } from "@heroui/dropdown";
|
import {
|
||||||
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, useDisclosure } from "@heroui/modal";
|
Dropdown,
|
||||||
|
DropdownTrigger,
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownItem,
|
||||||
|
} from "@heroui/dropdown";
|
||||||
|
import {
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
|
ModalHeader,
|
||||||
|
ModalBody,
|
||||||
|
ModalFooter,
|
||||||
|
useDisclosure,
|
||||||
|
} from "@heroui/modal";
|
||||||
import { Input } from "@heroui/input";
|
import { Input } from "@heroui/input";
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from "react-hot-toast";
|
||||||
|
|
||||||
import { Logo } from '@/components/icons';
|
import { Logo } from "@/components/icons";
|
||||||
import { updatePassword } from '@/api';
|
import { updatePassword } from "@/api";
|
||||||
import { safeLogout } from '@/utils/logout';
|
import { safeLogout } from "@/utils/logout";
|
||||||
import { siteConfig } from '@/config/site';
|
import { siteConfig } from "@/config/site";
|
||||||
|
|
||||||
interface MenuItem {
|
interface MenuItem {
|
||||||
path: string;
|
path: string;
|
||||||
@@ -36,86 +48,106 @@ export default function AdminLayout({
|
|||||||
|
|
||||||
const [isMobile, setIsMobile] = useState(false);
|
const [isMobile, setIsMobile] = useState(false);
|
||||||
const [mobileMenuVisible, setMobileMenuVisible] = useState(false);
|
const [mobileMenuVisible, setMobileMenuVisible] = useState(false);
|
||||||
const [username, setUsername] = useState('');
|
const [username, setUsername] = useState("");
|
||||||
const [isAdmin, setIsAdmin] = useState(false);
|
const [isAdmin, setIsAdmin] = useState(false);
|
||||||
const [passwordLoading, setPasswordLoading] = useState(false);
|
const [passwordLoading, setPasswordLoading] = useState(false);
|
||||||
const [passwordForm, setPasswordForm] = useState<PasswordForm>({
|
const [passwordForm, setPasswordForm] = useState<PasswordForm>({
|
||||||
newUsername: '',
|
newUsername: "",
|
||||||
currentPassword: '',
|
currentPassword: "",
|
||||||
newPassword: '',
|
newPassword: "",
|
||||||
confirmPassword: ''
|
confirmPassword: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
// 菜单项配置
|
// 菜单项配置
|
||||||
const menuItems: MenuItem[] = [
|
const menuItems: MenuItem[] = [
|
||||||
{
|
{
|
||||||
path: '/dashboard',
|
path: "/dashboard",
|
||||||
label: '仪表板',
|
label: "仪表板",
|
||||||
icon: (
|
icon: (
|
||||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||||
<path d="M3 4a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1V4zM3 10a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H4a1 1 0 01-1-1v-6zM14 9a1 1 0 00-1 1v6a1 1 0 001 1h2a1 1 0 001-1v-6a1 1 0 00-1-1h-2z" />
|
<path d="M3 4a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1V4zM3 10a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H4a1 1 0 01-1-1v-6zM14 9a1 1 0 00-1 1v6a1 1 0 001 1h2a1 1 0 001-1v-6a1 1 0 00-1-1h-2z" />
|
||||||
</svg>
|
</svg>
|
||||||
)
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/forward',
|
path: "/forward",
|
||||||
label: '转发管理',
|
label: "转发管理",
|
||||||
icon: (
|
icon: (
|
||||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||||
<path fillRule="evenodd" d="M3 17a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm3.293-7.707a1 1 0 011.414 0L9 10.586V3a1 1 0 112 0v7.586l1.293-1.293a1 1 0 111.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clipRule="evenodd" />
|
<path
|
||||||
</svg>
|
clipRule="evenodd"
|
||||||
)
|
d="M3 17a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm3.293-7.707a1 1 0 011.414 0L9 10.586V3a1 1 0 112 0v7.586l1.293-1.293a1 1 0 111.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z"
|
||||||
},
|
fillRule="evenodd"
|
||||||
{
|
/>
|
||||||
path: '/tunnel',
|
|
||||||
label: '隧道管理',
|
|
||||||
icon: (
|
|
||||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
|
||||||
<path fillRule="evenodd" d="M12.586 4.586a2 2 0 112.828 2.828l-3 3a2 2 0 01-2.828 0 1 1 0 00-1.414 1.414 4 4 0 005.656 0l3-3a4 4 0 00-5.656-5.656l-1.5 1.5a1 1 0 101.414 1.414l1.5-1.5zm-5 5a2 2 0 012.828 0 1 1 0 101.414-1.414 4 4 0 00-5.656 0l-3 3a4 4 0 105.656 5.656l1.5-1.5a1 1 0 10-1.414-1.414l-1.5 1.5a2 2 0 11-2.828-2.828l3-3z" clipRule="evenodd" />
|
|
||||||
</svg>
|
</svg>
|
||||||
),
|
),
|
||||||
adminOnly: true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/node',
|
path: "/tunnel",
|
||||||
label: '节点监控',
|
label: "隧道管理",
|
||||||
icon: (
|
icon: (
|
||||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||||
<path fillRule="evenodd" d="M3 3a1 1 0 000 2v8a2 2 0 002 2h2.586l-1.293 1.293a1 1 0 101.414 1.414L10 15.414l2.293 2.293a1 1 0 001.414-1.414L12.414 15H15a2 2 0 002-2V5a1 1 0 100-2H3zm11.707 4.707a1 1 0 00-1.414-1.414L10 9.586 8.707 8.293a1 1 0 00-1.414 0l-2 2a1 1 0 101.414 1.414L8 10.414l1.293 1.293a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
|
<path
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M12.586 4.586a2 2 0 112.828 2.828l-3 3a2 2 0 01-2.828 0 1 1 0 00-1.414 1.414 4 4 0 005.656 0l3-3a4 4 0 00-5.656-5.656l-1.5 1.5a1 1 0 101.414 1.414l1.5-1.5zm-5 5a2 2 0 012.828 0 1 1 0 101.414-1.414 4 4 0 00-5.656 0l-3 3a4 4 0 105.656 5.656l1.5-1.5a1 1 0 10-1.414-1.414l-1.5 1.5a2 2 0 11-2.828-2.828l3-3z"
|
||||||
|
fillRule="evenodd"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
),
|
),
|
||||||
adminOnly: true
|
adminOnly: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/limit',
|
path: "/node",
|
||||||
label: '限速管理',
|
label: "节点监控",
|
||||||
icon: (
|
icon: (
|
||||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clipRule="evenodd" />
|
<path
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M3 3a1 1 0 000 2v8a2 2 0 002 2h2.586l-1.293 1.293a1 1 0 101.414 1.414L10 15.414l2.293 2.293a1 1 0 001.414-1.414L12.414 15H15a2 2 0 002-2V5a1 1 0 100-2H3zm11.707 4.707a1 1 0 00-1.414-1.414L10 9.586 8.707 8.293a1 1 0 00-1.414 0l-2 2a1 1 0 101.414 1.414L8 10.414l1.293 1.293a1 1 0 001.414 0l4-4z"
|
||||||
|
fillRule="evenodd"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
),
|
),
|
||||||
adminOnly: true
|
adminOnly: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/user',
|
path: "/limit",
|
||||||
label: '用户管理',
|
label: "限速管理",
|
||||||
|
icon: (
|
||||||
|
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z"
|
||||||
|
fillRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
adminOnly: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/user",
|
||||||
|
label: "用户管理",
|
||||||
icon: (
|
icon: (
|
||||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||||
<path d="M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" />
|
<path d="M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" />
|
||||||
</svg>
|
</svg>
|
||||||
),
|
),
|
||||||
adminOnly: true
|
adminOnly: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/config',
|
path: "/config",
|
||||||
label: '网站配置',
|
label: "网站配置",
|
||||||
icon: (
|
icon: (
|
||||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||||
<path fillRule="evenodd" d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z" clipRule="evenodd" />
|
<path
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z"
|
||||||
|
fillRule="evenodd"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
),
|
),
|
||||||
adminOnly: true
|
adminOnly: true,
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// 检查移动端
|
// 检查移动端
|
||||||
@@ -128,33 +160,35 @@ export default function AdminLayout({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 获取用户信息
|
// 获取用户信息
|
||||||
const name = localStorage.getItem('name') || 'Admin';
|
const name = localStorage.getItem("name") || "Admin";
|
||||||
|
|
||||||
// 兼容处理:如果没有admin字段,根据role_id判断(0为管理员)
|
// 兼容处理:如果没有admin字段,根据role_id判断(0为管理员)
|
||||||
let adminFlag = localStorage.getItem('admin') === 'true';
|
let adminFlag = localStorage.getItem("admin") === "true";
|
||||||
if (localStorage.getItem('admin') === null) {
|
|
||||||
const roleId = parseInt(localStorage.getItem('role_id') || '1', 10);
|
if (localStorage.getItem("admin") === null) {
|
||||||
|
const roleId = parseInt(localStorage.getItem("role_id") || "1", 10);
|
||||||
|
|
||||||
adminFlag = roleId === 0;
|
adminFlag = roleId === 0;
|
||||||
// 补充设置admin字段,避免下次再次判断
|
// 补充设置admin字段,避免下次再次判断
|
||||||
localStorage.setItem('admin', adminFlag.toString());
|
localStorage.setItem("admin", adminFlag.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
setUsername(name);
|
setUsername(name);
|
||||||
setIsAdmin(adminFlag);
|
setIsAdmin(adminFlag);
|
||||||
|
|
||||||
// 响应式检查
|
// 响应式检查
|
||||||
checkMobile();
|
checkMobile();
|
||||||
window.addEventListener('resize', checkMobile);
|
window.addEventListener("resize", checkMobile);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('resize', checkMobile);
|
window.removeEventListener("resize", checkMobile);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 退出登录
|
// 退出登录
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
safeLogout();
|
safeLogout();
|
||||||
navigate('/');
|
navigate("/");
|
||||||
};
|
};
|
||||||
|
|
||||||
// 切换移动端菜单
|
// 切换移动端菜单
|
||||||
@@ -178,29 +212,36 @@ export default function AdminLayout({
|
|||||||
// 密码表单验证
|
// 密码表单验证
|
||||||
const validatePasswordForm = (): boolean => {
|
const validatePasswordForm = (): boolean => {
|
||||||
if (!passwordForm.newUsername.trim()) {
|
if (!passwordForm.newUsername.trim()) {
|
||||||
toast.error('请输入新用户名');
|
toast.error("请输入新用户名");
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (passwordForm.newUsername.length < 3) {
|
if (passwordForm.newUsername.length < 3) {
|
||||||
toast.error('用户名长度至少3位');
|
toast.error("用户名长度至少3位");
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!passwordForm.currentPassword) {
|
if (!passwordForm.currentPassword) {
|
||||||
toast.error('请输入当前密码');
|
toast.error("请输入当前密码");
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!passwordForm.newPassword) {
|
if (!passwordForm.newPassword) {
|
||||||
toast.error('请输入新密码');
|
toast.error("请输入新密码");
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (passwordForm.newPassword.length < 6) {
|
if (passwordForm.newPassword.length < 6) {
|
||||||
toast.error('新密码长度不能少于6位');
|
toast.error("新密码长度不能少于6位");
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (passwordForm.newPassword !== passwordForm.confirmPassword) {
|
if (passwordForm.newPassword !== passwordForm.confirmPassword) {
|
||||||
toast.error('两次输入密码不一致');
|
toast.error("两次输入密码不一致");
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -211,16 +252,17 @@ export default function AdminLayout({
|
|||||||
setPasswordLoading(true);
|
setPasswordLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await updatePassword(passwordForm);
|
const response = await updatePassword(passwordForm);
|
||||||
|
|
||||||
if (response.code === 0) {
|
if (response.code === 0) {
|
||||||
toast.success('密码修改成功,请重新登录');
|
toast.success("密码修改成功,请重新登录");
|
||||||
onOpenChange();
|
onOpenChange();
|
||||||
handleLogout();
|
handleLogout();
|
||||||
} else {
|
} else {
|
||||||
toast.error(response.msg || '密码修改失败');
|
toast.error(response.msg || "密码修改失败");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error('修改密码时发生错误');
|
toast.error("修改密码时发生错误");
|
||||||
console.error('修改密码错误:', error);
|
console.error("修改密码错误:", error);
|
||||||
} finally {
|
} finally {
|
||||||
setPasswordLoading(false);
|
setPasswordLoading(false);
|
||||||
}
|
}
|
||||||
@@ -229,92 +271,98 @@ export default function AdminLayout({
|
|||||||
// 重置密码表单
|
// 重置密码表单
|
||||||
const resetPasswordForm = () => {
|
const resetPasswordForm = () => {
|
||||||
setPasswordForm({
|
setPasswordForm({
|
||||||
newUsername: '',
|
newUsername: "",
|
||||||
currentPassword: '',
|
currentPassword: "",
|
||||||
newPassword: '',
|
newPassword: "",
|
||||||
confirmPassword: ''
|
confirmPassword: "",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// 过滤菜单项(根据权限)
|
// 过滤菜单项(根据权限)
|
||||||
const filteredMenuItems = menuItems.filter(item =>
|
const filteredMenuItems = menuItems.filter(
|
||||||
!item.adminOnly || isAdmin
|
(item) => !item.adminOnly || isAdmin,
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`flex ${isMobile ? 'min-h-screen' : 'h-screen'} bg-gray-100 dark:bg-black`}>
|
<div
|
||||||
|
className={`flex ${isMobile ? "min-h-screen" : "h-screen"} bg-gray-100 dark:bg-black`}
|
||||||
|
>
|
||||||
{/* 移动端遮罩层 */}
|
{/* 移动端遮罩层 */}
|
||||||
{isMobile && mobileMenuVisible && (
|
{isMobile && mobileMenuVisible && (
|
||||||
<div
|
<div
|
||||||
className="fixed inset-0 backdrop-blur-sm bg-white/50 dark:bg-black/30 z-40"
|
className="fixed inset-0 backdrop-blur-sm bg-white/50 dark:bg-black/30 z-40"
|
||||||
onClick={hideMobileMenu}
|
onClick={hideMobileMenu}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 左侧菜单栏 */}
|
{/* 左侧菜单栏 */}
|
||||||
<aside className={`
|
<aside
|
||||||
${isMobile ? 'fixed' : 'relative'}
|
className={`
|
||||||
${isMobile && !mobileMenuVisible ? '-translate-x-full' : 'translate-x-0'}
|
${isMobile ? "fixed" : "relative"}
|
||||||
${isMobile ? 'w-64' : 'w-72'}
|
${isMobile && !mobileMenuVisible ? "-translate-x-full" : "translate-x-0"}
|
||||||
|
${isMobile ? "w-64" : "w-72"}
|
||||||
bg-white dark:bg-black
|
bg-white dark:bg-black
|
||||||
shadow-lg
|
shadow-lg
|
||||||
border-r border-gray-200 dark:border-gray-600
|
border-r border-gray-200 dark:border-gray-600
|
||||||
z-50
|
z-50
|
||||||
transition-transform duration-300 ease-in-out
|
transition-transform duration-300 ease-in-out
|
||||||
flex flex-col
|
flex flex-col
|
||||||
${isMobile ? 'h-screen' : 'h-full'}
|
${isMobile ? "h-screen" : "h-full"}
|
||||||
${isMobile ? 'top-0 left-0' : ''}
|
${isMobile ? "top-0 left-0" : ""}
|
||||||
`}>
|
`}
|
||||||
{/* Logo 区域 */}
|
>
|
||||||
<div className="px-3 py-3 h-14 flex items-center">
|
{/* Logo 区域 */}
|
||||||
<div className="flex items-center gap-2 w-full">
|
<div className="px-3 py-3 h-14 flex items-center">
|
||||||
<Logo size={24} />
|
<div className="flex items-center gap-2 w-full">
|
||||||
<div className="flex-1 min-w-0">
|
<Logo size={24} />
|
||||||
<h1 className="text-sm font-bold text-foreground overflow-hidden whitespace-nowrap">{siteConfig.name}</h1>
|
<div className="flex-1 min-w-0">
|
||||||
<p className="text-xs text-default-500">v{siteConfig.version}</p>
|
<h1 className="text-sm font-bold text-foreground overflow-hidden whitespace-nowrap">
|
||||||
</div>
|
{siteConfig.name}
|
||||||
</div>
|
</h1>
|
||||||
</div>
|
<p className="text-xs text-default-500">v{siteConfig.version}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 菜单导航 */}
|
{/* 菜单导航 */}
|
||||||
<nav className="flex-1 px-4 py-6 overflow-y-auto">
|
<nav className="flex-1 px-4 py-6 overflow-y-auto">
|
||||||
<ul className="space-y-1">
|
<ul className="space-y-1">
|
||||||
{filteredMenuItems.map((item) => {
|
{filteredMenuItems.map((item) => {
|
||||||
const isActive = location.pathname === item.path;
|
const isActive = location.pathname === item.path;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li key={item.path}>
|
<li key={item.path}>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleMenuClick(item.path)}
|
className={`
|
||||||
className={`
|
|
||||||
w-full flex items-center gap-3 px-4 py-3 rounded-lg text-left
|
w-full flex items-center gap-3 px-4 py-3 rounded-lg text-left
|
||||||
transition-colors duration-200 min-h-[44px]
|
transition-colors duration-200 min-h-[44px]
|
||||||
${isActive
|
${
|
||||||
? 'bg-primary-100 dark:bg-primary-600/20 text-primary-600 dark:text-primary-300'
|
isActive
|
||||||
: 'text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-900'
|
? "bg-primary-100 dark:bg-primary-600/20 text-primary-600 dark:text-primary-300"
|
||||||
|
: "text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-900"
|
||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
>
|
onClick={() => handleMenuClick(item.path)}
|
||||||
<div className="flex-shrink-0">
|
>
|
||||||
{item.icon}
|
<div className="flex-shrink-0">{item.icon}</div>
|
||||||
</div>
|
<span className="font-medium text-sm">{item.label}</span>
|
||||||
<span className="font-medium text-sm">{item.label}</span>
|
</button>
|
||||||
</button>
|
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{/* 底部版权信息 */}
|
{/* 底部版权信息 */}
|
||||||
<div className="px-4 py-2 pb-4 mt-auto flex-shrink-0">
|
<div className="px-4 py-2 pb-4 mt-auto flex-shrink-0">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p className="text-xs text-gray-400 dark:text-gray-500">
|
<p className="text-xs text-gray-400 dark:text-gray-500">
|
||||||
Powered by{' '}
|
Powered by{" "}
|
||||||
<a
|
<a
|
||||||
href={siteConfig.github_repo}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="text-gray-500 dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
|
className="text-gray-500 dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
|
||||||
|
href={siteConfig.github_repo}
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
>
|
>
|
||||||
flux-panel
|
flux-panel
|
||||||
</a>
|
</a>
|
||||||
@@ -324,20 +372,32 @@ export default function AdminLayout({
|
|||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
{/* 主内容区域 */}
|
{/* 主内容区域 */}
|
||||||
<div className={`flex flex-col flex-1 ${isMobile ? 'min-h-0' : 'h-full overflow-hidden'}`}>
|
<div
|
||||||
{/* 顶部导航栏 */}
|
className={`flex flex-col flex-1 ${isMobile ? "min-h-0" : "h-full overflow-hidden"}`}
|
||||||
<header className="bg-white dark:bg-black shadow-md border-b border-gray-200 dark:border-gray-600 h-14 flex items-center justify-between px-4 lg:px-6 relative z-10">
|
>
|
||||||
|
{/* 顶部导航栏 */}
|
||||||
|
<header className="bg-white dark:bg-black shadow-md border-b border-gray-200 dark:border-gray-600 h-14 flex items-center justify-between px-4 lg:px-6 relative z-10">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
{/* 移动端菜单按钮 */}
|
{/* 移动端菜单按钮 */}
|
||||||
{isMobile && (
|
{isMobile && (
|
||||||
<Button
|
<Button
|
||||||
isIconOnly
|
isIconOnly
|
||||||
|
className="lg:hidden"
|
||||||
variant="light"
|
variant="light"
|
||||||
onPress={toggleMobileMenu}
|
onPress={toggleMobileMenu}
|
||||||
className="lg:hidden"
|
|
||||||
>
|
>
|
||||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
className="w-6 h-6"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M4 6h16M4 12h16M4 18h16"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
@@ -345,21 +405,40 @@ export default function AdminLayout({
|
|||||||
|
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
{/* 用户菜单 */}
|
{/* 用户菜单 */}
|
||||||
<Dropdown placement="bottom-end">
|
<Dropdown placement="bottom-end">
|
||||||
<DropdownTrigger>
|
<DropdownTrigger>
|
||||||
<Button variant="light" className="text-sm font-medium text-foreground">
|
<Button
|
||||||
{username}
|
className="text-sm font-medium text-foreground"
|
||||||
<svg className="w-4 h-4 ml-1" fill="currentColor" viewBox="0 0 20 20">
|
variant="light"
|
||||||
<path fillRule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clipRule="evenodd" />
|
>
|
||||||
</svg>
|
{username}
|
||||||
</Button>
|
<svg
|
||||||
</DropdownTrigger>
|
className="w-4 h-4 ml-1"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
|
||||||
|
fillRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</Button>
|
||||||
|
</DropdownTrigger>
|
||||||
<DropdownMenu aria-label="用户菜单">
|
<DropdownMenu aria-label="用户菜单">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
key="change-password"
|
key="change-password"
|
||||||
startContent={
|
startContent={
|
||||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
<svg
|
||||||
<path fillRule="evenodd" d="M18 8a6 6 0 01-7.743 5.743L10 14l-1 1-1 1H6v2H2v-4l4.257-4.257A6 6 0 1118 8zm-6-4a1 1 0 100 2 2 2 0 012 2 1 1 0 102 0 4 4 0 00-4-4z" clipRule="evenodd" />
|
className="w-4 h-4"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M18 8a6 6 0 01-7.743 5.743L10 14l-1 1-1 1H6v2H2v-4l4.257-4.257A6 6 0 1118 8zm-6-4a1 1 0 100 2 2 2 0 012 2 1 1 0 102 0 4 4 0 00-4-4z"
|
||||||
|
fillRule="evenodd"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
}
|
}
|
||||||
onPress={onOpen}
|
onPress={onOpen}
|
||||||
@@ -368,13 +447,21 @@ export default function AdminLayout({
|
|||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
key="logout"
|
key="logout"
|
||||||
startContent={
|
|
||||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
|
||||||
<path fillRule="evenodd" d="M3 3a1 1 0 00-1 1v12a1 1 0 102 0V4a1 1 0 00-1-1zm10.293 9.293a1 1 0 001.414 1.414l3-3a1 1 0 000-1.414l-3-3a1 1 0 10-1.414 1.414L14.586 9H7a1 1 0 100 2h7.586l-1.293 1.293z" clipRule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
}
|
|
||||||
className="text-danger"
|
className="text-danger"
|
||||||
color="danger"
|
color="danger"
|
||||||
|
startContent={
|
||||||
|
<svg
|
||||||
|
className="w-4 h-4"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M3 3a1 1 0 00-1 1v12a1 1 0 102 0V4a1 1 0 00-1-1zm10.293 9.293a1 1 0 001.414 1.414l3-3a1 1 0 000-1.414l-3-3a1 1 0 10-1.414 1.414L14.586 9H7a1 1 0 100 2h7.586l-1.293 1.293z"
|
||||||
|
fillRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
onPress={handleLogout}
|
onPress={handleLogout}
|
||||||
>
|
>
|
||||||
退出登录
|
退出登录
|
||||||
@@ -385,70 +472,94 @@ export default function AdminLayout({
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/* 主内容 */}
|
{/* 主内容 */}
|
||||||
<main className={`flex-1 bg-gray-100 dark:bg-black ${isMobile ? '' : 'overflow-y-auto'}`}>
|
<main
|
||||||
|
className={`flex-1 bg-gray-100 dark:bg-black ${isMobile ? "" : "overflow-y-auto"}`}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 修改密码弹窗 */}
|
{/* 修改密码弹窗 */}
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={isOpen}
|
backdrop="blur"
|
||||||
|
isOpen={isOpen}
|
||||||
|
placement="center"
|
||||||
|
scrollBehavior="outside"
|
||||||
|
size="2xl"
|
||||||
onOpenChange={() => {
|
onOpenChange={() => {
|
||||||
onOpenChange();
|
onOpenChange();
|
||||||
resetPasswordForm();
|
resetPasswordForm();
|
||||||
}}
|
}}
|
||||||
size="2xl"
|
|
||||||
scrollBehavior="outside"
|
|
||||||
backdrop="blur"
|
|
||||||
placement="center"
|
|
||||||
>
|
>
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
{(onClose: () => void) => (
|
{(onClose: () => void) => (
|
||||||
<>
|
<>
|
||||||
<ModalHeader className="flex flex-col gap-1">修改密码</ModalHeader>
|
<ModalHeader className="flex flex-col gap-1">
|
||||||
|
修改密码
|
||||||
|
</ModalHeader>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Input
|
<Input
|
||||||
label="新用户名"
|
label="新用户名"
|
||||||
placeholder="请输入新用户名(至少3位)"
|
placeholder="请输入新用户名(至少3位)"
|
||||||
value={passwordForm.newUsername}
|
value={passwordForm.newUsername}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setPasswordForm(prev => ({ ...prev, newUsername: e.target.value }))}
|
variant="bordered"
|
||||||
variant="bordered"
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
/>
|
setPasswordForm((prev) => ({
|
||||||
<Input
|
...prev,
|
||||||
label="当前密码"
|
newUsername: e.target.value,
|
||||||
type="password"
|
}))
|
||||||
placeholder="请输入当前密码"
|
}
|
||||||
value={passwordForm.currentPassword}
|
/>
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setPasswordForm(prev => ({ ...prev, currentPassword: e.target.value }))}
|
<Input
|
||||||
variant="bordered"
|
label="当前密码"
|
||||||
/>
|
placeholder="请输入当前密码"
|
||||||
<Input
|
type="password"
|
||||||
label="新密码"
|
value={passwordForm.currentPassword}
|
||||||
type="password"
|
variant="bordered"
|
||||||
placeholder="请输入新密码(至少6位)"
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
value={passwordForm.newPassword}
|
setPasswordForm((prev) => ({
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setPasswordForm(prev => ({ ...prev, newPassword: e.target.value }))}
|
...prev,
|
||||||
variant="bordered"
|
currentPassword: e.target.value,
|
||||||
/>
|
}))
|
||||||
<Input
|
}
|
||||||
label="确认密码"
|
/>
|
||||||
type="password"
|
<Input
|
||||||
placeholder="请再次输入新密码"
|
label="新密码"
|
||||||
value={passwordForm.confirmPassword}
|
placeholder="请输入新密码(至少6位)"
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setPasswordForm(prev => ({ ...prev, confirmPassword: e.target.value }))}
|
type="password"
|
||||||
variant="bordered"
|
value={passwordForm.newPassword}
|
||||||
/>
|
variant="bordered"
|
||||||
</div>
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
setPasswordForm((prev) => ({
|
||||||
|
...prev,
|
||||||
|
newPassword: e.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="确认密码"
|
||||||
|
placeholder="请再次输入新密码"
|
||||||
|
type="password"
|
||||||
|
value={passwordForm.confirmPassword}
|
||||||
|
variant="bordered"
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
setPasswordForm((prev) => ({
|
||||||
|
...prev,
|
||||||
|
confirmPassword: e.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button color="default" variant="light" onPress={onClose}>
|
<Button color="default" variant="light" onPress={onClose}>
|
||||||
取消
|
取消
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
color="primary"
|
color="primary"
|
||||||
onPress={handlePasswordSubmit}
|
|
||||||
isLoading={passwordLoading}
|
isLoading={passwordLoading}
|
||||||
|
onPress={handlePasswordSubmit}
|
||||||
>
|
>
|
||||||
确定
|
确定
|
||||||
</Button>
|
</Button>
|
||||||
@@ -459,4 +570,4 @@ export default function AdminLayout({
|
|||||||
</Modal>
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
import { useNavigate, useLocation } from 'react-router-dom';
|
import { useNavigate, useLocation } from "react-router-dom";
|
||||||
import { Button } from "@heroui/button";
|
import { Button } from "@heroui/button";
|
||||||
|
|
||||||
import { Logo } from '@/components/icons';
|
import { Logo } from "@/components/icons";
|
||||||
import { siteConfig } from '@/config/site';
|
import { siteConfig } from "@/config/site";
|
||||||
|
|
||||||
export default function H5SimpleLayout({
|
export default function H5SimpleLayout({
|
||||||
children,
|
children,
|
||||||
@@ -12,11 +12,11 @@ export default function H5SimpleLayout({
|
|||||||
}) {
|
}) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
// 路由切换时回到顶部,避免上一页滚动位置保留
|
// 路由切换时回到顶部,避免上一页滚动位置保留
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
try {
|
try {
|
||||||
window.scrollTo({ top: 0, left: 0, behavior: 'auto' });
|
window.scrollTo({ top: 0, left: 0, behavior: "auto" });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
}
|
}
|
||||||
@@ -25,7 +25,7 @@ export default function H5SimpleLayout({
|
|||||||
}, [location.pathname]);
|
}, [location.pathname]);
|
||||||
|
|
||||||
const handleBack = () => {
|
const handleBack = () => {
|
||||||
navigate('/profile');
|
navigate("/profile");
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -33,28 +33,26 @@ export default function H5SimpleLayout({
|
|||||||
{/* 顶部导航栏 */}
|
{/* 顶部导航栏 */}
|
||||||
<header className="bg-white dark:bg-black shadow-sm border-b border-gray-200 dark:border-gray-600 h-14 safe-top flex-shrink-0 flex items-center justify-between px-4 relative z-10">
|
<header className="bg-white dark:bg-black shadow-sm border-b border-gray-200 dark:border-gray-600 h-14 safe-top flex-shrink-0 flex items-center justify-between px-4 relative z-10">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Button
|
<Button isIconOnly size="sm" variant="light" onPress={handleBack}>
|
||||||
isIconOnly
|
|
||||||
variant="light"
|
|
||||||
size="sm"
|
|
||||||
onPress={handleBack}
|
|
||||||
>
|
|
||||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||||
<path fillRule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clipRule="evenodd" />
|
<path
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z"
|
||||||
|
fillRule="evenodd"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</Button>
|
</Button>
|
||||||
<Logo size={20} />
|
<Logo size={20} />
|
||||||
<h1 className="text-sm font-bold text-foreground">{siteConfig.name}</h1>
|
<h1 className="text-sm font-bold text-foreground">
|
||||||
|
{siteConfig.name}
|
||||||
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2" />
|
||||||
</div>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/* 主内容区域 */}
|
{/* 主内容区域 */}
|
||||||
<main className="flex-1 bg-gray-100 dark:bg-black pb-0">
|
<main className="flex-1 bg-gray-100 dark:bg-black pb-0">{children}</main>
|
||||||
{children}
|
|
||||||
</main>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from "react";
|
||||||
import { useNavigate, useLocation } from 'react-router-dom';
|
import { useNavigate, useLocation } from "react-router-dom";
|
||||||
|
|
||||||
import { Logo } from '@/components/icons';
|
import { Logo } from "@/components/icons";
|
||||||
import { siteConfig } from '@/config/site';
|
import { siteConfig } from "@/config/site";
|
||||||
|
|
||||||
interface TabItem {
|
interface TabItem {
|
||||||
path: string;
|
path: string;
|
||||||
@@ -11,13 +11,7 @@ interface TabItem {
|
|||||||
adminOnly?: boolean;
|
adminOnly?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default function H5Layout({ children }: { children: React.ReactNode }) {
|
||||||
|
|
||||||
export default function H5Layout({
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode;
|
|
||||||
}) {
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [isAdmin, setIsAdmin] = useState(false);
|
const [isAdmin, setIsAdmin] = useState(false);
|
||||||
@@ -25,65 +19,77 @@ export default function H5Layout({
|
|||||||
// Tabbar配置
|
// Tabbar配置
|
||||||
const tabItems: TabItem[] = [
|
const tabItems: TabItem[] = [
|
||||||
{
|
{
|
||||||
path: '/dashboard',
|
path: "/dashboard",
|
||||||
label: '首页',
|
label: "首页",
|
||||||
icon: (
|
icon: (
|
||||||
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 20 20">
|
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 20 20">
|
||||||
<path d="M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" />
|
<path d="M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" />
|
||||||
</svg>
|
</svg>
|
||||||
)
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/forward',
|
path: "/forward",
|
||||||
label: '转发',
|
label: "转发",
|
||||||
icon: (
|
icon: (
|
||||||
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 20 20">
|
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 20 20">
|
||||||
<path fillRule="evenodd" d="M3 17a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm3.293-7.707a1 1 0 011.414 0L9 10.586V3a1 1 0 112 0v7.586l1.293-1.293a1 1 0 111.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clipRule="evenodd" />
|
<path
|
||||||
</svg>
|
clipRule="evenodd"
|
||||||
)
|
d="M3 17a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm3.293-7.707a1 1 0 011.414 0L9 10.586V3a1 1 0 112 0v7.586l1.293-1.293a1 1 0 111.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z"
|
||||||
},
|
fillRule="evenodd"
|
||||||
{
|
/>
|
||||||
path: '/tunnel',
|
|
||||||
label: '隧道',
|
|
||||||
icon: (
|
|
||||||
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 20 20">
|
|
||||||
<path fillRule="evenodd" d="M12.586 4.586a2 2 0 112.828 2.828l-3 3a2 2 0 01-2.828 0 1 1 0 00-1.414 1.414 4 4 0 005.656 0l3-3a4 4 0 00-5.656-5.656l-1.5 1.5a1 1 0 101.414 1.414l1.5-1.5zm-5 5a2 2 0 012.828 0 1 1 0 101.414-1.414 4 4 0 00-5.656 0l-3 3a4 4 0 105.656 5.656l1.5-1.5a1 1 0 10-1.414-1.414l-1.5 1.5a2 2 0 11-2.828-2.828l3-3z" clipRule="evenodd" />
|
|
||||||
</svg>
|
</svg>
|
||||||
),
|
),
|
||||||
adminOnly: true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/node',
|
path: "/tunnel",
|
||||||
label: '节点',
|
label: "隧道",
|
||||||
icon: (
|
icon: (
|
||||||
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 20 20">
|
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 20 20">
|
||||||
<path fillRule="evenodd" d="M3 3a1 1 0 000 2v8a2 2 0 002 2h2.586l-1.293 1.293a1 1 0 101.414 1.414L10 15.414l2.293 2.293a1 1 0 001.414-1.414L12.414 15H15a2 2 0 002-2V5a1 1 0 100-2H3zm11.707 4.707a1 1 0 00-1.414-1.414L10 9.586 8.707 8.293a1 1 0 00-1.414 0l-2 2a1 1 0 101.414 1.414L8 10.414l1.293 1.293a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
|
<path
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M12.586 4.586a2 2 0 112.828 2.828l-3 3a2 2 0 01-2.828 0 1 1 0 00-1.414 1.414 4 4 0 005.656 0l3-3a4 4 0 00-5.656-5.656l-1.5 1.5a1 1 0 101.414 1.414l1.5-1.5zm-5 5a2 2 0 012.828 0 1 1 0 101.414-1.414 4 4 0 00-5.656 0l-3 3a4 4 0 105.656 5.656l1.5-1.5a1 1 0 10-1.414-1.414l-1.5 1.5a2 2 0 11-2.828-2.828l3-3z"
|
||||||
|
fillRule="evenodd"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
),
|
),
|
||||||
adminOnly: true
|
adminOnly: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/profile',
|
path: "/node",
|
||||||
label: '我的',
|
label: "节点",
|
||||||
|
icon: (
|
||||||
|
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M3 3a1 1 0 000 2v8a2 2 0 002 2h2.586l-1.293 1.293a1 1 0 101.414 1.414L10 15.414l2.293 2.293a1 1 0 001.414-1.414L12.414 15H15a2 2 0 002-2V5a1 1 0 100-2H3zm11.707 4.707a1 1 0 00-1.414-1.414L10 9.586 8.707 8.293a1 1 0 00-1.414 0l-2 2a1 1 0 101.414 1.414L8 10.414l1.293 1.293a1 1 0 001.414 0l4-4z"
|
||||||
|
fillRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
adminOnly: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/profile",
|
||||||
|
label: "我的",
|
||||||
icon: (
|
icon: (
|
||||||
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 20 20">
|
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 20 20">
|
||||||
<path d="M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" />
|
<path d="M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" />
|
||||||
</svg>
|
</svg>
|
||||||
)
|
),
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
||||||
// 兼容处理:如果没有admin字段,根据role_id判断(0为管理员)
|
// 兼容处理:如果没有admin字段,根据role_id判断(0为管理员)
|
||||||
let adminFlag = localStorage.getItem('admin') === 'true';
|
let adminFlag = localStorage.getItem("admin") === "true";
|
||||||
if (localStorage.getItem('admin') === null) {
|
|
||||||
const roleId = parseInt(localStorage.getItem('role_id') || '1', 10);
|
if (localStorage.getItem("admin") === null) {
|
||||||
|
const roleId = parseInt(localStorage.getItem("role_id") || "1", 10);
|
||||||
|
|
||||||
adminFlag = roleId === 0;
|
adminFlag = roleId === 0;
|
||||||
// 补充设置admin字段,避免下次再次判断
|
// 补充设置admin字段,避免下次再次判断
|
||||||
localStorage.setItem('admin', adminFlag.toString());
|
localStorage.setItem("admin", adminFlag.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
setIsAdmin(adminFlag);
|
setIsAdmin(adminFlag);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -94,14 +100,14 @@ export default function H5Layout({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 过滤tab项(根据权限)
|
// 过滤tab项(根据权限)
|
||||||
const filteredTabItems = tabItems.filter(item =>
|
const filteredTabItems = tabItems.filter(
|
||||||
!item.adminOnly || isAdmin
|
(item) => !item.adminOnly || isAdmin,
|
||||||
);
|
);
|
||||||
|
|
||||||
// 路由切换时回到页面顶部,避免上一页的滚动位置遗留
|
// 路由切换时回到页面顶部,避免上一页的滚动位置遗留
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
try {
|
try {
|
||||||
window.scrollTo({ top: 0, left: 0, behavior: 'auto' });
|
window.scrollTo({ top: 0, left: 0, behavior: "auto" });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
}
|
}
|
||||||
@@ -115,17 +121,16 @@ export default function H5Layout({
|
|||||||
<header className="bg-white dark:bg-black shadow-sm border-b border-gray-200 dark:border-gray-600 h-14 safe-top flex-shrink-0 flex items-center justify-between px-4 relative z-10">
|
<header className="bg-white dark:bg-black shadow-sm border-b border-gray-200 dark:border-gray-600 h-14 safe-top flex-shrink-0 flex items-center justify-between px-4 relative z-10">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Logo size={20} />
|
<Logo size={20} />
|
||||||
<h1 className="text-sm font-bold text-foreground">{siteConfig.name}</h1>
|
<h1 className="text-sm font-bold text-foreground">
|
||||||
|
{siteConfig.name}
|
||||||
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2" />
|
||||||
</div>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/* 主内容区域 */}
|
{/* 主内容区域 */}
|
||||||
<main className="flex-1 bg-gray-100 dark:bg-black">
|
<main className="flex-1 bg-gray-100 dark:bg-black">{children}</main>
|
||||||
{children}
|
|
||||||
</main>
|
|
||||||
|
|
||||||
{/* 用于给固定 Tabbar 腾出空间的占位元素 */}
|
{/* 用于给固定 Tabbar 腾出空间的占位元素 */}
|
||||||
<div aria-hidden className="h-16 safe-bottom" />
|
<div aria-hidden className="h-16 safe-bottom" />
|
||||||
@@ -134,28 +139,27 @@ export default function H5Layout({
|
|||||||
<nav className="bg-white dark:bg-black border-t border-gray-200 dark:border-gray-600 h-16 safe-bottom flex-shrink-0 flex items-center justify-around px-2 fixed bottom-0 left-0 right-0 z-30">
|
<nav className="bg-white dark:bg-black border-t border-gray-200 dark:border-gray-600 h-16 safe-bottom flex-shrink-0 flex items-center justify-around px-2 fixed bottom-0 left-0 right-0 z-30">
|
||||||
{filteredTabItems.map((item) => {
|
{filteredTabItems.map((item) => {
|
||||||
const isActive = location.pathname === item.path;
|
const isActive = location.pathname === item.path;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={item.path}
|
key={item.path}
|
||||||
onClick={() => handleTabClick(item.path)}
|
|
||||||
className={`
|
className={`
|
||||||
flex flex-col items-center justify-center flex-1 h-full
|
flex flex-col items-center justify-center flex-1 h-full
|
||||||
transition-colors duration-200 min-h-[44px]
|
transition-colors duration-200 min-h-[44px]
|
||||||
${isActive
|
${
|
||||||
? 'text-primary-600 dark:text-primary-400'
|
isActive
|
||||||
: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200'
|
? "text-primary-600 dark:text-primary-400"
|
||||||
|
: "text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200"
|
||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
|
onClick={() => handleTabClick(item.path)}
|
||||||
>
|
>
|
||||||
<div className="flex-shrink-0 mb-1">
|
<div className="flex-shrink-0 mb-1">{item.icon}</div>
|
||||||
{item.icon}
|
|
||||||
</div>
|
|
||||||
<span className="text-xs font-medium">{item.label}</span>
|
<span className="text-xs font-medium">{item.label}</span>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import ReactDOM from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
import { BrowserRouter } from "react-router-dom";
|
import { BrowserRouter } from "react-router-dom";
|
||||||
|
|
||||||
@@ -11,6 +10,5 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
|
|||||||
<Provider>
|
<Provider>
|
||||||
<App />
|
<App />
|
||||||
</Provider>
|
</Provider>
|
||||||
</BrowserRouter>
|
</BrowserRouter>,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Input } from "@heroui/input";
|
|||||||
import { Card, CardBody, CardHeader } from "@heroui/card";
|
import { Card, CardBody, CardHeader } from "@heroui/card";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import toast from 'react-hot-toast';
|
import toast from "react-hot-toast";
|
||||||
|
|
||||||
import { title } from "@/components/primitives";
|
import { title } from "@/components/primitives";
|
||||||
import { updatePassword } from "@/api";
|
import { updatePassword } from "@/api";
|
||||||
@@ -19,10 +19,10 @@ interface PasswordForm {
|
|||||||
|
|
||||||
export default function ChangePasswordPage() {
|
export default function ChangePasswordPage() {
|
||||||
const [form, setForm] = useState<PasswordForm>({
|
const [form, setForm] = useState<PasswordForm>({
|
||||||
newUsername: '',
|
newUsername: "",
|
||||||
currentPassword: '',
|
currentPassword: "",
|
||||||
newPassword: '',
|
newPassword: "",
|
||||||
confirmPassword: ''
|
confirmPassword: "",
|
||||||
});
|
});
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [errors, setErrors] = useState<Partial<PasswordForm>>({});
|
const [errors, setErrors] = useState<Partial<PasswordForm>>({});
|
||||||
@@ -32,39 +32,40 @@ export default function ChangePasswordPage() {
|
|||||||
const newErrors: Partial<PasswordForm> = {};
|
const newErrors: Partial<PasswordForm> = {};
|
||||||
|
|
||||||
if (!form.newUsername.trim()) {
|
if (!form.newUsername.trim()) {
|
||||||
newErrors.newUsername = '请输入新用户名';
|
newErrors.newUsername = "请输入新用户名";
|
||||||
} else if (form.newUsername.length < 3) {
|
} else if (form.newUsername.length < 3) {
|
||||||
newErrors.newUsername = '用户名长度至少3位';
|
newErrors.newUsername = "用户名长度至少3位";
|
||||||
} else if (form.newUsername.length > 20) {
|
} else if (form.newUsername.length > 20) {
|
||||||
newErrors.newUsername = '用户名长度不能超过20位';
|
newErrors.newUsername = "用户名长度不能超过20位";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!form.currentPassword.trim()) {
|
if (!form.currentPassword.trim()) {
|
||||||
newErrors.currentPassword = '请输入当前密码';
|
newErrors.currentPassword = "请输入当前密码";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!form.newPassword.trim()) {
|
if (!form.newPassword.trim()) {
|
||||||
newErrors.newPassword = '请输入新密码';
|
newErrors.newPassword = "请输入新密码";
|
||||||
} else if (form.newPassword.length < 6) {
|
} else if (form.newPassword.length < 6) {
|
||||||
newErrors.newPassword = '新密码长度不能少于6位';
|
newErrors.newPassword = "新密码长度不能少于6位";
|
||||||
} else if (form.newPassword.length > 20) {
|
} else if (form.newPassword.length > 20) {
|
||||||
newErrors.newPassword = '新密码长度不能超过20位';
|
newErrors.newPassword = "新密码长度不能超过20位";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!form.confirmPassword.trim()) {
|
if (!form.confirmPassword.trim()) {
|
||||||
newErrors.confirmPassword = '请再次输入新密码';
|
newErrors.confirmPassword = "请再次输入新密码";
|
||||||
} else if (form.confirmPassword !== form.newPassword) {
|
} else if (form.confirmPassword !== form.newPassword) {
|
||||||
newErrors.confirmPassword = '两次输入密码不一致';
|
newErrors.confirmPassword = "两次输入密码不一致";
|
||||||
}
|
}
|
||||||
|
|
||||||
setErrors(newErrors);
|
setErrors(newErrors);
|
||||||
|
|
||||||
return Object.keys(newErrors).length === 0;
|
return Object.keys(newErrors).length === 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleInputChange = (field: keyof PasswordForm, value: string) => {
|
const handleInputChange = (field: keyof PasswordForm, value: string) => {
|
||||||
setForm(prev => ({ ...prev, [field]: value }));
|
setForm((prev) => ({ ...prev, [field]: value }));
|
||||||
if (errors[field]) {
|
if (errors[field]) {
|
||||||
setErrors(prev => ({ ...prev, [field]: undefined }));
|
setErrors((prev) => ({ ...prev, [field]: undefined }));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -74,23 +75,23 @@ export default function ChangePasswordPage() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await updatePassword(form);
|
const response = await updatePassword(form);
|
||||||
|
|
||||||
if (response.code === 0) {
|
if (response.code === 0) {
|
||||||
toast.success(response.msg || '账号密码修改成功');
|
toast.success(response.msg || "账号密码修改成功");
|
||||||
|
|
||||||
// 使用 toast 确认对话框的替代方案
|
// 使用 toast 确认对话框的替代方案
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
toast.success('即将跳转到登陆页面,请重新登录');
|
toast.success("即将跳转到登陆页面,请重新登录");
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
logout();
|
logout();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
} else {
|
} else {
|
||||||
toast.error(response.msg || '账号密码修改失败');
|
toast.error(response.msg || "账号密码修改失败");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('修改账号密码错误:', error);
|
console.error("修改账号密码错误:", error);
|
||||||
toast.error('修改账号密码时发生错误');
|
toast.error("修改账号密码时发生错误");
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -98,11 +99,11 @@ export default function ChangePasswordPage() {
|
|||||||
|
|
||||||
const logout = () => {
|
const logout = () => {
|
||||||
safeLogout();
|
safeLogout();
|
||||||
navigate('/');
|
navigate("/");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleKeyPress = (e: React.KeyboardEvent) => {
|
const handleKeyPress = (e: React.KeyboardEvent) => {
|
||||||
if (e.key === 'Enter' && !loading) {
|
if (e.key === "Enter" && !loading) {
|
||||||
handleSubmit();
|
handleSubmit();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -114,79 +115,97 @@ export default function ChangePasswordPage() {
|
|||||||
<Card className="w-full">
|
<Card className="w-full">
|
||||||
<CardHeader className="pb-0 pt-6 px-6 flex-col items-center">
|
<CardHeader className="pb-0 pt-6 px-6 flex-col items-center">
|
||||||
<div className="w-12 h-12 bg-warning-100 rounded-full flex items-center justify-center mb-3">
|
<div className="w-12 h-12 bg-warning-100 rounded-full flex items-center justify-center mb-3">
|
||||||
<svg className="w-6 h-6 text-warning-600" fill="currentColor" viewBox="0 0 20 20">
|
<svg
|
||||||
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
|
className="w-6 h-6 text-warning-600"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z"
|
||||||
|
fillRule="evenodd"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1 className={title({ size: "sm" })}>安全提醒</h1>
|
<h1 className={title({ size: "sm" })}>安全提醒</h1>
|
||||||
<p className="text-small text-default-500 mt-2 text-center">检测到您使用的是默认账号密码,为了您的账户安全,请立即修改</p>
|
<p className="text-small text-default-500 mt-2 text-center">
|
||||||
|
检测到您使用的是默认账号密码,为了您的账户安全,请立即修改
|
||||||
|
</p>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<CardBody className="px-6 py-6">
|
<CardBody className="px-6 py-6">
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<Input
|
<Input
|
||||||
|
errorMessage={errors.newUsername}
|
||||||
|
isDisabled={loading}
|
||||||
|
isInvalid={!!errors.newUsername}
|
||||||
label="新用户名"
|
label="新用户名"
|
||||||
placeholder="请输入新用户名(至少3位)"
|
placeholder="请输入新用户名(至少3位)"
|
||||||
value={form.newUsername}
|
value={form.newUsername}
|
||||||
onChange={(e) => handleInputChange('newUsername', e.target.value)}
|
|
||||||
onKeyDown={handleKeyPress}
|
|
||||||
variant="bordered"
|
variant="bordered"
|
||||||
isDisabled={loading}
|
onChange={(e) =>
|
||||||
isInvalid={!!errors.newUsername}
|
handleInputChange("newUsername", e.target.value)
|
||||||
errorMessage={errors.newUsername}
|
}
|
||||||
|
onKeyDown={handleKeyPress}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
|
errorMessage={errors.currentPassword}
|
||||||
|
isDisabled={loading}
|
||||||
|
isInvalid={!!errors.currentPassword}
|
||||||
label="当前密码"
|
label="当前密码"
|
||||||
placeholder="请输入当前密码"
|
placeholder="请输入当前密码"
|
||||||
type="password"
|
type="password"
|
||||||
value={form.currentPassword}
|
value={form.currentPassword}
|
||||||
onChange={(e) => handleInputChange('currentPassword', e.target.value)}
|
|
||||||
onKeyDown={handleKeyPress}
|
|
||||||
variant="bordered"
|
variant="bordered"
|
||||||
isDisabled={loading}
|
onChange={(e) =>
|
||||||
isInvalid={!!errors.currentPassword}
|
handleInputChange("currentPassword", e.target.value)
|
||||||
errorMessage={errors.currentPassword}
|
}
|
||||||
|
onKeyDown={handleKeyPress}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
|
errorMessage={errors.newPassword}
|
||||||
|
isDisabled={loading}
|
||||||
|
isInvalid={!!errors.newPassword}
|
||||||
label="新密码"
|
label="新密码"
|
||||||
placeholder="请输入新密码(至少6位)"
|
placeholder="请输入新密码(至少6位)"
|
||||||
type="password"
|
type="password"
|
||||||
value={form.newPassword}
|
value={form.newPassword}
|
||||||
onChange={(e) => handleInputChange('newPassword', e.target.value)}
|
|
||||||
onKeyDown={handleKeyPress}
|
|
||||||
variant="bordered"
|
variant="bordered"
|
||||||
isDisabled={loading}
|
onChange={(e) =>
|
||||||
isInvalid={!!errors.newPassword}
|
handleInputChange("newPassword", e.target.value)
|
||||||
errorMessage={errors.newPassword}
|
}
|
||||||
|
onKeyDown={handleKeyPress}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
|
errorMessage={errors.confirmPassword}
|
||||||
|
isDisabled={loading}
|
||||||
|
isInvalid={!!errors.confirmPassword}
|
||||||
label="确认新密码"
|
label="确认新密码"
|
||||||
placeholder="请再次输入新密码"
|
placeholder="请再次输入新密码"
|
||||||
type="password"
|
type="password"
|
||||||
value={form.confirmPassword}
|
value={form.confirmPassword}
|
||||||
onChange={(e) => handleInputChange('confirmPassword', e.target.value)}
|
|
||||||
onKeyDown={handleKeyPress}
|
|
||||||
variant="bordered"
|
variant="bordered"
|
||||||
isDisabled={loading}
|
onChange={(e) =>
|
||||||
isInvalid={!!errors.confirmPassword}
|
handleInputChange("confirmPassword", e.target.value)
|
||||||
errorMessage={errors.confirmPassword}
|
}
|
||||||
|
onKeyDown={handleKeyPress}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
className="mt-2"
|
||||||
color="warning"
|
color="warning"
|
||||||
|
disabled={loading}
|
||||||
|
isLoading={loading}
|
||||||
size="lg"
|
size="lg"
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
isLoading={loading}
|
|
||||||
disabled={loading}
|
|
||||||
className="mt-2"
|
|
||||||
>
|
>
|
||||||
{loading ? "修改中..." : "立即修改账号密码"}
|
{loading ? "修改中..." : "立即修改账号密码"}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<div className="bg-warning-50 border border-warning-200 text-warning-700 px-3 py-2 rounded-lg text-sm text-center">
|
<div className="bg-warning-50 border border-warning-200 text-warning-700 px-3 py-2 rounded-lg text-sm text-center">
|
||||||
⚠️ 注意:修改账号密码后需要重新登录
|
⚠️ 注意:修改账号密码后需要重新登录
|
||||||
</div>
|
</div>
|
||||||
@@ -197,4 +216,4 @@ export default function ChangePasswordPage() {
|
|||||||
</section>
|
</section>
|
||||||
</DefaultLayout>
|
</DefaultLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+227
-214
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from "react";
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from "react-router-dom";
|
||||||
import { Button } from "@heroui/button";
|
import { Button } from "@heroui/button";
|
||||||
import { Card, CardBody, CardHeader } from "@heroui/card";
|
import { Card, CardBody, CardHeader } from "@heroui/card";
|
||||||
import { Input } from "@heroui/input";
|
import { Input } from "@heroui/input";
|
||||||
@@ -7,23 +7,27 @@ import { Spinner } from "@heroui/spinner";
|
|||||||
import { Divider } from "@heroui/divider";
|
import { Divider } from "@heroui/divider";
|
||||||
import { Switch } from "@heroui/switch";
|
import { Switch } from "@heroui/switch";
|
||||||
import { Select, SelectItem } from "@heroui/select";
|
import { Select, SelectItem } from "@heroui/select";
|
||||||
import toast from 'react-hot-toast';
|
import toast from "react-hot-toast";
|
||||||
import { updateConfigs } from '@/api';
|
|
||||||
import { SettingsIcon } from '@/components/icons';
|
|
||||||
|
|
||||||
import { isAdmin } from '@/utils/auth';
|
import { updateConfigs } from "@/api";
|
||||||
import { getCachedConfigs, clearConfigCache, updateSiteConfig } from '@/config/site';
|
import { SettingsIcon } from "@/components/icons";
|
||||||
|
import { isAdmin } from "@/utils/auth";
|
||||||
|
import {
|
||||||
|
getCachedConfigs,
|
||||||
|
clearConfigCache,
|
||||||
|
updateSiteConfig,
|
||||||
|
} from "@/config/site";
|
||||||
|
|
||||||
// 简单的保存图标组件
|
// 简单的保存图标组件
|
||||||
const SaveIcon = ({ className }: { className?: string }) => (
|
const SaveIcon = ({ className }: { className?: string }) => (
|
||||||
<svg
|
<svg
|
||||||
className={className}
|
className={className}
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z" />
|
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z" />
|
||||||
<polyline points="17,21 17,13 7,13 7,21" />
|
<polyline points="17,21 17,13 7,13 7,21" />
|
||||||
@@ -36,7 +40,7 @@ interface ConfigItem {
|
|||||||
label: string;
|
label: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
type: 'input' | 'switch' | 'select';
|
type: "input" | "switch" | "select";
|
||||||
options?: { label: string; value: string; description?: string }[];
|
options?: { label: string; value: string; description?: string }[];
|
||||||
dependsOn?: string; // 依赖的配置项key
|
dependsOn?: string; // 依赖的配置项key
|
||||||
dependsValue?: string; // 依赖的配置项值
|
dependsValue?: string; // 依赖的配置项值
|
||||||
@@ -45,96 +49,102 @@ interface ConfigItem {
|
|||||||
// 网站配置项定义
|
// 网站配置项定义
|
||||||
const CONFIG_ITEMS: ConfigItem[] = [
|
const CONFIG_ITEMS: ConfigItem[] = [
|
||||||
{
|
{
|
||||||
key: 'ip',
|
key: "ip",
|
||||||
label: '面板后端地址',
|
label: "面板后端地址",
|
||||||
placeholder: '请输入面板后端IP:PORT',
|
placeholder: "请输入面板后端IP:PORT",
|
||||||
description: '格式“ip:port”,用于对接节点时使用,ip是你安装面板服务器的公网ip,端口是安装脚本内输入的后端端口。不要套CDN,不支持https,通讯数据有加密',
|
description:
|
||||||
type: 'input'
|
"格式“ip:port”,用于对接节点时使用,ip是你安装面板服务器的公网ip,端口是安装脚本内输入的后端端口。不要套CDN,不支持https,通讯数据有加密",
|
||||||
|
type: "input",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'app_name',
|
key: "app_name",
|
||||||
label: '应用名称',
|
label: "应用名称",
|
||||||
placeholder: '请输入应用名称',
|
placeholder: "请输入应用名称",
|
||||||
description: '在浏览器标签页和导航栏显示的应用名称',
|
description: "在浏览器标签页和导航栏显示的应用名称",
|
||||||
type: 'input'
|
type: "input",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'captcha_enabled',
|
key: "captcha_enabled",
|
||||||
label: '启用验证码',
|
label: "启用验证码",
|
||||||
description: '开启后,用户登录时需要完成验证码验证',
|
description: "开启后,用户登录时需要完成验证码验证",
|
||||||
type: 'switch'
|
type: "switch",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'captcha_type',
|
key: "captcha_type",
|
||||||
label: '验证码类型',
|
label: "验证码类型",
|
||||||
description: '选择验证码的显示类型,不同类型有不同的安全级别',
|
description: "选择验证码的显示类型,不同类型有不同的安全级别",
|
||||||
type: 'select',
|
type: "select",
|
||||||
dependsOn: 'captcha_enabled',
|
dependsOn: "captcha_enabled",
|
||||||
dependsValue: 'true',
|
dependsValue: "true",
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
label: '随机类型',
|
label: "随机类型",
|
||||||
value: 'RANDOM',
|
value: "RANDOM",
|
||||||
description: '系统随机选择验证码类型'
|
description: "系统随机选择验证码类型",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '滑块验证码',
|
label: "滑块验证码",
|
||||||
value: 'SLIDER',
|
value: "SLIDER",
|
||||||
description: '拖动滑块完成拼图验证'
|
description: "拖动滑块完成拼图验证",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '文字点选验证码',
|
label: "文字点选验证码",
|
||||||
value: 'WORD_IMAGE_CLICK',
|
value: "WORD_IMAGE_CLICK",
|
||||||
description: '按顺序点击指定文字'
|
description: "按顺序点击指定文字",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '旋转验证码',
|
label: "旋转验证码",
|
||||||
value: 'ROTATE',
|
value: "ROTATE",
|
||||||
description: '旋转图片到正确角度'
|
description: "旋转图片到正确角度",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '拼图验证码',
|
label: "拼图验证码",
|
||||||
value: 'CONCAT',
|
value: "CONCAT",
|
||||||
description: '拖动滑块完成图片拼接'
|
description: "拖动滑块完成图片拼接",
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// 初始化时从缓存读取配置,避免闪烁
|
// 初始化时从缓存读取配置,避免闪烁
|
||||||
const getInitialConfigs = (): Record<string, string> => {
|
const getInitialConfigs = (): Record<string, string> => {
|
||||||
if (typeof window === 'undefined') return {};
|
if (typeof window === "undefined") return {};
|
||||||
|
|
||||||
const configKeys = ['app_name', 'captcha_enabled', 'captcha_type', 'ip'];
|
const configKeys = ["app_name", "captcha_enabled", "captcha_type", "ip"];
|
||||||
const initialConfigs: Record<string, string> = {};
|
const initialConfigs: Record<string, string> = {};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
configKeys.forEach(key => {
|
configKeys.forEach((key) => {
|
||||||
const cachedValue = localStorage.getItem('vite_config_' + key);
|
const cachedValue = localStorage.getItem("vite_config_" + key);
|
||||||
|
|
||||||
if (cachedValue) {
|
if (cachedValue) {
|
||||||
initialConfigs[key] = cachedValue;
|
initialConfigs[key] = cachedValue;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {}
|
||||||
}
|
|
||||||
|
|
||||||
return initialConfigs;
|
return initialConfigs;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ConfigPage() {
|
export default function ConfigPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const initialConfigs = getInitialConfigs();
|
const initialConfigs = getInitialConfigs();
|
||||||
const [configs, setConfigs] = useState<Record<string, string>>(initialConfigs);
|
const [configs, setConfigs] =
|
||||||
const [loading, setLoading] = useState(Object.keys(initialConfigs).length === 0); // 如果有缓存数据,不显示loading
|
useState<Record<string, string>>(initialConfigs);
|
||||||
|
const [loading, setLoading] = useState(
|
||||||
|
Object.keys(initialConfigs).length === 0,
|
||||||
|
); // 如果有缓存数据,不显示loading
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [hasChanges, setHasChanges] = useState(false);
|
const [hasChanges, setHasChanges] = useState(false);
|
||||||
const [originalConfigs, setOriginalConfigs] = useState<Record<string, string>>(initialConfigs);
|
const [originalConfigs, setOriginalConfigs] =
|
||||||
|
useState<Record<string, string>>(initialConfigs);
|
||||||
|
|
||||||
// 权限检查
|
// 权限检查
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isAdmin()) {
|
if (!isAdmin()) {
|
||||||
toast.error('权限不足,只有管理员可以访问此页面');
|
toast.error("权限不足,只有管理员可以访问此页面");
|
||||||
navigate('/dashboard', { replace: true });
|
navigate("/dashboard", { replace: true });
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}, [navigate]);
|
}, [navigate]);
|
||||||
@@ -143,17 +153,19 @@ export default function ConfigPage() {
|
|||||||
const loadConfigs = async (currentConfigs?: Record<string, string>) => {
|
const loadConfigs = async (currentConfigs?: Record<string, string>) => {
|
||||||
const configsToCompare = currentConfigs || configs;
|
const configsToCompare = currentConfigs || configs;
|
||||||
const hasInitialData = Object.keys(configsToCompare).length > 0;
|
const hasInitialData = Object.keys(configsToCompare).length > 0;
|
||||||
|
|
||||||
// 如果已有缓存数据,不显示loading,静默更新
|
// 如果已有缓存数据,不显示loading,静默更新
|
||||||
if (!hasInitialData) {
|
if (!hasInitialData) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const configData = await getCachedConfigs();
|
const configData = await getCachedConfigs();
|
||||||
|
|
||||||
// 只有在数据有变化时才更新
|
// 只有在数据有变化时才更新
|
||||||
const hasDataChanged = JSON.stringify(configData) !== JSON.stringify(configsToCompare);
|
const hasDataChanged =
|
||||||
|
JSON.stringify(configData) !== JSON.stringify(configsToCompare);
|
||||||
|
|
||||||
if (hasDataChanged) {
|
if (hasDataChanged) {
|
||||||
setConfigs(configData);
|
setConfigs(configData);
|
||||||
setOriginalConfigs({ ...configData });
|
setOriginalConfigs({ ...configData });
|
||||||
@@ -163,7 +175,7 @@ export default function ConfigPage() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 只有在没有缓存数据时才显示错误
|
// 只有在没有缓存数据时才显示错误
|
||||||
if (!hasInitialData) {
|
if (!hasInitialData) {
|
||||||
toast.error('加载配置出错,请重试');
|
toast.error("加载配置出错,请重试");
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -182,22 +194,25 @@ export default function ConfigPage() {
|
|||||||
// 处理配置项变更
|
// 处理配置项变更
|
||||||
const handleConfigChange = (key: string, value: string) => {
|
const handleConfigChange = (key: string, value: string) => {
|
||||||
let newConfigs = { ...configs, [key]: value };
|
let newConfigs = { ...configs, [key]: value };
|
||||||
|
|
||||||
// 特殊处理:启用验证码时,如果验证码类型未设置,默认为随机
|
// 特殊处理:启用验证码时,如果验证码类型未设置,默认为随机
|
||||||
if (key === 'captcha_enabled' && value === 'true') {
|
if (key === "captcha_enabled" && value === "true") {
|
||||||
if (!newConfigs.captcha_type) {
|
if (!newConfigs.captcha_type) {
|
||||||
newConfigs.captcha_type = 'RANDOM';
|
newConfigs.captcha_type = "RANDOM";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setConfigs(newConfigs);
|
setConfigs(newConfigs);
|
||||||
|
|
||||||
// 检查是否有变更
|
// 检查是否有变更
|
||||||
const hasChangesNow = Object.keys(newConfigs).some(
|
const hasChangesNow =
|
||||||
k => newConfigs[k] !== originalConfigs[k]
|
Object.keys(newConfigs).some(
|
||||||
) || Object.keys(originalConfigs).some(
|
(k) => newConfigs[k] !== originalConfigs[k],
|
||||||
k => originalConfigs[k] !== newConfigs[k]
|
) ||
|
||||||
);
|
Object.keys(originalConfigs).some(
|
||||||
|
(k) => originalConfigs[k] !== newConfigs[k],
|
||||||
|
);
|
||||||
|
|
||||||
setHasChanges(hasChangesNow);
|
setHasChanges(hasChangesNow);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -206,112 +221,115 @@ export default function ConfigPage() {
|
|||||||
setSaving(true);
|
setSaving(true);
|
||||||
try {
|
try {
|
||||||
const response = await updateConfigs(configs);
|
const response = await updateConfigs(configs);
|
||||||
|
|
||||||
if (response.code === 0) {
|
if (response.code === 0) {
|
||||||
toast.success('配置保存成功');
|
toast.success("配置保存成功");
|
||||||
|
|
||||||
// 清除所有配置缓存,强制下次重新获取
|
// 清除所有配置缓存,强制下次重新获取
|
||||||
clearConfigCache();
|
clearConfigCache();
|
||||||
|
|
||||||
// 获取变更的配置项
|
// 获取变更的配置项
|
||||||
const changedKeys = Object.keys(configs).filter(
|
const changedKeys = Object.keys(configs).filter(
|
||||||
key => configs[key] !== originalConfigs[key]
|
(key) => configs[key] !== originalConfigs[key],
|
||||||
);
|
);
|
||||||
|
|
||||||
setOriginalConfigs({ ...configs });
|
setOriginalConfigs({ ...configs });
|
||||||
setHasChanges(false);
|
setHasChanges(false);
|
||||||
|
|
||||||
// 如果应用名称发生变化,立即更新网站配置
|
// 如果应用名称发生变化,立即更新网站配置
|
||||||
if (changedKeys.includes('app_name')) {
|
if (changedKeys.includes("app_name")) {
|
||||||
await updateSiteConfig();
|
await updateSiteConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 触发配置更新事件,通知其他组件
|
// 触发配置更新事件,通知其他组件
|
||||||
window.dispatchEvent(new CustomEvent('configUpdated', {
|
window.dispatchEvent(
|
||||||
detail: { changedKeys }
|
new CustomEvent("configUpdated", {
|
||||||
}));
|
detail: { changedKeys },
|
||||||
|
}),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
toast.error('保存配置失败: ' + response.msg);
|
toast.error("保存配置失败: " + response.msg);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error('保存配置出错,请重试');
|
toast.error("保存配置出错,请重试");
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 检查配置项是否应该显示(依赖检查)
|
// 检查配置项是否应该显示(依赖检查)
|
||||||
const shouldShowItem = (item: ConfigItem): boolean => {
|
const shouldShowItem = (item: ConfigItem): boolean => {
|
||||||
if (!item.dependsOn || !item.dependsValue) {
|
if (!item.dependsOn || !item.dependsValue) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return configs[item.dependsOn] === item.dependsValue;
|
return configs[item.dependsOn] === item.dependsValue;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 渲染不同类型的配置项
|
// 渲染不同类型的配置项
|
||||||
const renderConfigItem = (item: ConfigItem) => {
|
const renderConfigItem = (item: ConfigItem) => {
|
||||||
const isChanged = hasChanges && configs[item.key] !== originalConfigs[item.key];
|
const isChanged =
|
||||||
|
hasChanges && configs[item.key] !== originalConfigs[item.key];
|
||||||
|
|
||||||
switch (item.type) {
|
switch (item.type) {
|
||||||
case 'input':
|
case "input":
|
||||||
return (
|
return (
|
||||||
<Input
|
<Input
|
||||||
value={configs[item.key] || ''}
|
|
||||||
onChange={(e) => handleConfigChange(item.key, e.target.value)}
|
|
||||||
placeholder={item.placeholder}
|
|
||||||
variant="bordered"
|
|
||||||
size="md"
|
|
||||||
classNames={{
|
classNames={{
|
||||||
input: "text-sm",
|
input: "text-sm",
|
||||||
inputWrapper: isChanged
|
inputWrapper: isChanged
|
||||||
? "border-warning-300 data-[hover=true]:border-warning-400"
|
? "border-warning-300 data-[hover=true]:border-warning-400"
|
||||||
: ""
|
: "",
|
||||||
}}
|
}}
|
||||||
|
placeholder={item.placeholder}
|
||||||
|
size="md"
|
||||||
|
value={configs[item.key] || ""}
|
||||||
|
variant="bordered"
|
||||||
|
onChange={(e) => handleConfigChange(item.key, e.target.value)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
case 'switch':
|
case "switch":
|
||||||
return (
|
return (
|
||||||
<Switch
|
<Switch
|
||||||
isSelected={configs[item.key] === 'true'}
|
|
||||||
onValueChange={(checked) => handleConfigChange(item.key, checked ? 'true' : 'false')}
|
|
||||||
color="primary"
|
|
||||||
size="md"
|
|
||||||
classNames={{
|
classNames={{
|
||||||
wrapper: isChanged ? "border-warning-300" : ""
|
wrapper: isChanged ? "border-warning-300" : "",
|
||||||
}}
|
}}
|
||||||
|
color="primary"
|
||||||
|
isSelected={configs[item.key] === "true"}
|
||||||
|
size="md"
|
||||||
|
onValueChange={(checked) =>
|
||||||
|
handleConfigChange(item.key, checked ? "true" : "false")
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<span className="text-sm text-gray-700 dark:text-gray-300">
|
<span className="text-sm text-gray-700 dark:text-gray-300">
|
||||||
{configs[item.key] === 'true' ? '已启用' : '已禁用'}
|
{configs[item.key] === "true" ? "已启用" : "已禁用"}
|
||||||
</span>
|
</span>
|
||||||
</Switch>
|
</Switch>
|
||||||
);
|
);
|
||||||
|
|
||||||
case 'select':
|
case "select":
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
|
classNames={{
|
||||||
|
trigger: isChanged
|
||||||
|
? "border-warning-300 data-[hover=true]:border-warning-400"
|
||||||
|
: "",
|
||||||
|
}}
|
||||||
|
placeholder="请选择验证码类型"
|
||||||
selectedKeys={configs[item.key] ? [configs[item.key]] : []}
|
selectedKeys={configs[item.key] ? [configs[item.key]] : []}
|
||||||
|
size="md"
|
||||||
|
variant="bordered"
|
||||||
onSelectionChange={(keys) => {
|
onSelectionChange={(keys) => {
|
||||||
const selectedKey = Array.from(keys)[0] as string;
|
const selectedKey = Array.from(keys)[0] as string;
|
||||||
|
|
||||||
if (selectedKey) {
|
if (selectedKey) {
|
||||||
handleConfigChange(item.key, selectedKey);
|
handleConfigChange(item.key, selectedKey);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
placeholder="请选择验证码类型"
|
|
||||||
variant="bordered"
|
|
||||||
size="md"
|
|
||||||
classNames={{
|
|
||||||
trigger: isChanged
|
|
||||||
? "border-warning-300 data-[hover=true]:border-warning-400"
|
|
||||||
: ""
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{item.options?.map((option) => (
|
{item.options?.map((option) => (
|
||||||
<SelectItem
|
<SelectItem key={option.value} description={option.description}>
|
||||||
key={option.value}
|
|
||||||
description={option.description}
|
|
||||||
>
|
|
||||||
{option.label}
|
{option.label}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
)) || []}
|
)) || []}
|
||||||
@@ -325,105 +343,100 @@ export default function ConfigPage() {
|
|||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
|
<div className="flex items-center justify-center min-h-[400px]">
|
||||||
<div className="flex items-center justify-center min-h-[400px]">
|
<Spinner label="加载配置中..." size="lg" />
|
||||||
<Spinner size="lg" label="加载配置中..." />
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div className="p-6 max-w-4xl mx-auto">
|
||||||
<div className="p-6 max-w-4xl mx-auto">
|
{/* 页面标题 */}
|
||||||
{/* 页面标题 */}
|
<div className="flex items-center gap-3 mb-6">
|
||||||
<div className="flex items-center gap-3 mb-6">
|
<SettingsIcon className="w-8 h-8 text-primary" />
|
||||||
<SettingsIcon className="w-8 h-8 text-primary" />
|
<div>
|
||||||
<div>
|
<h1 className="text-2xl font-bold">网站配置</h1>
|
||||||
<h1 className="text-2xl font-bold">网站配置</h1>
|
<p className="text-gray-600 dark:text-gray-400">
|
||||||
<p className="text-gray-600 dark:text-gray-400">
|
管理网站的基本信息和显示设置
|
||||||
管理网站的基本信息和显示设置
|
</p>
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Card className="shadow-md">
|
<Card className="shadow-md">
|
||||||
<CardHeader className="pb-4">
|
<CardHeader className="pb-4">
|
||||||
<div className="flex justify-between items-center w-full">
|
<div className="flex justify-between items-center w-full">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xl font-semibold">基本设置</h2>
|
<h2 className="text-xl font-semibold">基本设置</h2>
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
配置网站的基本信息,这些设置会影响网站的显示效果
|
配置网站的基本信息,这些设置会影响网站的显示效果
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
|
|
||||||
<Button
|
|
||||||
color="primary"
|
|
||||||
startContent={<SaveIcon className="w-4 h-4" />}
|
|
||||||
onClick={handleSave}
|
|
||||||
isLoading={saving}
|
|
||||||
disabled={!hasChanges}
|
|
||||||
>
|
|
||||||
{saving ? '保存中...' : '保存配置'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
color="primary"
|
||||||
|
disabled={!hasChanges}
|
||||||
|
isLoading={saving}
|
||||||
|
startContent={<SaveIcon className="w-4 h-4" />}
|
||||||
|
onClick={handleSave}
|
||||||
|
>
|
||||||
|
{saving ? "保存中..." : "保存配置"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
<CardBody className="space-y-6 pt-6">
|
<CardBody className="space-y-6 pt-6">
|
||||||
{CONFIG_ITEMS.map((item, index) => {
|
{CONFIG_ITEMS.map((item, index) => {
|
||||||
// 检查配置项是否应该显示
|
// 检查配置项是否应该显示
|
||||||
if (!shouldShowItem(item)) {
|
if (!shouldShowItem(item)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算是否是最后一个显示的项目(用于决定是否显示分隔线)
|
// 计算是否是最后一个显示的项目(用于决定是否显示分隔线)
|
||||||
const remainingItems = CONFIG_ITEMS.slice(index + 1).filter(shouldShowItem);
|
const remainingItems = CONFIG_ITEMS.slice(index + 1).filter(
|
||||||
const isLastItem = remainingItems.length === 0;
|
shouldShowItem,
|
||||||
|
);
|
||||||
|
const isLastItem = remainingItems.length === 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={item.key} className="space-y-3">
|
<div key={item.key} className="space-y-3">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
{item.label}
|
{item.label}
|
||||||
</label>
|
</label>
|
||||||
{item.description && (
|
{item.description && (
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
{item.description}
|
{item.description}
|
||||||
</p>
|
</p>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 渲染配置项 */}
|
|
||||||
{renderConfigItem(item)}
|
|
||||||
|
|
||||||
{/* 分隔线 */}
|
|
||||||
{!isLastItem && (
|
|
||||||
<Divider className="mt-6" />
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
|
||||||
})}
|
{/* 渲染配置项 */}
|
||||||
|
{renderConfigItem(item)}
|
||||||
|
|
||||||
|
{/* 分隔线 */}
|
||||||
|
{!isLastItem && <Divider className="mt-6" />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 操作提示 */}
|
||||||
|
{hasChanges && (
|
||||||
|
<Card className="mt-4 bg-warning-50 dark:bg-warning-900/20 border-warning-200 dark:border-warning-800">
|
||||||
|
<CardBody className="py-3">
|
||||||
|
<div className="flex items-center gap-2 text-warning-700 dark:text-warning-300">
|
||||||
|
<div className="w-2 h-2 bg-warning-500 rounded-full animate-pulse" />
|
||||||
|
<span className="text-sm">
|
||||||
|
检测到配置变更,请记得保存您的修改
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
|
)}
|
||||||
{/* 操作提示 */}
|
</div>
|
||||||
{hasChanges && (
|
|
||||||
<Card className="mt-4 bg-warning-50 dark:bg-warning-900/20 border-warning-200 dark:border-warning-800">
|
|
||||||
<CardBody className="py-3">
|
|
||||||
<div className="flex items-center gap-2 text-warning-700 dark:text-warning-300">
|
|
||||||
<div className="w-2 h-2 bg-warning-500 rounded-full animate-pulse" />
|
|
||||||
<span className="text-sm">
|
|
||||||
检测到配置变更,请记得保存您的修改
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</CardBody>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
+1809
-1325
File diff suppressed because it is too large
Load Diff
@@ -3,10 +3,11 @@ import { Input } from "@heroui/input";
|
|||||||
import { Card, CardBody, CardHeader } from "@heroui/card";
|
import { Card, CardBody, CardHeader } from "@heroui/card";
|
||||||
import { useState, useEffect, useRef } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import toast from 'react-hot-toast';
|
import toast from "react-hot-toast";
|
||||||
import axios from 'axios';
|
import axios from "axios";
|
||||||
import { isWebViewFunc } from '@/utils/panel';
|
|
||||||
import { siteConfig } from '@/config/site';
|
import { isWebViewFunc } from "@/utils/panel";
|
||||||
|
import { siteConfig } from "@/config/site";
|
||||||
import { title } from "@/components/primitives";
|
import { title } from "@/components/primitives";
|
||||||
import DefaultLayout from "@/layouts/default";
|
import DefaultLayout from "@/layouts/default";
|
||||||
import { login, LoginData, checkCaptcha } from "@/api";
|
import { login, LoginData, checkCaptcha } from "@/api";
|
||||||
@@ -14,15 +15,12 @@ import "@/utils/tac.css";
|
|||||||
import "@/utils/tac.min.js";
|
import "@/utils/tac.min.js";
|
||||||
import bgImage from "@/images/bg.jpg";
|
import bgImage from "@/images/bg.jpg";
|
||||||
|
|
||||||
|
|
||||||
interface LoginForm {
|
interface LoginForm {
|
||||||
username: string;
|
username: string;
|
||||||
password: string;
|
password: string;
|
||||||
captchaId: string;
|
captchaId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
interface CaptchaConfig {
|
interface CaptchaConfig {
|
||||||
requestCaptchaDataUrl: string;
|
requestCaptchaDataUrl: string;
|
||||||
validCaptchaUrl: string;
|
validCaptchaUrl: string;
|
||||||
@@ -54,6 +52,7 @@ export default function IndexPage() {
|
|||||||
const tacInstanceRef = useRef<any>(null);
|
const tacInstanceRef = useRef<any>(null);
|
||||||
const captchaContainerRef = useRef<HTMLDivElement>(null);
|
const captchaContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const [isWebView, setIsWebView] = useState(false);
|
const [isWebView, setIsWebView] = useState(false);
|
||||||
|
|
||||||
// 清理验证码实例
|
// 清理验证码实例
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
@@ -72,26 +71,26 @@ export default function IndexPage() {
|
|||||||
const newErrors: Partial<LoginForm> = {};
|
const newErrors: Partial<LoginForm> = {};
|
||||||
|
|
||||||
if (!form.username.trim()) {
|
if (!form.username.trim()) {
|
||||||
newErrors.username = '请输入用户名';
|
newErrors.username = "请输入用户名";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!form.password.trim()) {
|
if (!form.password.trim()) {
|
||||||
newErrors.password = '请输入密码';
|
newErrors.password = "请输入密码";
|
||||||
} else if (form.password.length < 6) {
|
} else if (form.password.length < 6) {
|
||||||
newErrors.password = '密码长度至少6位';
|
newErrors.password = "密码长度至少6位";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
setErrors(newErrors);
|
setErrors(newErrors);
|
||||||
|
|
||||||
return Object.keys(newErrors).length === 0;
|
return Object.keys(newErrors).length === 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理输入变化
|
// 处理输入变化
|
||||||
const handleInputChange = (field: keyof LoginForm, value: string) => {
|
const handleInputChange = (field: keyof LoginForm, value: string) => {
|
||||||
setForm(prev => ({ ...prev, [field]: value }));
|
setForm((prev) => ({ ...prev, [field]: value }));
|
||||||
// 清除该字段的错误
|
// 清除该字段的错误
|
||||||
if (errors[field]) {
|
if (errors[field]) {
|
||||||
setErrors(prev => ({ ...prev, [field]: undefined }));
|
setErrors((prev) => ({ ...prev, [field]: undefined }));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -109,16 +108,18 @@ export default function IndexPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 使用axios的baseURL,确保在WebView中使用正确的面板地址
|
// 使用axios的baseURL,确保在WebView中使用正确的面板地址
|
||||||
const baseURL = axios.defaults.baseURL || (import.meta.env.VITE_API_BASE ? `${import.meta.env.VITE_API_BASE}/api/v1/` : '/api/v1/');
|
const baseURL =
|
||||||
|
axios.defaults.baseURL ||
|
||||||
|
(import.meta.env.VITE_API_BASE
|
||||||
|
? `${import.meta.env.VITE_API_BASE}/api/v1/`
|
||||||
|
: "/api/v1/");
|
||||||
|
|
||||||
const config: CaptchaConfig = {
|
const config: CaptchaConfig = {
|
||||||
requestCaptchaDataUrl: `${baseURL}captcha/generate`,
|
requestCaptchaDataUrl: `${baseURL}captcha/generate`,
|
||||||
validCaptchaUrl: `${baseURL}captcha/verify`,
|
validCaptchaUrl: `${baseURL}captcha/verify`,
|
||||||
bindEl: "#captcha-container",
|
bindEl: "#captcha-container",
|
||||||
validSuccess: (res: any, _: any, tac: any) => {
|
validSuccess: (res: any, _: any, tac: any) => {
|
||||||
|
form.captchaId = res.data.validToken;
|
||||||
|
|
||||||
form.captchaId = res.data.validToken
|
|
||||||
|
|
||||||
setShowCaptcha(false);
|
setShowCaptcha(false);
|
||||||
tac.destroyWindow();
|
tac.destroyWindow();
|
||||||
@@ -134,30 +135,30 @@ export default function IndexPage() {
|
|||||||
},
|
},
|
||||||
btnRefreshFun: (_event: any, tac: any) => {
|
btnRefreshFun: (_event: any, tac: any) => {
|
||||||
tac.reloadCaptcha();
|
tac.reloadCaptcha();
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// 检测暗黑模式
|
// 检测暗黑模式
|
||||||
const isDarkMode = document.documentElement.classList.contains('dark') ||
|
const isDarkMode =
|
||||||
document.documentElement.getAttribute('data-theme') === 'dark' ||
|
document.documentElement.classList.contains("dark") ||
|
||||||
window.matchMedia('(prefers-color-scheme: dark)').matches;
|
document.documentElement.getAttribute("data-theme") === "dark" ||
|
||||||
|
window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||||
|
|
||||||
// 根据主题调整颜色
|
// 根据主题调整颜色
|
||||||
const trackColor = isDarkMode ? "#4a5568" : "#7db0be"; // 暗黑模式使用更深的灰蓝色
|
const trackColor = isDarkMode ? "#4a5568" : "#7db0be"; // 暗黑模式使用更深的灰蓝色
|
||||||
|
|
||||||
const style: CaptchaStyle = {
|
const style: CaptchaStyle = {
|
||||||
bgUrl: bgImage,
|
bgUrl: bgImage,
|
||||||
logoUrl: null,
|
logoUrl: null,
|
||||||
moveTrackMaskBgColor: trackColor,
|
moveTrackMaskBgColor: trackColor,
|
||||||
moveTrackMaskBorderColor: trackColor
|
moveTrackMaskBorderColor: trackColor,
|
||||||
};
|
};
|
||||||
|
|
||||||
tacInstanceRef.current = new window.TAC(config, style);
|
tacInstanceRef.current = new window.TAC(config, style);
|
||||||
tacInstanceRef.current.init();
|
tacInstanceRef.current.init();
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('初始化验证码失败:', error);
|
console.error("初始化验证码失败:", error);
|
||||||
toast.error('验证码初始化失败,请刷新页面重试');
|
toast.error("验证码初始化失败,请刷新页面重试");
|
||||||
setShowCaptcha(false);
|
setShowCaptcha(false);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -165,8 +166,6 @@ export default function IndexPage() {
|
|||||||
|
|
||||||
// 执行登录请求
|
// 执行登录请求
|
||||||
const performLogin = async () => {
|
const performLogin = async () => {
|
||||||
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const loginData: LoginData = {
|
const loginData: LoginData = {
|
||||||
username: form.username.trim(),
|
username: form.username.trim(),
|
||||||
@@ -175,35 +174,36 @@ export default function IndexPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const response = await login(loginData);
|
const response = await login(loginData);
|
||||||
|
|
||||||
if (response.code !== 0) {
|
if (response.code !== 0) {
|
||||||
toast.error(response.msg || "登录失败");
|
toast.error(response.msg || "登录失败");
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否需要强制修改密码
|
// 检查是否需要强制修改密码
|
||||||
if (response.data.requirePasswordChange) {
|
if (response.data.requirePasswordChange) {
|
||||||
localStorage.setItem('token', response.data.token);
|
localStorage.setItem("token", response.data.token);
|
||||||
localStorage.setItem("role_id", response.data.role_id.toString());
|
localStorage.setItem("role_id", response.data.role_id.toString());
|
||||||
localStorage.setItem("name", response.data.name);
|
localStorage.setItem("name", response.data.name);
|
||||||
localStorage.setItem("admin", (response.data.role_id === 0).toString());
|
localStorage.setItem("admin", (response.data.role_id === 0).toString());
|
||||||
toast.success('检测到默认密码,即将跳转到修改密码页面');
|
toast.success("检测到默认密码,即将跳转到修改密码页面");
|
||||||
navigate("/change-password");
|
navigate("/change-password");
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存登录信息
|
// 保存登录信息
|
||||||
localStorage.setItem('token', response.data.token);
|
localStorage.setItem("token", response.data.token);
|
||||||
localStorage.setItem("role_id", response.data.role_id.toString());
|
localStorage.setItem("role_id", response.data.role_id.toString());
|
||||||
localStorage.setItem("name", response.data.name);
|
localStorage.setItem("name", response.data.name);
|
||||||
localStorage.setItem("admin", (response.data.role_id === 0).toString());
|
localStorage.setItem("admin", (response.data.role_id === 0).toString());
|
||||||
|
|
||||||
// 登录成功
|
// 登录成功
|
||||||
toast.success('登录成功');
|
toast.success("登录成功");
|
||||||
navigate("/dashboard");
|
navigate("/dashboard");
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('登录错误:', error);
|
console.error("登录错误:", error);
|
||||||
toast.error("网络错误,请稍后重试");
|
toast.error("网络错误,请稍后重试");
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -218,10 +218,11 @@ export default function IndexPage() {
|
|||||||
try {
|
try {
|
||||||
// 先检查是否需要验证码
|
// 先检查是否需要验证码
|
||||||
const checkResponse = await checkCaptcha();
|
const checkResponse = await checkCaptcha();
|
||||||
|
|
||||||
if (checkResponse.code !== 0) {
|
if (checkResponse.code !== 0) {
|
||||||
toast.error("检查验证码状态失败,请重试" + checkResponse.msg);
|
toast.error("检查验证码状态失败,请重试" + checkResponse.msg);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,15 +239,14 @@ export default function IndexPage() {
|
|||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('检查验证码状态错误:', error);
|
console.error("检查验证码状态错误:", error);
|
||||||
toast.error("网络错误,请稍后重试" + error);
|
toast.error("网络错误,请稍后重试" + error);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const handleKeyPress = (e: React.KeyboardEvent) => {
|
const handleKeyPress = (e: React.KeyboardEvent) => {
|
||||||
if (e.key === 'Enter' && !loading) {
|
if (e.key === "Enter" && !loading) {
|
||||||
handleLogin();
|
handleLogin();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -258,42 +258,47 @@ export default function IndexPage() {
|
|||||||
<Card className="w-full">
|
<Card className="w-full">
|
||||||
<CardHeader className="pb-0 pt-6 px-6 flex-col items-center">
|
<CardHeader className="pb-0 pt-6 px-6 flex-col items-center">
|
||||||
<h1 className={title({ size: "sm" })}>登陆</h1>
|
<h1 className={title({ size: "sm" })}>登陆</h1>
|
||||||
<p className="text-small text-default-500 mt-2">请输入您的账号信息</p>
|
<p className="text-small text-default-500 mt-2">
|
||||||
|
请输入您的账号信息
|
||||||
|
</p>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardBody className="px-6 py-6">
|
<CardBody className="px-6 py-6">
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<Input
|
<Input
|
||||||
|
errorMessage={errors.username}
|
||||||
|
isDisabled={loading}
|
||||||
|
isInvalid={!!errors.username}
|
||||||
label="用户名"
|
label="用户名"
|
||||||
placeholder="请输入用户名"
|
placeholder="请输入用户名"
|
||||||
value={form.username}
|
value={form.username}
|
||||||
onChange={(e) => handleInputChange('username', e.target.value)}
|
|
||||||
onKeyDown={handleKeyPress}
|
|
||||||
variant="bordered"
|
variant="bordered"
|
||||||
isDisabled={loading}
|
onChange={(e) =>
|
||||||
isInvalid={!!errors.username}
|
handleInputChange("username", e.target.value)
|
||||||
errorMessage={errors.username}
|
}
|
||||||
|
onKeyDown={handleKeyPress}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
|
isDisabled={loading}
|
||||||
|
isInvalid={!!errors.password}
|
||||||
label="密码"
|
label="密码"
|
||||||
placeholder="请输入密码"
|
placeholder="请输入密码"
|
||||||
type="password"
|
type="password"
|
||||||
value={form.password}
|
value={form.password}
|
||||||
onChange={(e) => handleInputChange('password', e.target.value)}
|
|
||||||
onKeyDown={handleKeyPress}
|
|
||||||
variant="bordered"
|
variant="bordered"
|
||||||
isDisabled={loading}
|
onChange={(e) =>
|
||||||
isInvalid={!!errors.password}
|
handleInputChange("password", e.target.value)
|
||||||
|
}
|
||||||
|
onKeyDown={handleKeyPress}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
className="mt-2"
|
||||||
color="primary"
|
color="primary"
|
||||||
|
disabled={loading}
|
||||||
|
isLoading={loading}
|
||||||
size="lg"
|
size="lg"
|
||||||
onClick={handleLogin}
|
onClick={handleLogin}
|
||||||
isLoading={loading}
|
|
||||||
disabled={loading}
|
|
||||||
className="mt-2"
|
|
||||||
>
|
>
|
||||||
{loading ? (showCaptcha ? "验证中..." : "登录中...") : "登录"}
|
{loading ? (showCaptcha ? "验证中..." : "登录中...") : "登录"}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -302,47 +307,47 @@ export default function IndexPage() {
|
|||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 版权信息 - 固定在底部,不占据布局空间 */}
|
||||||
|
|
||||||
{/* 版权信息 - 固定在底部,不占据布局空间 */}
|
<div className="fixed inset-x-0 bottom-4 text-center py-4">
|
||||||
|
<p className="text-xs text-gray-400 dark:text-gray-500">
|
||||||
<div className="fixed inset-x-0 bottom-4 text-center py-4">
|
Powered by{" "}
|
||||||
<p className="text-xs text-gray-400 dark:text-gray-500">
|
<a
|
||||||
Powered by{' '}
|
className="text-gray-500 dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
|
||||||
<a
|
href={siteConfig.github_repo}
|
||||||
href={siteConfig.github_repo}
|
rel="noopener noreferrer"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
>
|
||||||
className="text-gray-500 dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
|
flux-panel
|
||||||
>
|
</a>
|
||||||
flux-panel
|
</p>
|
||||||
</a>
|
<p className="text-xs text-gray-400 dark:text-gray-500 mt-1">
|
||||||
</p>
|
v{isWebView ? siteConfig.app_version : siteConfig.version}
|
||||||
<p className="text-xs text-gray-400 dark:text-gray-500 mt-1">
|
</p>
|
||||||
v{ isWebView ? siteConfig.app_version : siteConfig.version}
|
</div>
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{/* 验证码弹层 */}
|
{/* 验证码弹层 */}
|
||||||
{showCaptcha && (
|
{showCaptcha && (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||||
{/* 背景遮罩层 - 模糊效果,暗黑模式下更深 */}
|
{/* 背景遮罩层 - 模糊效果,暗黑模式下更深 */}
|
||||||
<div className="absolute inset-0 bg-black/60 dark:bg-black/80 backdrop-blur-sm captcha-backdrop-enter" />
|
<div className="absolute inset-0 bg-black/60 dark:bg-black/80 backdrop-blur-sm captcha-backdrop-enter" />
|
||||||
{/* 验证码容器 */}
|
{/* 验证码容器 */}
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<div
|
<div
|
||||||
id="captcha-container"
|
ref={captchaContainerRef}
|
||||||
ref={captchaContainerRef}
|
className="w-full flex justify-center"
|
||||||
className="w-full flex justify-center"
|
id="captcha-container"
|
||||||
style={{
|
style={{
|
||||||
filter: document.documentElement.classList.contains('dark') ||
|
filter:
|
||||||
document.documentElement.getAttribute('data-theme') === 'dark' ||
|
document.documentElement.classList.contains("dark") ||
|
||||||
window.matchMedia('(prefers-color-scheme: dark)').matches
|
document.documentElement.getAttribute("data-theme") ===
|
||||||
? 'brightness(0.8) contrast(0.9)' : 'none'
|
"dark" ||
|
||||||
}}
|
window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||||
/>
|
? "brightness(0.8) contrast(0.9)"
|
||||||
</div>
|
: "none",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
+320
-275
@@ -3,18 +3,23 @@ import { Card, CardBody, CardHeader } from "@heroui/card";
|
|||||||
import { Button } from "@heroui/button";
|
import { Button } from "@heroui/button";
|
||||||
import { Input } from "@heroui/input";
|
import { Input } from "@heroui/input";
|
||||||
import { Select, SelectItem } from "@heroui/select";
|
import { Select, SelectItem } from "@heroui/select";
|
||||||
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter } from "@heroui/modal";
|
import {
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
|
ModalHeader,
|
||||||
|
ModalBody,
|
||||||
|
ModalFooter,
|
||||||
|
} from "@heroui/modal";
|
||||||
import { Chip } from "@heroui/chip";
|
import { Chip } from "@heroui/chip";
|
||||||
import { Spinner } from "@heroui/spinner";
|
import { Spinner } from "@heroui/spinner";
|
||||||
import toast from 'react-hot-toast';
|
import toast from "react-hot-toast";
|
||||||
|
|
||||||
|
import {
|
||||||
import {
|
createSpeedLimit,
|
||||||
createSpeedLimit,
|
getSpeedLimitList,
|
||||||
getSpeedLimitList,
|
updateSpeedLimit,
|
||||||
updateSpeedLimit,
|
deleteSpeedLimit,
|
||||||
deleteSpeedLimit,
|
getTunnelList,
|
||||||
getTunnelList
|
|
||||||
} from "@/api";
|
} from "@/api";
|
||||||
|
|
||||||
interface SpeedLimitRule {
|
interface SpeedLimitRule {
|
||||||
@@ -46,7 +51,7 @@ export default function LimitPage() {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [rules, setRules] = useState<SpeedLimitRule[]>([]);
|
const [rules, setRules] = useState<SpeedLimitRule[]>([]);
|
||||||
const [tunnels, setTunnels] = useState<Tunnel[]>([]);
|
const [tunnels, setTunnels] = useState<Tunnel[]>([]);
|
||||||
|
|
||||||
// 模态框状态
|
// 模态框状态
|
||||||
const [modalOpen, setModalOpen] = useState(false);
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
|
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
|
||||||
@@ -54,18 +59,18 @@ export default function LimitPage() {
|
|||||||
const [submitLoading, setSubmitLoading] = useState(false);
|
const [submitLoading, setSubmitLoading] = useState(false);
|
||||||
const [deleteLoading, setDeleteLoading] = useState(false);
|
const [deleteLoading, setDeleteLoading] = useState(false);
|
||||||
const [ruleToDelete, setRuleToDelete] = useState<SpeedLimitRule | null>(null);
|
const [ruleToDelete, setRuleToDelete] = useState<SpeedLimitRule | null>(null);
|
||||||
|
|
||||||
// 表单状态
|
// 表单状态
|
||||||
const [form, setForm] = useState<SpeedLimitForm>({
|
const [form, setForm] = useState<SpeedLimitForm>({
|
||||||
name: '',
|
name: "",
|
||||||
speed: 100,
|
speed: 100,
|
||||||
tunnelId: null,
|
tunnelId: null,
|
||||||
tunnelName: '',
|
tunnelName: "",
|
||||||
status: 1
|
status: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 表单验证错误
|
// 表单验证错误
|
||||||
const [errors, setErrors] = useState<{[key: string]: string}>({});
|
const [errors, setErrors] = useState<{ [key: string]: string }>({});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadData();
|
loadData();
|
||||||
@@ -77,23 +82,23 @@ export default function LimitPage() {
|
|||||||
try {
|
try {
|
||||||
const [rulesRes, tunnelsRes] = await Promise.all([
|
const [rulesRes, tunnelsRes] = await Promise.all([
|
||||||
getSpeedLimitList(),
|
getSpeedLimitList(),
|
||||||
getTunnelList()
|
getTunnelList(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (rulesRes.code === 0) {
|
if (rulesRes.code === 0) {
|
||||||
setRules(rulesRes.data || []);
|
setRules(rulesRes.data || []);
|
||||||
} else {
|
} else {
|
||||||
toast.error(rulesRes.msg || '获取限速规则失败');
|
toast.error(rulesRes.msg || "获取限速规则失败");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tunnelsRes.code === 0) {
|
if (tunnelsRes.code === 0) {
|
||||||
setTunnels(tunnelsRes.data || []);
|
setTunnels(tunnelsRes.data || []);
|
||||||
} else {
|
} else {
|
||||||
console.warn('获取隧道列表失败:', tunnelsRes.msg);
|
console.warn("获取隧道列表失败:", tunnelsRes.msg);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载数据失败:', error);
|
console.error("加载数据失败:", error);
|
||||||
toast.error('加载数据失败');
|
toast.error("加载数据失败");
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -101,23 +106,24 @@ export default function LimitPage() {
|
|||||||
|
|
||||||
// 表单验证
|
// 表单验证
|
||||||
const validateForm = (): boolean => {
|
const validateForm = (): boolean => {
|
||||||
const newErrors: {[key: string]: string} = {};
|
const newErrors: { [key: string]: string } = {};
|
||||||
|
|
||||||
if (!form.name.trim()) {
|
if (!form.name.trim()) {
|
||||||
newErrors.name = '请输入规则名称';
|
newErrors.name = "请输入规则名称";
|
||||||
} else if (form.name.length < 2 || form.name.length > 50) {
|
} else if (form.name.length < 2 || form.name.length > 50) {
|
||||||
newErrors.name = '规则名称长度应在2-50个字符之间';
|
newErrors.name = "规则名称长度应在2-50个字符之间";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!form.speed || form.speed < 1) {
|
if (!form.speed || form.speed < 1) {
|
||||||
newErrors.speed = '请输入有效的速度限制(≥1 Mbps)';
|
newErrors.speed = "请输入有效的速度限制(≥1 Mbps)";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!form.tunnelId) {
|
if (!form.tunnelId) {
|
||||||
newErrors.tunnelId = '请选择要绑定的隧道';
|
newErrors.tunnelId = "请选择要绑定的隧道";
|
||||||
}
|
}
|
||||||
|
|
||||||
setErrors(newErrors);
|
setErrors(newErrors);
|
||||||
|
|
||||||
return Object.keys(newErrors).length === 0;
|
return Object.keys(newErrors).length === 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -125,11 +131,11 @@ export default function LimitPage() {
|
|||||||
const handleAdd = () => {
|
const handleAdd = () => {
|
||||||
setIsEdit(false);
|
setIsEdit(false);
|
||||||
setForm({
|
setForm({
|
||||||
name: '',
|
name: "",
|
||||||
speed: 100,
|
speed: 100,
|
||||||
tunnelId: null,
|
tunnelId: null,
|
||||||
tunnelName: '',
|
tunnelName: "",
|
||||||
status: 1
|
status: 1,
|
||||||
});
|
});
|
||||||
setErrors({});
|
setErrors({});
|
||||||
setModalOpen(true);
|
setModalOpen(true);
|
||||||
@@ -144,7 +150,7 @@ export default function LimitPage() {
|
|||||||
speed: rule.speed,
|
speed: rule.speed,
|
||||||
tunnelId: rule.tunnelId,
|
tunnelId: rule.tunnelId,
|
||||||
tunnelName: rule.tunnelName,
|
tunnelName: rule.tunnelName,
|
||||||
status: rule.status
|
status: rule.status,
|
||||||
});
|
});
|
||||||
setErrors({});
|
setErrors({});
|
||||||
setModalOpen(true);
|
setModalOpen(true);
|
||||||
@@ -159,20 +165,21 @@ export default function LimitPage() {
|
|||||||
// 确认删除规则
|
// 确认删除规则
|
||||||
const confirmDelete = async () => {
|
const confirmDelete = async () => {
|
||||||
if (!ruleToDelete) return;
|
if (!ruleToDelete) return;
|
||||||
|
|
||||||
setDeleteLoading(true);
|
setDeleteLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await deleteSpeedLimit(ruleToDelete.id);
|
const res = await deleteSpeedLimit(ruleToDelete.id);
|
||||||
|
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
toast.success('删除成功');
|
toast.success("删除成功");
|
||||||
setDeleteModalOpen(false);
|
setDeleteModalOpen(false);
|
||||||
loadData();
|
loadData();
|
||||||
} else {
|
} else {
|
||||||
toast.error(res.msg || '删除失败');
|
toast.error(res.msg || "删除失败");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('删除失败:', error);
|
console.error("删除失败:", error);
|
||||||
toast.error('删除失败');
|
toast.error("删除失败");
|
||||||
} finally {
|
} finally {
|
||||||
setDeleteLoading(false);
|
setDeleteLoading(false);
|
||||||
}
|
}
|
||||||
@@ -181,27 +188,29 @@ export default function LimitPage() {
|
|||||||
// 提交表单
|
// 提交表单
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!validateForm()) return;
|
if (!validateForm()) return;
|
||||||
|
|
||||||
setSubmitLoading(true);
|
setSubmitLoading(true);
|
||||||
try {
|
try {
|
||||||
let res;
|
let res;
|
||||||
|
|
||||||
if (isEdit) {
|
if (isEdit) {
|
||||||
res = await updateSpeedLimit(form);
|
res = await updateSpeedLimit(form);
|
||||||
} else {
|
} else {
|
||||||
const { id, ...createData } = form;
|
const { id, ...createData } = form;
|
||||||
|
|
||||||
res = await createSpeedLimit(createData);
|
res = await createSpeedLimit(createData);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
toast.success(isEdit ? '修改成功' : '创建成功');
|
toast.success(isEdit ? "修改成功" : "创建成功");
|
||||||
setModalOpen(false);
|
setModalOpen(false);
|
||||||
loadData();
|
loadData();
|
||||||
} else {
|
} else {
|
||||||
toast.error(res.msg || '操作失败');
|
toast.error(res.msg || "操作失败");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('提交失败:', error);
|
console.error("提交失败:", error);
|
||||||
toast.error('操作失败');
|
toast.error("操作失败");
|
||||||
} finally {
|
} finally {
|
||||||
setSubmitLoading(false);
|
setSubmitLoading(false);
|
||||||
}
|
}
|
||||||
@@ -209,269 +218,305 @@ export default function LimitPage() {
|
|||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
|
<div className="flex items-center justify-center h-64">
|
||||||
<div className="flex items-center justify-center h-64">
|
<div className="flex items-center gap-3">
|
||||||
<div className="flex items-center gap-3">
|
<Spinner size="sm" />
|
||||||
<Spinner size="sm" />
|
<span className="text-default-600">正在加载...</span>
|
||||||
<span className="text-default-600">正在加载...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div className="px-3 lg:px-6 py-8">
|
||||||
<div className="px-3 lg:px-6 py-8">
|
{/* 页面头部 */}
|
||||||
{/* 页面头部 */}
|
<div className="flex items-center justify-between mb-6">
|
||||||
<div className="flex items-center justify-between mb-6">
|
<div className="flex-1" />
|
||||||
<div className="flex-1">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
<Button size="sm" variant="flat" color="primary" onPress={handleAdd}>
|
||||||
size="sm"
|
新增
|
||||||
variant="flat"
|
</Button>
|
||||||
color="primary"
|
</div>
|
||||||
onPress={handleAdd}
|
|
||||||
|
{/* 统一卡片网格 */}
|
||||||
|
{rules.length > 0 ? (
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4">
|
||||||
|
{rules.map((rule) => (
|
||||||
|
<Card
|
||||||
|
key={rule.id}
|
||||||
|
className="shadow-sm border border-gray-200 dark:border-gray-700"
|
||||||
>
|
>
|
||||||
新增
|
<CardHeader className="pb-3">
|
||||||
</Button>
|
<div className="flex justify-between items-start w-full">
|
||||||
</div>
|
<div>
|
||||||
|
<h3 className="font-semibold text-foreground">
|
||||||
{/* 统一卡片网格 */}
|
{rule.name}
|
||||||
{rules.length > 0 ? (
|
</h3>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4">
|
</div>
|
||||||
{rules.map((rule) => (
|
<Chip
|
||||||
<Card key={rule.id} className="shadow-sm border border-gray-200 dark:border-gray-700">
|
color={rule.status === 1 ? "success" : "danger"}
|
||||||
<CardHeader className="pb-3">
|
variant="flat"
|
||||||
<div className="flex justify-between items-start w-full">
|
size="sm"
|
||||||
<div>
|
>
|
||||||
<h3 className="font-semibold text-foreground">{rule.name}</h3>
|
{rule.status === 1 ? "运行" : "异常"}
|
||||||
</div>
|
</Chip>
|
||||||
<Chip
|
</div>
|
||||||
color={rule.status === 1 ? "success" : "danger"}
|
</CardHeader>
|
||||||
variant="flat"
|
<CardBody className="pt-0">
|
||||||
size="sm"
|
<div className="space-y-3">
|
||||||
>
|
<div className="flex justify-between items-center">
|
||||||
{rule.status === 1 ? '运行' : '异常'}
|
<span className="text-small text-default-600">
|
||||||
|
速度限制
|
||||||
|
</span>
|
||||||
|
<Chip color="secondary" size="sm" variant="flat">
|
||||||
|
{rule.speed} Mbps
|
||||||
</Chip>
|
</Chip>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
<div className="flex justify-between items-center">
|
||||||
<CardBody className="pt-0">
|
<span className="text-small text-default-600">
|
||||||
<div className="space-y-3">
|
绑定隧道
|
||||||
<div className="flex justify-between items-center">
|
</span>
|
||||||
<span className="text-small text-default-600">速度限制</span>
|
{rule.tunnelName ? (
|
||||||
<Chip color="secondary" variant="flat" size="sm">
|
<Chip color="primary" size="sm" variant="flat">
|
||||||
{rule.speed} Mbps
|
{rule.tunnelName}
|
||||||
</Chip>
|
</Chip>
|
||||||
</div>
|
) : (
|
||||||
<div className="flex justify-between items-center">
|
<span className="text-default-400 text-small">
|
||||||
<span className="text-small text-default-600">绑定隧道</span>
|
未绑定
|
||||||
{rule.tunnelName ? (
|
</span>
|
||||||
<Chip color="primary" variant="flat" size="sm">
|
)}
|
||||||
{rule.tunnelName}
|
|
||||||
</Chip>
|
|
||||||
) : (
|
|
||||||
<span className="text-default-400 text-small">未绑定</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div className="flex gap-2 mt-4">
|
|
||||||
<Button
|
<div className="flex gap-2 mt-4">
|
||||||
size="sm"
|
<Button
|
||||||
variant="flat"
|
className="flex-1"
|
||||||
color="primary"
|
color="primary"
|
||||||
onPress={() => handleEdit(rule)}
|
size="sm"
|
||||||
className="flex-1"
|
|
||||||
startContent={
|
startContent={
|
||||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||||
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" />
|
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" />
|
||||||
</svg>
|
</svg>
|
||||||
}
|
}
|
||||||
>
|
|
||||||
编辑
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="flat"
|
variant="flat"
|
||||||
color="danger"
|
onPress={() => handleEdit(rule)}
|
||||||
onPress={() => handleDelete(rule)}
|
>
|
||||||
className="flex-1"
|
编辑
|
||||||
startContent={
|
</Button>
|
||||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
<Button
|
||||||
<path fillRule="evenodd" d="M9 2a1 1 0 000 2h2a1 1 0 100-2H9z" clipRule="evenodd" />
|
size="sm"
|
||||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8 7a1 1 0 012 0v4a1 1 0 11-2 0V7zM12 7a1 1 0 012 0v4a1 1 0 11-2 0V7z" clipRule="evenodd" />
|
variant="flat"
|
||||||
</svg>
|
color="danger"
|
||||||
}
|
onPress={() => handleDelete(rule)}
|
||||||
>
|
className="flex-1"
|
||||||
删除
|
startContent={
|
||||||
</Button>
|
<svg
|
||||||
</div>
|
className="w-4 h-4"
|
||||||
</CardBody>
|
fill="currentColor"
|
||||||
</Card>
|
viewBox="0 0 20 20"
|
||||||
))}
|
>
|
||||||
</div>
|
<path
|
||||||
) : (
|
fillRule="evenodd"
|
||||||
/* 空状态 */
|
d="M9 2a1 1 0 000 2h2a1 1 0 100-2H9z"
|
||||||
<Card className="shadow-sm border border-gray-200 dark:border-gray-700">
|
clipRule="evenodd"
|
||||||
<CardBody className="text-center py-16">
|
/>
|
||||||
<div className="flex flex-col items-center gap-4">
|
<path
|
||||||
<div className="w-16 h-16 bg-default-100 rounded-full flex items-center justify-center">
|
fillRule="evenodd"
|
||||||
<svg className="w-8 h-8 text-default-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8 7a1 1 0 012 0v4a1 1 0 11-2 0V7zM12 7a1 1 0 012 0v4a1 1 0 11-2 0V7z"
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M12 6v6l4 2m6-6a9 9 0 11-18 0 9 9 0 0118 0z" />
|
clipRule="evenodd"
|
||||||
</svg>
|
/>
|
||||||
</div>
|
</svg>
|
||||||
<div>
|
}
|
||||||
<h3 className="text-lg font-semibold text-foreground">暂无限速规则</h3>
|
>
|
||||||
<p className="text-default-500 text-sm mt-1">还没有创建任何限速规则,点击上方按钮开始创建</p>
|
删除
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
/* 空状态 */
|
||||||
|
<Card className="shadow-sm border border-gray-200 dark:border-gray-700">
|
||||||
|
<CardBody className="text-center py-16">
|
||||||
|
<div className="flex flex-col items-center gap-4">
|
||||||
|
<div className="w-16 h-16 bg-default-100 rounded-full flex items-center justify-center">
|
||||||
|
<svg
|
||||||
|
className="w-8 h-8 text-default-400"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
d="M12 6v6l4 2m6-6a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</CardBody>
|
<div>
|
||||||
</Card>
|
<h3 className="text-lg font-semibold text-foreground">
|
||||||
)}
|
暂无限速规则
|
||||||
|
</h3>
|
||||||
|
<p className="text-default-500 text-sm mt-1">
|
||||||
|
还没有创建任何限速规则,点击上方按钮开始创建
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 新增/编辑模态框 */}
|
{/* 新增/编辑模态框 */}
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={modalOpen}
|
isOpen={modalOpen}
|
||||||
onOpenChange={setModalOpen}
|
onOpenChange={setModalOpen}
|
||||||
size="2xl"
|
size="2xl"
|
||||||
scrollBehavior="outside"
|
scrollBehavior="outside"
|
||||||
backdrop="blur"
|
backdrop="blur"
|
||||||
placement="center"
|
placement="center"
|
||||||
>
|
>
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
{(onClose) => (
|
{(onClose) => (
|
||||||
<>
|
<>
|
||||||
<ModalHeader className="flex flex-col gap-1">
|
<ModalHeader className="flex flex-col gap-1">
|
||||||
<h2 className="text-xl font-bold">
|
<h2 className="text-xl font-bold">
|
||||||
{isEdit ? '编辑限速规则' : '新增限速规则'}
|
{isEdit ? "编辑限速规则" : "新增限速规则"}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-small text-default-500">
|
<p className="text-small text-default-500">
|
||||||
{isEdit ? '修改现有限速规则的配置信息' : '创建新的限速规则并绑定到隧道'}
|
{isEdit
|
||||||
</p>
|
? "修改现有限速规则的配置信息"
|
||||||
</ModalHeader>
|
: "创建新的限速规则并绑定到隧道"}
|
||||||
<ModalBody>
|
</p>
|
||||||
<div className="space-y-4">
|
</ModalHeader>
|
||||||
<Input
|
<ModalBody>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Input
|
||||||
|
errorMessage={errors.name}
|
||||||
|
isInvalid={!!errors.name}
|
||||||
label="规则名称"
|
label="规则名称"
|
||||||
placeholder="请输入限速规则名称"
|
placeholder="请输入限速规则名称"
|
||||||
value={form.name}
|
value={form.name}
|
||||||
|
variant="bordered"
|
||||||
onChange={(e) => setForm(prev => ({ ...prev, name: e.target.value }))}
|
onChange={(e) => setForm(prev => ({ ...prev, name: e.target.value }))}
|
||||||
isInvalid={!!errors.name}
|
/>
|
||||||
errorMessage={errors.name}
|
|
||||||
variant="bordered"
|
<Input
|
||||||
/>
|
endContent={
|
||||||
|
|
||||||
<Input
|
|
||||||
label="速度限制"
|
|
||||||
placeholder="请输入速度限制"
|
|
||||||
type="number"
|
|
||||||
value={form.speed.toString()}
|
|
||||||
onChange={(e) => setForm(prev => ({ ...prev, speed: parseInt(e.target.value) || 0 }))}
|
|
||||||
isInvalid={!!errors.speed}
|
|
||||||
errorMessage={errors.speed}
|
|
||||||
variant="bordered"
|
|
||||||
endContent={
|
|
||||||
<div className="pointer-events-none flex items-center">
|
<div className="pointer-events-none flex items-center">
|
||||||
<span className="text-default-400 text-small">Mbps</span>
|
<span className="text-default-400 text-small">Mbps</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
errorMessage={errors.speed}
|
||||||
|
isInvalid={!!errors.speed}
|
||||||
<Select
|
label="速度限制"
|
||||||
label="绑定隧道"
|
placeholder="请输入速度限制"
|
||||||
placeholder="请选择要绑定的隧道"
|
type="number"
|
||||||
selectedKeys={form.tunnelId ? [form.tunnelId.toString()] : []}
|
value={form.speed.toString()}
|
||||||
onSelectionChange={(keys) => {
|
|
||||||
const selectedKey = Array.from(keys)[0] as string;
|
|
||||||
if (selectedKey) {
|
|
||||||
const selectedTunnel = tunnels.find(tunnel => tunnel.id === parseInt(selectedKey));
|
|
||||||
setForm(prev => ({
|
|
||||||
...prev,
|
|
||||||
tunnelId: parseInt(selectedKey),
|
|
||||||
tunnelName: selectedTunnel?.name || ''
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
setForm(prev => ({
|
|
||||||
...prev,
|
|
||||||
tunnelId: null,
|
|
||||||
tunnelName: ''
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
isInvalid={!!errors.tunnelId}
|
|
||||||
errorMessage={errors.tunnelId}
|
|
||||||
variant="bordered"
|
variant="bordered"
|
||||||
isDisabled={isEdit}
|
onChange={(e) => setForm(prev => ({ ...prev, speed: parseInt(e.target.value) || 0 }))}
|
||||||
description={isEdit ? "编辑时无法修改绑定隧道" : undefined}
|
}
|
||||||
>
|
/>
|
||||||
{tunnels.map((tunnel) => (
|
|
||||||
<SelectItem key={tunnel.id}>
|
|
||||||
{tunnel.name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</ModalBody>
|
|
||||||
<ModalFooter>
|
|
||||||
<Button variant="light" onPress={onClose}>
|
|
||||||
取消
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
color="primary"
|
|
||||||
onPress={handleSubmit}
|
|
||||||
isLoading={submitLoading}
|
|
||||||
>
|
|
||||||
{isEdit ? '保存修改' : '创建规则'}
|
|
||||||
</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
{/* 删除确认模态框 */}
|
<Select
|
||||||
<Modal
|
label="绑定隧道"
|
||||||
isOpen={deleteModalOpen}
|
placeholder="请选择要绑定的隧道"
|
||||||
onOpenChange={setDeleteModalOpen}
|
selectedKeys={
|
||||||
size="2xl"
|
form.tunnelId ? [form.tunnelId.toString()] : []
|
||||||
|
}
|
||||||
|
onSelectionChange={(keys) => {
|
||||||
|
const selectedKey = Array.from(keys)[0] as string;
|
||||||
|
|
||||||
|
if (selectedKey) {
|
||||||
|
const selectedTunnel = tunnels.find(
|
||||||
|
(tunnel) => tunnel.id === parseInt(selectedKey),
|
||||||
|
);
|
||||||
|
setForm((prev) => ({
|
||||||
|
...prev,
|
||||||
|
tunnelId: parseInt(selectedKey),
|
||||||
|
tunnelName: selectedTunnel?.name || "",
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
setForm((prev) => ({
|
||||||
|
...prev,
|
||||||
|
tunnelId: null,
|
||||||
|
tunnelName: "",
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
isInvalid={!!errors.tunnelId}
|
||||||
|
errorMessage={errors.tunnelId}
|
||||||
|
variant="bordered"
|
||||||
|
isDisabled={isEdit}
|
||||||
|
description={isEdit ? "编辑时无法修改绑定隧道" : undefined}
|
||||||
|
>
|
||||||
|
{tunnels.map((tunnel) => (
|
||||||
|
<SelectItem key={tunnel.id}>{tunnel.name}</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button variant="light" onPress={onClose}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color="primary"
|
||||||
|
onPress={handleSubmit}
|
||||||
|
isLoading={submitLoading}
|
||||||
|
>
|
||||||
|
{isEdit ? "保存修改" : "创建规则"}
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
{/* 删除确认模态框 */}
|
||||||
|
<Modal
|
||||||
|
isOpen={deleteModalOpen}
|
||||||
|
onOpenChange={setDeleteModalOpen}
|
||||||
|
size="2xl"
|
||||||
scrollBehavior="outside"
|
scrollBehavior="outside"
|
||||||
backdrop="blur"
|
backdrop="blur"
|
||||||
placement="center"
|
placement="center"
|
||||||
>
|
>
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
{(onClose) => (
|
{(onClose) => (
|
||||||
<>
|
<>
|
||||||
<ModalHeader className="flex flex-col gap-1">
|
<ModalHeader className="flex flex-col gap-1">
|
||||||
<h2 className="text-lg font-bold text-danger">确认删除</h2>
|
<h2 className="text-lg font-bold text-danger">确认删除</h2>
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<p className="text-default-600">
|
<p className="text-default-600">
|
||||||
确定要删除限速规则 <span className="font-semibold text-foreground">"{ruleToDelete?.name}"</span> 吗?
|
确定要删除限速规则{" "}
|
||||||
</p>
|
<span className="font-semibold text-foreground">
|
||||||
<p className="text-small text-default-500 mt-2">
|
"{ruleToDelete?.name}"
|
||||||
此操作无法撤销,删除后该规则将永久消失。
|
</span>{" "}
|
||||||
</p>
|
吗?
|
||||||
</ModalBody>
|
</p>
|
||||||
<ModalFooter>
|
<p className="text-small text-default-500 mt-2">
|
||||||
<Button variant="light" onPress={onClose}>
|
此操作无法撤销,删除后该规则将永久消失。
|
||||||
取消
|
</p>
|
||||||
</Button>
|
</ModalBody>
|
||||||
<Button
|
<ModalFooter>
|
||||||
color="danger"
|
<Button variant="light" onPress={onClose}>
|
||||||
onPress={confirmDelete}
|
取消
|
||||||
isLoading={deleteLoading}
|
</Button>
|
||||||
>
|
<Button
|
||||||
确认删除
|
color="danger"
|
||||||
</Button>
|
onPress={confirmDelete}
|
||||||
</ModalFooter>
|
isLoading={deleteLoading}
|
||||||
</>
|
>
|
||||||
)}
|
确认删除
|
||||||
</ModalContent>
|
</Button>
|
||||||
</Modal>
|
</ModalFooter>
|
||||||
</div>
|
</>
|
||||||
|
)}
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+1078
-820
File diff suppressed because it is too large
Load Diff
+201
-121
@@ -1,14 +1,22 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from "react";
|
||||||
import { Card, CardBody } from "@heroui/card";
|
import { Card, CardBody } from "@heroui/card";
|
||||||
import { Button } from "@heroui/button";
|
import { Button } from "@heroui/button";
|
||||||
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, useDisclosure } from "@heroui/modal";
|
import {
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
|
ModalHeader,
|
||||||
|
ModalBody,
|
||||||
|
ModalFooter,
|
||||||
|
useDisclosure,
|
||||||
|
} from "@heroui/modal";
|
||||||
import { Input } from "@heroui/input";
|
import { Input } from "@heroui/input";
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from "react-hot-toast";
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from "react-router-dom";
|
||||||
import { isWebViewFunc } from '@/utils/panel';
|
|
||||||
import { siteConfig } from '@/config/site';
|
import { isWebViewFunc } from "@/utils/panel";
|
||||||
import { updatePassword } from '@/api';
|
import { siteConfig } from "@/config/site";
|
||||||
import { safeLogout } from '@/utils/logout';
|
import { updatePassword } from "@/api";
|
||||||
|
import { safeLogout } from "@/utils/logout";
|
||||||
interface PasswordForm {
|
interface PasswordForm {
|
||||||
newUsername: string;
|
newUsername: string;
|
||||||
currentPassword: string;
|
currentPassword: string;
|
||||||
@@ -16,7 +24,6 @@ interface PasswordForm {
|
|||||||
confirmPassword: string;
|
confirmPassword: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
interface MenuItem {
|
interface MenuItem {
|
||||||
path: string;
|
path: string;
|
||||||
label: string;
|
label: string;
|
||||||
@@ -28,29 +35,31 @@ interface MenuItem {
|
|||||||
export default function ProfilePage() {
|
export default function ProfilePage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { isOpen, onOpen, onOpenChange } = useDisclosure();
|
const { isOpen, onOpen, onOpenChange } = useDisclosure();
|
||||||
const [username, setUsername] = useState('');
|
const [username, setUsername] = useState("");
|
||||||
const [isAdmin, setIsAdmin] = useState(false);
|
const [isAdmin, setIsAdmin] = useState(false);
|
||||||
const [passwordLoading, setPasswordLoading] = useState(false);
|
const [passwordLoading, setPasswordLoading] = useState(false);
|
||||||
const [passwordForm, setPasswordForm] = useState<PasswordForm>({
|
const [passwordForm, setPasswordForm] = useState<PasswordForm>({
|
||||||
newUsername: '',
|
newUsername: "",
|
||||||
currentPassword: '',
|
currentPassword: "",
|
||||||
newPassword: '',
|
newPassword: "",
|
||||||
confirmPassword: ''
|
confirmPassword: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 获取用户信息
|
// 获取用户信息
|
||||||
const name = localStorage.getItem('name') || 'Admin';
|
const name = localStorage.getItem("name") || "Admin";
|
||||||
|
|
||||||
// 兼容处理:如果没有admin字段,根据role_id判断(0为管理员)
|
// 兼容处理:如果没有admin字段,根据role_id判断(0为管理员)
|
||||||
let adminFlag = localStorage.getItem('admin') === 'true';
|
let adminFlag = localStorage.getItem("admin") === "true";
|
||||||
if (localStorage.getItem('admin') === null) {
|
|
||||||
const roleId = parseInt(localStorage.getItem('role_id') || '1', 10);
|
if (localStorage.getItem("admin") === null) {
|
||||||
|
const roleId = parseInt(localStorage.getItem("role_id") || "1", 10);
|
||||||
|
|
||||||
adminFlag = roleId === 0;
|
adminFlag = roleId === 0;
|
||||||
// 补充设置admin字段,避免下次再次判断
|
// 补充设置admin字段,避免下次再次判断
|
||||||
localStorage.setItem('admin', adminFlag.toString());
|
localStorage.setItem("admin", adminFlag.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
setUsername(name);
|
setUsername(name);
|
||||||
setIsAdmin(adminFlag);
|
setIsAdmin(adminFlag);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -58,72 +67,89 @@ export default function ProfilePage() {
|
|||||||
// 管理员菜单项
|
// 管理员菜单项
|
||||||
const adminMenuItems: MenuItem[] = [
|
const adminMenuItems: MenuItem[] = [
|
||||||
{
|
{
|
||||||
path: '/limit',
|
path: "/limit",
|
||||||
label: '限速管理',
|
label: "限速管理",
|
||||||
icon: (
|
icon: (
|
||||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clipRule="evenodd" />
|
<path
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z"
|
||||||
|
fillRule="evenodd"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
),
|
),
|
||||||
color: 'bg-orange-100 dark:bg-orange-500/20 text-orange-600 dark:text-orange-400',
|
color:
|
||||||
description: '管理用户限速策略'
|
"bg-orange-100 dark:bg-orange-500/20 text-orange-600 dark:text-orange-400",
|
||||||
|
description: "管理用户限速策略",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/user',
|
path: "/user",
|
||||||
label: '用户管理',
|
label: "用户管理",
|
||||||
icon: (
|
icon: (
|
||||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||||
<path d="M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" />
|
<path d="M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" />
|
||||||
</svg>
|
</svg>
|
||||||
),
|
),
|
||||||
color: 'bg-blue-100 dark:bg-blue-500/20 text-blue-600 dark:text-blue-400',
|
color: "bg-blue-100 dark:bg-blue-500/20 text-blue-600 dark:text-blue-400",
|
||||||
description: '管理系统用户'
|
description: "管理系统用户",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/config',
|
path: "/config",
|
||||||
label: '网站配置',
|
label: "网站配置",
|
||||||
icon: (
|
icon: (
|
||||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||||
<path fillRule="evenodd" d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z" clipRule="evenodd" />
|
<path
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z"
|
||||||
|
fillRule="evenodd"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
),
|
),
|
||||||
color: 'bg-purple-100 dark:bg-purple-500/20 text-purple-600 dark:text-purple-400',
|
color:
|
||||||
description: '配置网站设置'
|
"bg-purple-100 dark:bg-purple-500/20 text-purple-600 dark:text-purple-400",
|
||||||
}
|
description: "配置网站设置",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// 退出登录
|
// 退出登录
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
safeLogout();
|
safeLogout();
|
||||||
navigate('/', { replace: true });
|
navigate("/", { replace: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
// 密码表单验证
|
// 密码表单验证
|
||||||
const validatePasswordForm = (): boolean => {
|
const validatePasswordForm = (): boolean => {
|
||||||
if (!passwordForm.newUsername.trim()) {
|
if (!passwordForm.newUsername.trim()) {
|
||||||
toast.error('请输入新用户名');
|
toast.error("请输入新用户名");
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (passwordForm.newUsername.length < 3) {
|
if (passwordForm.newUsername.length < 3) {
|
||||||
toast.error('用户名长度至少3位');
|
toast.error("用户名长度至少3位");
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!passwordForm.currentPassword) {
|
if (!passwordForm.currentPassword) {
|
||||||
toast.error('请输入当前密码');
|
toast.error("请输入当前密码");
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!passwordForm.newPassword) {
|
if (!passwordForm.newPassword) {
|
||||||
toast.error('请输入新密码');
|
toast.error("请输入新密码");
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (passwordForm.newPassword.length < 6) {
|
if (passwordForm.newPassword.length < 6) {
|
||||||
toast.error('新密码长度不能少于6位');
|
toast.error("新密码长度不能少于6位");
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (passwordForm.newPassword !== passwordForm.confirmPassword) {
|
if (passwordForm.newPassword !== passwordForm.confirmPassword) {
|
||||||
toast.error('两次输入密码不一致');
|
toast.error("两次输入密码不一致");
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -134,16 +160,17 @@ export default function ProfilePage() {
|
|||||||
setPasswordLoading(true);
|
setPasswordLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await updatePassword(passwordForm);
|
const response = await updatePassword(passwordForm);
|
||||||
|
|
||||||
if (response.code === 0) {
|
if (response.code === 0) {
|
||||||
toast.success('密码修改成功,请重新登录');
|
toast.success("密码修改成功,请重新登录");
|
||||||
onOpenChange();
|
onOpenChange();
|
||||||
handleLogout();
|
handleLogout();
|
||||||
} else {
|
} else {
|
||||||
toast.error(response.msg || '密码修改失败');
|
toast.error(response.msg || "密码修改失败");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error('修改密码时发生错误');
|
toast.error("修改密码时发生错误");
|
||||||
console.error('修改密码错误:', error);
|
console.error("修改密码错误:", error);
|
||||||
} finally {
|
} finally {
|
||||||
setPasswordLoading(false);
|
setPasswordLoading(false);
|
||||||
}
|
}
|
||||||
@@ -152,38 +179,49 @@ export default function ProfilePage() {
|
|||||||
// 重置密码表单
|
// 重置密码表单
|
||||||
const resetPasswordForm = () => {
|
const resetPasswordForm = () => {
|
||||||
setPasswordForm({
|
setPasswordForm({
|
||||||
newUsername: '',
|
newUsername: "",
|
||||||
currentPassword: '',
|
currentPassword: "",
|
||||||
newPassword: '',
|
newPassword: "",
|
||||||
confirmPassword: ''
|
confirmPassword: "",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="px-3 lg:px-6 py-8 flex flex-col h-full">
|
<div className="px-3 lg:px-6 py-8 flex flex-col h-full">
|
||||||
|
|
||||||
<div className="space-y-6 flex-1">
|
<div className="space-y-6 flex-1">
|
||||||
{/* 用户信息卡片 */}
|
{/* 用户信息卡片 */}
|
||||||
<Card className="border border-gray-200 dark:border-default-200 shadow-md hover:shadow-lg transition-shadow">
|
<Card className="border border-gray-200 dark:border-default-200 shadow-md hover:shadow-lg transition-shadow">
|
||||||
<CardBody className="p-4">
|
<CardBody className="p-4">
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<div className="w-12 h-12 bg-primary-100 dark:bg-primary-900/30 rounded-full flex items-center justify-center">
|
<div className="w-12 h-12 bg-primary-100 dark:bg-primary-900/30 rounded-full flex items-center justify-center">
|
||||||
<svg className="w-6 h-6 text-primary" fill="currentColor" viewBox="0 0 20 20">
|
<svg
|
||||||
<path fillRule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clipRule="evenodd" />
|
className="w-6 h-6 text-primary"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z"
|
||||||
|
fillRule="evenodd"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<h3 className="text-base font-medium text-foreground">{username}</h3>
|
<h3 className="text-base font-medium text-foreground">
|
||||||
|
{username}
|
||||||
|
</h3>
|
||||||
<div className="flex items-center space-x-2 mt-1">
|
<div className="flex items-center space-x-2 mt-1">
|
||||||
<span className={`px-2 py-1 rounded-md text-xs font-medium ${
|
<span
|
||||||
isAdmin
|
className={`px-2 py-1 rounded-md text-xs font-medium ${
|
||||||
? 'bg-primary-100 dark:bg-primary-500/20 text-primary-700 dark:text-primary-300'
|
isAdmin
|
||||||
: 'bg-blue-100 dark:bg-blue-500/20 text-blue-700 dark:text-blue-300'
|
? "bg-primary-100 dark:bg-primary-500/20 text-primary-700 dark:text-primary-300"
|
||||||
}`}>
|
: "bg-blue-100 dark:bg-blue-500/20 text-blue-700 dark:text-blue-300"
|
||||||
{isAdmin ? '管理员' : '普通用户'}
|
}`}
|
||||||
|
>
|
||||||
|
{isAdmin ? "管理员" : "普通用户"}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs text-default-500">
|
<span className="text-xs text-default-500">
|
||||||
{new Date().toLocaleDateString('zh-CN')}
|
{new Date().toLocaleDateString("zh-CN")}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -196,119 +234,161 @@ export default function ProfilePage() {
|
|||||||
<CardBody className="p-4">
|
<CardBody className="p-4">
|
||||||
<div className="grid grid-cols-3 gap-3">
|
<div className="grid grid-cols-3 gap-3">
|
||||||
{/* 管理员功能 */}
|
{/* 管理员功能 */}
|
||||||
{isAdmin && adminMenuItems.map((item) => (
|
{isAdmin &&
|
||||||
<button
|
adminMenuItems.map((item) => (
|
||||||
key={item.path}
|
<button
|
||||||
onClick={() => navigate(item.path)}
|
key={item.path}
|
||||||
className="flex flex-col items-center p-3 rounded-2xl bg-gray-50 dark:bg-default-100 hover:bg-gray-100 dark:hover:bg-default-200 transition-colors duration-200"
|
className="flex flex-col items-center p-3 rounded-2xl bg-gray-50 dark:bg-default-100 hover:bg-gray-100 dark:hover:bg-default-200 transition-colors duration-200"
|
||||||
>
|
onClick={() => navigate(item.path)}
|
||||||
<div className={`w-10 h-10 ${item.color} rounded-full flex items-center justify-center mb-2`}>
|
>
|
||||||
{item.icon}
|
<div
|
||||||
</div>
|
className={`w-10 h-10 ${item.color} rounded-full flex items-center justify-center mb-2`}
|
||||||
<span className="text-xs text-foreground text-center">{item.label}</span>
|
>
|
||||||
</button>
|
{item.icon}
|
||||||
))}
|
</div>
|
||||||
|
<span className="text-xs text-foreground text-center">
|
||||||
|
{item.label}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
|
||||||
{/* 修改密码 */}
|
{/* 修改密码 */}
|
||||||
<button
|
<button
|
||||||
onClick={onOpen}
|
|
||||||
className="flex flex-col items-center p-3 rounded-2xl bg-gray-50 dark:bg-default-100 hover:bg-gray-100 dark:hover:bg-default-200 transition-colors duration-200"
|
className="flex flex-col items-center p-3 rounded-2xl bg-gray-50 dark:bg-default-100 hover:bg-gray-100 dark:hover:bg-default-200 transition-colors duration-200"
|
||||||
|
onClick={onOpen}
|
||||||
>
|
>
|
||||||
<div className="w-10 h-10 bg-blue-100 dark:bg-blue-500/20 text-blue-600 dark:text-blue-400 rounded-full flex items-center justify-center mb-2">
|
<div className="w-10 h-10 bg-blue-100 dark:bg-blue-500/20 text-blue-600 dark:text-blue-400 rounded-full flex items-center justify-center mb-2">
|
||||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
<svg
|
||||||
<path fillRule="evenodd" d="M18 8a6 6 0 01-7.743 5.743L10 14l-1 1-1 1H6v2H2v-4l4.257-4.257A6 6 0 1118 8zm-6-4a1 1 0 100 2 2 2 0 012 2 1 1 0 102 0 4 4 0 00-4-4z" clipRule="evenodd" />
|
className="w-5 h-5"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M18 8a6 6 0 01-7.743 5.743L10 14l-1 1-1 1H6v2H2v-4l4.257-4.257A6 6 0 1118 8zm-6-4a1 1 0 100 2 2 2 0 012 2 1 1 0 102 0 4 4 0 00-4-4z"
|
||||||
|
fillRule="evenodd"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs text-foreground text-center">修改密码</span>
|
<span className="text-xs text-foreground text-center">
|
||||||
|
修改密码
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* 退出登录 */}
|
{/* 退出登录 */}
|
||||||
<button
|
<button
|
||||||
onClick={handleLogout}
|
|
||||||
className="flex flex-col items-center p-3 rounded-2xl bg-gray-50 dark:bg-default-100 hover:bg-gray-100 dark:hover:bg-default-200 transition-colors duration-200"
|
className="flex flex-col items-center p-3 rounded-2xl bg-gray-50 dark:bg-default-100 hover:bg-gray-100 dark:hover:bg-default-200 transition-colors duration-200"
|
||||||
|
onClick={handleLogout}
|
||||||
>
|
>
|
||||||
<div className="w-10 h-10 bg-red-100 dark:bg-red-500/20 text-red-600 dark:text-red-400 rounded-full flex items-center justify-center mb-2">
|
<div className="w-10 h-10 bg-red-100 dark:bg-red-500/20 text-red-600 dark:text-red-400 rounded-full flex items-center justify-center mb-2">
|
||||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
<svg
|
||||||
<path fillRule="evenodd" d="M3 3a1 1 0 00-1 1v12a1 1 0 102 0V4a1 1 0 00-1-1zm10.293 9.293a1 1 0 001.414 1.414l3-3a1 1 0 000-1.414l-3-3a1 1 0 10-1.414 1.414L14.586 9H7a1 1 0 100 2h7.586l-1.293 1.293z" clipRule="evenodd" />
|
className="w-5 h-5"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M3 3a1 1 0 00-1 1v12a1 1 0 102 0V4a1 1 0 00-1-1zm10.293 9.293a1 1 0 001.414 1.414l3-3a1 1 0 000-1.414l-3-3a1 1 0 10-1.414 1.414L14.586 9H7a1 1 0 100 2h7.586l-1.293 1.293z"
|
||||||
|
fillRule="evenodd"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs text-foreground text-center">退出登录</span>
|
<span className="text-xs text-foreground text-center">
|
||||||
|
退出登录
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<div className="fixed inset-x-0 bottom-20 text-center py-4">
|
<div className="fixed inset-x-0 bottom-20 text-center py-4">
|
||||||
<p className="text-xs text-gray-400 dark:text-gray-500">
|
<p className="text-xs text-gray-400 dark:text-gray-500">
|
||||||
Powered by{' '}
|
Powered by{" "}
|
||||||
<a
|
<a
|
||||||
href={siteConfig.github_repo}
|
className="text-gray-500 dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
|
||||||
target="_blank"
|
href={siteConfig.github_repo}
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="text-gray-500 dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
|
target="_blank"
|
||||||
>
|
>
|
||||||
flux-panel
|
flux-panel
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-gray-400 dark:text-gray-500 mt-1">
|
<p className="text-xs text-gray-400 dark:text-gray-500 mt-1">
|
||||||
v{ isWebViewFunc() ? siteConfig.app_version : siteConfig.version}
|
v{isWebViewFunc() ? siteConfig.app_version : siteConfig.version}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{/* 修改密码弹窗 */}
|
{/* 修改密码弹窗 */}
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={isOpen}
|
backdrop="blur"
|
||||||
|
isOpen={isOpen}
|
||||||
|
placement="center"
|
||||||
|
scrollBehavior="outside"
|
||||||
|
size="2xl"
|
||||||
onOpenChange={() => {
|
onOpenChange={() => {
|
||||||
onOpenChange();
|
onOpenChange();
|
||||||
resetPasswordForm();
|
resetPasswordForm();
|
||||||
}}
|
}}
|
||||||
size="2xl"
|
|
||||||
scrollBehavior="outside"
|
|
||||||
backdrop="blur"
|
|
||||||
placement="center"
|
|
||||||
>
|
>
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
{(onClose: () => void) => (
|
{(onClose: () => void) => (
|
||||||
<>
|
<>
|
||||||
<ModalHeader className="flex flex-col gap-1">修改密码</ModalHeader>
|
<ModalHeader className="flex flex-col gap-1">
|
||||||
|
修改密码
|
||||||
|
</ModalHeader>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Input
|
<Input
|
||||||
label="新用户名"
|
label="新用户名"
|
||||||
placeholder="请输入新用户名(至少3位)"
|
placeholder="请输入新用户名(至少3位)"
|
||||||
value={passwordForm.newUsername}
|
value={passwordForm.newUsername}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setPasswordForm(prev => ({ ...prev, newUsername: e.target.value }))}
|
|
||||||
variant="bordered"
|
variant="bordered"
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
setPasswordForm((prev) => ({
|
||||||
|
...prev,
|
||||||
|
newUsername: e.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
label="当前密码"
|
label="当前密码"
|
||||||
type="password"
|
|
||||||
placeholder="请输入当前密码"
|
placeholder="请输入当前密码"
|
||||||
|
type="password"
|
||||||
value={passwordForm.currentPassword}
|
value={passwordForm.currentPassword}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setPasswordForm(prev => ({ ...prev, currentPassword: e.target.value }))}
|
|
||||||
variant="bordered"
|
variant="bordered"
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
setPasswordForm((prev) => ({
|
||||||
|
...prev,
|
||||||
|
currentPassword: e.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
label="新密码"
|
label="新密码"
|
||||||
type="password"
|
|
||||||
placeholder="请输入新密码(至少6位)"
|
placeholder="请输入新密码(至少6位)"
|
||||||
|
type="password"
|
||||||
value={passwordForm.newPassword}
|
value={passwordForm.newPassword}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setPasswordForm(prev => ({ ...prev, newPassword: e.target.value }))}
|
|
||||||
variant="bordered"
|
variant="bordered"
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
setPasswordForm((prev) => ({
|
||||||
|
...prev,
|
||||||
|
newPassword: e.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
label="确认密码"
|
label="确认密码"
|
||||||
type="password"
|
|
||||||
placeholder="请再次输入新密码"
|
placeholder="请再次输入新密码"
|
||||||
|
type="password"
|
||||||
value={passwordForm.confirmPassword}
|
value={passwordForm.confirmPassword}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setPasswordForm(prev => ({ ...prev, confirmPassword: e.target.value }))}
|
|
||||||
variant="bordered"
|
variant="bordered"
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
setPasswordForm((prev) => ({
|
||||||
|
...prev,
|
||||||
|
confirmPassword: e.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
@@ -316,10 +396,10 @@ export default function ProfilePage() {
|
|||||||
<Button color="default" variant="light" onPress={onClose}>
|
<Button color="default" variant="light" onPress={onClose}>
|
||||||
取消
|
取消
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
color="primary"
|
color="primary"
|
||||||
onPress={handlePasswordSubmit}
|
|
||||||
isLoading={passwordLoading}
|
isLoading={passwordLoading}
|
||||||
|
onPress={handlePasswordSubmit}
|
||||||
>
|
>
|
||||||
确定
|
确定
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,74 +1,77 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from "react";
|
||||||
import { Input } from "@heroui/input";
|
import { Input } from "@heroui/input";
|
||||||
import { Button } from "@heroui/button";
|
import { Button } from "@heroui/button";
|
||||||
import { Card, CardBody } from "@heroui/card";
|
import { Card, CardBody } from "@heroui/card";
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from "react-router-dom";
|
||||||
import toast from 'react-hot-toast';
|
import toast from "react-hot-toast";
|
||||||
import { reinitializeBaseURL } from '@/api/network';
|
|
||||||
import {
|
import { reinitializeBaseURL } from "@/api/network";
|
||||||
getPanelAddresses,
|
import {
|
||||||
savePanelAddress,
|
getPanelAddresses,
|
||||||
setCurrentPanelAddress,
|
savePanelAddress,
|
||||||
deletePanelAddress,
|
setCurrentPanelAddress,
|
||||||
|
deletePanelAddress,
|
||||||
validatePanelAddress,
|
validatePanelAddress,
|
||||||
} from '@/utils/panel';
|
} from "@/utils/panel";
|
||||||
|
|
||||||
interface PanelAddress {
|
interface PanelAddress {
|
||||||
name: string;
|
name: string;
|
||||||
address: string;
|
address: string;
|
||||||
inx: boolean;
|
inx: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export const SettingsPage = () => {
|
export const SettingsPage = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [panelAddresses, setPanelAddresses] = useState<PanelAddress[]>([]);
|
const [panelAddresses, setPanelAddresses] = useState<PanelAddress[]>([]);
|
||||||
const [newName, setNewName] = useState('');
|
const [newName, setNewName] = useState("");
|
||||||
const [newAddress, setNewAddress] = useState('');
|
const [newAddress, setNewAddress] = useState("");
|
||||||
|
|
||||||
|
|
||||||
const setPanelAddressesFunc = (newAddress: PanelAddress[]) => {
|
const setPanelAddressesFunc = (newAddress: PanelAddress[]) => {
|
||||||
setPanelAddresses(newAddress);
|
setPanelAddresses(newAddress);
|
||||||
}
|
};
|
||||||
|
|
||||||
// 加载面板地址列表
|
// 加载面板地址列表
|
||||||
const loadPanelAddresses = async () => {
|
const loadPanelAddresses = async () => {
|
||||||
(window as any).setPanelAddresses = setPanelAddressesFunc
|
(window as any).setPanelAddresses = setPanelAddressesFunc;
|
||||||
getPanelAddresses();
|
getPanelAddresses();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 添加新面板地址
|
// 添加新面板地址
|
||||||
const addPanelAddress = async () => {
|
const addPanelAddress = async () => {
|
||||||
if (!newName.trim() || !newAddress.trim()) {
|
if (!newName.trim() || !newAddress.trim()) {
|
||||||
toast.error('请输入名称和地址');
|
toast.error("请输入名称和地址");
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证地址格式
|
// 验证地址格式
|
||||||
if (!validatePanelAddress(newAddress.trim())) {
|
if (!validatePanelAddress(newAddress.trim())) {
|
||||||
toast.error('地址格式不正确,请检查:\n• 必须是完整的URL格式\n• 必须以 http:// 或 https:// 开头\n• 支持域名、IPv4、IPv6 地址\n• 端口号范围:1-65535\n• 示例:http://192.168.1.100:3000');
|
toast.error(
|
||||||
|
"地址格式不正确,请检查:\n• 必须是完整的URL格式\n• 必须以 http:// 或 https:// 开头\n• 支持域名、IPv4、IPv6 地址\n• 端口号范围:1-65535\n• 示例:http://192.168.1.100:3000",
|
||||||
|
);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
(window as any).setPanelAddresses = setPanelAddressesFunc
|
(window as any).setPanelAddresses = setPanelAddressesFunc;
|
||||||
savePanelAddress(newName.trim(), newAddress.trim());
|
savePanelAddress(newName.trim(), newAddress.trim());
|
||||||
setNewName('');
|
setNewName("");
|
||||||
setNewAddress('');
|
setNewAddress("");
|
||||||
toast.success('添加成功');
|
toast.success("添加成功");
|
||||||
};
|
};
|
||||||
|
|
||||||
// 设置当前面板地址
|
// 设置当前面板地址
|
||||||
const setCurrentPanel = async (name: string) => {
|
const setCurrentPanel = async (name: string) => {
|
||||||
(window as any).setPanelAddresses = setPanelAddressesFunc
|
(window as any).setPanelAddresses = setPanelAddressesFunc;
|
||||||
setCurrentPanelAddress(name);
|
setCurrentPanelAddress(name);
|
||||||
reinitializeBaseURL();
|
reinitializeBaseURL();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 删除面板地址
|
// 删除面板地址
|
||||||
const handleDeletePanelAddress = async (name: string) => {
|
const handleDeletePanelAddress = async (name: string) => {
|
||||||
(window as any).setPanelAddresses = setPanelAddressesFunc
|
(window as any).setPanelAddresses = setPanelAddressesFunc;
|
||||||
deletePanelAddress(name);
|
deletePanelAddress(name);
|
||||||
reinitializeBaseURL();
|
reinitializeBaseURL();
|
||||||
toast.success('删除成功');
|
toast.success("删除成功");
|
||||||
};
|
};
|
||||||
|
|
||||||
// 页面加载时获取数据
|
// 页面加载时获取数据
|
||||||
@@ -84,15 +87,27 @@ export const SettingsPage = () => {
|
|||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Button
|
<Button
|
||||||
isIconOnly
|
isIconOnly
|
||||||
|
className="text-gray-600 dark:text-gray-300"
|
||||||
variant="light"
|
variant="light"
|
||||||
onClick={() => navigate(-1)}
|
onClick={() => navigate(-1)}
|
||||||
className="text-gray-600 dark:text-gray-300"
|
|
||||||
>
|
>
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
className="w-5 h-5"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M15 19l-7-7 7-7"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</Button>
|
</Button>
|
||||||
<h1 className="text-xl font-semibold text-gray-900 dark:text-white">面板设置</h1>
|
<h1 className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||||
|
面板设置
|
||||||
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -103,7 +118,9 @@ export const SettingsPage = () => {
|
|||||||
{/* 添加新地址 */}
|
{/* 添加新地址 */}
|
||||||
<Card className="border border-gray-200 dark:border-gray-700">
|
<Card className="border border-gray-200 dark:border-gray-700">
|
||||||
<CardBody className="p-6">
|
<CardBody className="p-6">
|
||||||
<h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4">添加新面板地址</h2>
|
<h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
|
||||||
|
添加新面板地址
|
||||||
|
</h2>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
<Input
|
<Input
|
||||||
@@ -129,30 +146,41 @@ export const SettingsPage = () => {
|
|||||||
{/* 地址列表 */}
|
{/* 地址列表 */}
|
||||||
<Card className="border border-gray-200 dark:border-gray-700">
|
<Card className="border border-gray-200 dark:border-gray-700">
|
||||||
<CardBody className="p-6">
|
<CardBody className="p-6">
|
||||||
<h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4">已保存的面板地址</h2>
|
<h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
|
||||||
|
已保存的面板地址
|
||||||
|
</h2>
|
||||||
{panelAddresses.length === 0 ? (
|
{panelAddresses.length === 0 ? (
|
||||||
<p className="text-gray-500 dark:text-gray-400 text-center py-8">暂无保存的面板地址</p>
|
<p className="text-gray-500 dark:text-gray-400 text-center py-8">
|
||||||
|
暂无保存的面板地址
|
||||||
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{panelAddresses.map((panel, index) => (
|
{panelAddresses.map((panel, index) => (
|
||||||
<div key={index} className="border border-gray-200 dark:border-gray-600 rounded-lg p-4">
|
<div
|
||||||
|
key={index}
|
||||||
|
className="border border-gray-200 dark:border-gray-600 rounded-lg p-4"
|
||||||
|
>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="font-medium text-gray-900 dark:text-white">{panel.name}</span>
|
<span className="font-medium text-gray-900 dark:text-white">
|
||||||
|
{panel.name}
|
||||||
|
</span>
|
||||||
{panel.inx && (
|
{panel.inx && (
|
||||||
<span className="px-2 py-1 bg-green-100 dark:bg-green-500/20 text-green-700 dark:text-green-300 text-xs rounded">
|
<span className="px-2 py-1 bg-green-100 dark:bg-green-500/20 text-green-700 dark:text-green-300 text-xs rounded">
|
||||||
当前
|
当前
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">{panel.address}</p>
|
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||||
|
{panel.address}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{!panel.inx && (
|
{!panel.inx && (
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
|
||||||
color="primary"
|
color="primary"
|
||||||
|
size="sm"
|
||||||
variant="flat"
|
variant="flat"
|
||||||
onClick={() => setCurrentPanel(panel.name)}
|
onClick={() => setCurrentPanel(panel.name)}
|
||||||
>
|
>
|
||||||
@@ -160,8 +188,8 @@ export const SettingsPage = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
|
||||||
color="danger"
|
color="danger"
|
||||||
|
size="sm"
|
||||||
variant="light"
|
variant="light"
|
||||||
onClick={() => handleDeletePanelAddress(panel.name)}
|
onClick={() => handleDeletePanelAddress(panel.name)}
|
||||||
>
|
>
|
||||||
|
|||||||
+1782
-1239
File diff suppressed because it is too large
Load Diff
+714
-427
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,13 @@
|
|||||||
import type { NavigateOptions } from "react-router-dom";
|
import type { NavigateOptions } from "react-router-dom";
|
||||||
import * as React from "react";
|
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
import { HeroUIProvider } from "@heroui/system";
|
import { HeroUIProvider } from "@heroui/system";
|
||||||
import { useHref, useNavigate } from "react-router-dom";
|
import { useHref, useNavigate } from "react-router-dom";
|
||||||
import { Toaster } from 'react-hot-toast';
|
import { Toaster } from "react-hot-toast";
|
||||||
import { ThemeProvider } from '@/components/theme-provider';
|
|
||||||
import { I18nProvider } from "@react-aria/i18n";
|
import { I18nProvider } from "@react-aria/i18n";
|
||||||
|
|
||||||
|
import { ThemeProvider } from "@/components/theme-provider";
|
||||||
|
|
||||||
declare module "@react-types/shared" {
|
declare module "@react-types/shared" {
|
||||||
interface RouterConfig {
|
interface RouterConfig {
|
||||||
routerOptions: NavigateOptions;
|
routerOptions: NavigateOptions;
|
||||||
@@ -25,28 +26,28 @@ export function Provider({ children }: ProvidersProps) {
|
|||||||
<HeroUIProvider navigate={navigate} useHref={useHref}>
|
<HeroUIProvider navigate={navigate} useHref={useHref}>
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
{children}
|
{children}
|
||||||
<Toaster
|
<Toaster
|
||||||
position="top-center"
|
position="top-center"
|
||||||
toastOptions={{
|
toastOptions={{
|
||||||
duration: 2000,
|
duration: 2000,
|
||||||
className: 'dark:bg-gray-800 dark:text-white',
|
className: "dark:bg-gray-800 dark:text-white",
|
||||||
style: {
|
style: {
|
||||||
background: 'var(--toaster-bg, #ffffff)',
|
background: "var(--toaster-bg, #ffffff)",
|
||||||
color: 'var(--toaster-color, #000000)',
|
color: "var(--toaster-color, #000000)",
|
||||||
border: '1px solid var(--toaster-border, #e5e7eb)',
|
border: "1px solid var(--toaster-border, #e5e7eb)",
|
||||||
},
|
},
|
||||||
success: {
|
success: {
|
||||||
duration: 2000,
|
duration: 2000,
|
||||||
style: {
|
style: {
|
||||||
background: '#10b981',
|
background: "#10b981",
|
||||||
color: '#ffffff',
|
color: "#ffffff",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
error: {
|
error: {
|
||||||
duration: 2000,
|
duration: 2000,
|
||||||
style: {
|
style: {
|
||||||
background: '#ef4444',
|
background: "#ef4444",
|
||||||
color: '#ffffff',
|
color: "#ffffff",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
|||||||
+5
-3
@@ -1,5 +1,5 @@
|
|||||||
declare module 'recharts' {
|
declare module "recharts" {
|
||||||
import * as React from 'react';
|
import * as React from "react";
|
||||||
|
|
||||||
export const LineChart: React.ComponentType<Record<string, unknown>>;
|
export const LineChart: React.ComponentType<Record<string, unknown>>;
|
||||||
export const Line: React.ComponentType<Record<string, unknown>>;
|
export const Line: React.ComponentType<Record<string, unknown>>;
|
||||||
@@ -7,5 +7,7 @@ declare module 'recharts' {
|
|||||||
export const YAxis: React.ComponentType<Record<string, unknown>>;
|
export const YAxis: React.ComponentType<Record<string, unknown>>;
|
||||||
export const CartesianGrid: React.ComponentType<Record<string, unknown>>;
|
export const CartesianGrid: React.ComponentType<Record<string, unknown>>;
|
||||||
export const Tooltip: React.ComponentType<Record<string, unknown>>;
|
export const Tooltip: React.ComponentType<Record<string, unknown>>;
|
||||||
export const ResponsiveContainer: React.ComponentType<Record<string, unknown>>;
|
export const ResponsiveContainer: React.ComponentType<
|
||||||
|
Record<string, unknown>
|
||||||
|
>;
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+1
-1
@@ -44,7 +44,7 @@ interface CaptchaStyle {
|
|||||||
|
|
||||||
declare class TAC {
|
declare class TAC {
|
||||||
constructor(config: CaptchaConfig, style?: CaptchaStyle);
|
constructor(config: CaptchaConfig, style?: CaptchaStyle);
|
||||||
|
|
||||||
init(): TAC;
|
init(): TAC;
|
||||||
reloadCaptcha(): void;
|
reloadCaptcha(): void;
|
||||||
destroyWindow(): void;
|
destroyWindow(): void;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getRoleIdFromToken, isTokenValid } from './jwt';
|
import { getRoleIdFromToken, isTokenValid } from "./jwt";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 权限工具类
|
* 权限工具类
|
||||||
@@ -9,7 +9,7 @@ import { getRoleIdFromToken, isTokenValid } from './jwt';
|
|||||||
* @returns token
|
* @returns token
|
||||||
*/
|
*/
|
||||||
export function getToken(): string | null {
|
export function getToken(): string | null {
|
||||||
return localStorage.getItem('token');
|
return localStorage.getItem("token");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -18,9 +18,11 @@ export function getToken(): string | null {
|
|||||||
*/
|
*/
|
||||||
export function getCurrentUserRoleId(): number | null {
|
export function getCurrentUserRoleId(): number | null {
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
|
|
||||||
if (!token || !isTokenValid(token)) {
|
if (!token || !isTokenValid(token)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return getRoleIdFromToken(token);
|
return getRoleIdFromToken(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,6 +32,7 @@ export function getCurrentUserRoleId(): number | null {
|
|||||||
*/
|
*/
|
||||||
export function isAdmin(): boolean {
|
export function isAdmin(): boolean {
|
||||||
const roleId = getCurrentUserRoleId();
|
const roleId = getCurrentUserRoleId();
|
||||||
|
|
||||||
return roleId === 0;
|
return roleId === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,6 +43,7 @@ export function isAdmin(): boolean {
|
|||||||
*/
|
*/
|
||||||
export function hasRole(targetRoleId: number): boolean {
|
export function hasRole(targetRoleId: number): boolean {
|
||||||
const roleId = getCurrentUserRoleId();
|
const roleId = getCurrentUserRoleId();
|
||||||
|
|
||||||
return roleId === targetRoleId;
|
return roleId === targetRoleId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,6 +53,7 @@ export function hasRole(targetRoleId: number): boolean {
|
|||||||
*/
|
*/
|
||||||
export function isLoggedIn(): boolean {
|
export function isLoggedIn(): boolean {
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
|
|
||||||
return token ? isTokenValid(token) : false;
|
return token ? isTokenValid(token) : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,14 +64,16 @@ export function isLoggedIn(): boolean {
|
|||||||
* @returns 包装后的函数
|
* @returns 包装后的函数
|
||||||
*/
|
*/
|
||||||
export function requireAdmin<T extends (...args: any[]) => any>(
|
export function requireAdmin<T extends (...args: any[]) => any>(
|
||||||
fn: T,
|
fn: T,
|
||||||
errorMsg: string = '权限不足,仅管理员可操作'
|
errorMsg: string = "权限不足,仅管理员可操作",
|
||||||
): T {
|
): T {
|
||||||
return ((...args: Parameters<T>) => {
|
return ((...args: Parameters<T>) => {
|
||||||
if (!isAdmin()) {
|
if (!isAdmin()) {
|
||||||
console.warn(errorMsg);
|
console.warn(errorMsg);
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return fn(...args);
|
return fn(...args);
|
||||||
}) as T;
|
}) as T;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,12 +18,14 @@ interface JWTPayload {
|
|||||||
function getPayloadFromToken(token: string): JWTPayload | null {
|
function getPayloadFromToken(token: string): JWTPayload | null {
|
||||||
try {
|
try {
|
||||||
if (!token) return null;
|
if (!token) return null;
|
||||||
|
|
||||||
const parts = token.split('.');
|
const parts = token.split(".");
|
||||||
|
|
||||||
if (parts.length !== 3) return null;
|
if (parts.length !== 3) return null;
|
||||||
|
|
||||||
const encodedPayload = parts[1];
|
const encodedPayload = parts[1];
|
||||||
const decodedPayload = atob(encodedPayload);
|
const decodedPayload = atob(encodedPayload);
|
||||||
|
|
||||||
return JSON.parse(decodedPayload) as JWTPayload;
|
return JSON.parse(decodedPayload) as JWTPayload;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return null;
|
return null;
|
||||||
@@ -37,6 +39,7 @@ function getPayloadFromToken(token: string): JWTPayload | null {
|
|||||||
*/
|
*/
|
||||||
export function getUserIdFromToken(token: string): number | null {
|
export function getUserIdFromToken(token: string): number | null {
|
||||||
const payload = getPayloadFromToken(token);
|
const payload = getPayloadFromToken(token);
|
||||||
|
|
||||||
return payload ? parseInt(payload.sub) : null;
|
return payload ? parseInt(payload.sub) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,6 +50,7 @@ export function getUserIdFromToken(token: string): number | null {
|
|||||||
*/
|
*/
|
||||||
export function getRoleIdFromToken(token: string): number | null {
|
export function getRoleIdFromToken(token: string): number | null {
|
||||||
const payload = getPayloadFromToken(token);
|
const payload = getPayloadFromToken(token);
|
||||||
|
|
||||||
return payload ? payload.role_id : null;
|
return payload ? payload.role_id : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,6 +61,7 @@ export function getRoleIdFromToken(token: string): number | null {
|
|||||||
*/
|
*/
|
||||||
export function getUsernameFromToken(token: string): string | null {
|
export function getUsernameFromToken(token: string): string | null {
|
||||||
const payload = getPayloadFromToken(token);
|
const payload = getPayloadFromToken(token);
|
||||||
|
|
||||||
return payload ? payload.user : null;
|
return payload ? payload.user : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,9 +72,11 @@ export function getUsernameFromToken(token: string): string | null {
|
|||||||
*/
|
*/
|
||||||
export function isTokenValid(token: string): boolean {
|
export function isTokenValid(token: string): boolean {
|
||||||
const payload = getPayloadFromToken(token);
|
const payload = getPayloadFromToken(token);
|
||||||
|
|
||||||
if (!payload) return false;
|
if (!payload) return false;
|
||||||
|
|
||||||
const now = Math.floor(Date.now() / 1000);
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
return payload.exp > now;
|
return payload.exp > now;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,34 +87,38 @@ export const JwtUtil = {
|
|||||||
* @returns 用户ID
|
* @returns 用户ID
|
||||||
*/
|
*/
|
||||||
getUserIdFromToken(): number | null {
|
getUserIdFromToken(): number | null {
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem("token");
|
||||||
|
|
||||||
return token ? getUserIdFromToken(token) : null;
|
return token ? getUserIdFromToken(token) : null;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 从localStorage获取token并解析角色ID
|
* 从localStorage获取token并解析角色ID
|
||||||
* @returns 角色ID
|
* @returns 角色ID
|
||||||
*/
|
*/
|
||||||
getRoleIdFromToken(): number | null {
|
getRoleIdFromToken(): number | null {
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem("token");
|
||||||
|
|
||||||
return token ? getRoleIdFromToken(token) : null;
|
return token ? getRoleIdFromToken(token) : null;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 从localStorage获取token并解析用户名
|
* 从localStorage获取token并解析用户名
|
||||||
* @returns 用户名
|
* @returns 用户名
|
||||||
*/
|
*/
|
||||||
getUsernameFromToken(): string | null {
|
getUsernameFromToken(): string | null {
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem("token");
|
||||||
|
|
||||||
return token ? getUsernameFromToken(token) : null;
|
return token ? getUsernameFromToken(token) : null;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 验证localStorage中的token是否有效
|
* 验证localStorage中的token是否有效
|
||||||
* @returns 是否有效
|
* @returns 是否有效
|
||||||
*/
|
*/
|
||||||
isTokenValid(): boolean {
|
isTokenValid(): boolean {
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem("token");
|
||||||
|
|
||||||
return token ? isTokenValid(token) : false;
|
return token ? isTokenValid(token) : false;
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,4 +4,4 @@
|
|||||||
*/
|
*/
|
||||||
export const safeLogout = () => {
|
export const safeLogout = () => {
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,49 +1,66 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 获取面板地址列表
|
// 获取面板地址列表
|
||||||
export async function getPanelAddresses(callback: string = "setPanelAddresses"){
|
export async function getPanelAddresses(
|
||||||
if ((window as any).JsInterface && (window as any).JsInterface.getPanelAddresses) {
|
callback: string = "setPanelAddresses",
|
||||||
(window as any).JsInterface.getPanelAddresses(callback);
|
) {
|
||||||
} else if ((window as any).webkit && (window as any).webkit.messageHandlers) {
|
if (
|
||||||
(window as any).webkit.messageHandlers.getPanelAddresses.postMessage(callback);
|
(window as any).JsInterface &&
|
||||||
}
|
(window as any).JsInterface.getPanelAddresses
|
||||||
|
) {
|
||||||
|
(window as any).JsInterface.getPanelAddresses(callback);
|
||||||
|
} else if ((window as any).webkit && (window as any).webkit.messageHandlers) {
|
||||||
|
(window as any).webkit.messageHandlers.getPanelAddresses.postMessage(
|
||||||
|
callback,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存面板地址
|
// 保存面板地址
|
||||||
export async function savePanelAddress(name: string, address: string){
|
export async function savePanelAddress(name: string, address: string) {
|
||||||
if ((window as any).JsInterface) {
|
if ((window as any).JsInterface) {
|
||||||
(window as any).JsInterface.savePanelAddress(name, address);
|
(window as any).JsInterface.savePanelAddress(name, address);
|
||||||
} else if ((window as any).webkit && (window as any).webkit.messageHandlers) {
|
} else if ((window as any).webkit && (window as any).webkit.messageHandlers) {
|
||||||
(window as any).webkit.messageHandlers.savePanelAddress.postMessage({ name, address });
|
(window as any).webkit.messageHandlers.savePanelAddress.postMessage({
|
||||||
}
|
name,
|
||||||
|
address,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置当前面板地址
|
// 设置当前面板地址
|
||||||
export async function setCurrentPanelAddress(name: string) {
|
export async function setCurrentPanelAddress(name: string) {
|
||||||
if ((window as any).JsInterface) {
|
if ((window as any).JsInterface) {
|
||||||
(window as any).JsInterface.setCurrentPanelAddress(name);
|
(window as any).JsInterface.setCurrentPanelAddress(name);
|
||||||
} else if ((window as any).webkit && (window as any).webkit.messageHandlers) {
|
} else if ((window as any).webkit && (window as any).webkit.messageHandlers) {
|
||||||
(window as any).webkit.messageHandlers.setCurrentPanelAddress.postMessage({ name });
|
(window as any).webkit.messageHandlers.setCurrentPanelAddress.postMessage({
|
||||||
}
|
name,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除面板地址
|
// 删除面板地址
|
||||||
export async function deletePanelAddress(name: string){
|
export async function deletePanelAddress(name: string) {
|
||||||
if ((window as any).JsInterface) {
|
if ((window as any).JsInterface) {
|
||||||
(window as any).JsInterface.deletePanelAddress(name);
|
(window as any).JsInterface.deletePanelAddress(name);
|
||||||
} else if ((window as any).webkit && (window as any).webkit.messageHandlers) {
|
} else if ((window as any).webkit && (window as any).webkit.messageHandlers) {
|
||||||
(window as any).webkit.messageHandlers.deletePanelAddress.postMessage({ name });
|
(window as any).webkit.messageHandlers.deletePanelAddress.postMessage({
|
||||||
}
|
name,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isWebViewFunc(){
|
export function isWebViewFunc() {
|
||||||
if((window as any).JsInterface !== undefined && (window as any).JsInterface.getPanelAddresses !== undefined) {
|
if (
|
||||||
|
(window as any).JsInterface !== undefined &&
|
||||||
|
(window as any).JsInterface.getPanelAddresses !== undefined
|
||||||
|
) {
|
||||||
return true;
|
return true;
|
||||||
}else if((window as any).webkit && (window as any).webkit.messageHandlers && (window as any).webkit.messageHandlers.getPanelAddresses !== undefined) {
|
} else if (
|
||||||
|
(window as any).webkit &&
|
||||||
|
(window as any).webkit.messageHandlers &&
|
||||||
|
(window as any).webkit.messageHandlers.getPanelAddresses !== undefined
|
||||||
|
) {
|
||||||
return true;
|
return true;
|
||||||
}else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -52,52 +69,58 @@ export function isWebViewFunc(){
|
|||||||
export function validatePanelAddress(address: string): boolean {
|
export function validatePanelAddress(address: string): boolean {
|
||||||
try {
|
try {
|
||||||
// 基本格式检查:必须以 http:// 或 https:// 开头
|
// 基本格式检查:必须以 http:// 或 https:// 开头
|
||||||
if (!address.startsWith('http://') && !address.startsWith('https://')) {
|
if (!address.startsWith("http://") && !address.startsWith("https://")) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用URL构造函数验证完整URL格式
|
// 使用URL构造函数验证完整URL格式
|
||||||
const url = new URL(address);
|
const url = new URL(address);
|
||||||
|
|
||||||
// 检查主机名不能为空
|
// 检查主机名不能为空
|
||||||
if (!url.hostname || url.hostname.trim() === '') {
|
if (!url.hostname || url.hostname.trim() === "") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查主机名
|
// 检查主机名
|
||||||
const hostname = url.hostname;
|
const hostname = url.hostname;
|
||||||
|
|
||||||
// 支持 localhost
|
// 支持 localhost
|
||||||
if (hostname === 'localhost') {
|
if (hostname === "localhost") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 支持 IPv4 地址
|
// 支持 IPv4 地址
|
||||||
const ipv4Pattern = /^(\d{1,3}\.){3}\d{1,3}$/;
|
const ipv4Pattern = /^(\d{1,3}\.){3}\d{1,3}$/;
|
||||||
|
|
||||||
if (ipv4Pattern.test(hostname)) {
|
if (ipv4Pattern.test(hostname)) {
|
||||||
const parts = hostname.split('.');
|
const parts = hostname.split(".");
|
||||||
return parts.every(part => {
|
|
||||||
|
return parts.every((part) => {
|
||||||
const num = parseInt(part);
|
const num = parseInt(part);
|
||||||
|
|
||||||
return num >= 0 && num <= 255;
|
return num >= 0 && num <= 255;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 支持 IPv6 地址
|
// 支持 IPv6 地址
|
||||||
const ipv6Pattern = /^\[([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\]$|^\[([0-9a-fA-F]{1,4}:)*:([0-9a-fA-F]{1,4}:)*[0-9a-fA-F]{1,4}\]$/;
|
const ipv6Pattern =
|
||||||
|
/^\[([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\]$|^\[([0-9a-fA-F]{1,4}:)*:([0-9a-fA-F]{1,4}:)*[0-9a-fA-F]{1,4}\]$/;
|
||||||
|
|
||||||
if (ipv6Pattern.test(hostname)) {
|
if (ipv6Pattern.test(hostname)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 支持域名
|
// 支持域名
|
||||||
const domainPattern = /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*\.[a-zA-Z]{2,}$/;
|
const domainPattern =
|
||||||
|
/^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*\.[a-zA-Z]{2,}$/;
|
||||||
|
|
||||||
if (domainPattern.test(hostname)) {
|
if (domainPattern.test(hostname)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// URL构造函数失败说明格式不正确
|
// URL构造函数失败说明格式不正确
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import { defineConfig } from "vite";
|
|
||||||
import react from "@vitejs/plugin-react";
|
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
||||||
|
import { defineConfig } from "vite";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [react()],
|
||||||
react(),
|
base: "/",
|
||||||
],
|
|
||||||
base: '/',
|
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
"@": path.resolve(__dirname, "./src"),
|
"@": path.resolve(__dirname, "./src"),
|
||||||
@@ -14,14 +13,14 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
port: 3000,
|
port: 3000,
|
||||||
host: '0.0.0.0'
|
host: "0.0.0.0",
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
outDir: 'dist',
|
outDir: "dist",
|
||||||
sourcemap: false,
|
sourcemap: false,
|
||||||
minify: false,
|
minify: false,
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
treeshake: false,
|
treeshake: false,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user