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:
root
2026-02-04 02:22:28 +00:00
parent 72c2e28667
commit 6f6fececa8
31 changed files with 8080 additions and 5740 deletions
+201 -121
View File
@@ -1,14 +1,22 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect } from "react";
import { Card, CardBody } from "@heroui/card";
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 { toast } from 'react-hot-toast';
import { useNavigate } from 'react-router-dom';
import { isWebViewFunc } from '@/utils/panel';
import { siteConfig } from '@/config/site';
import { updatePassword } from '@/api';
import { safeLogout } from '@/utils/logout';
import { toast } from "react-hot-toast";
import { useNavigate } from "react-router-dom";
import { isWebViewFunc } from "@/utils/panel";
import { siteConfig } from "@/config/site";
import { updatePassword } from "@/api";
import { safeLogout } from "@/utils/logout";
interface PasswordForm {
newUsername: string;
currentPassword: string;
@@ -16,7 +24,6 @@ interface PasswordForm {
confirmPassword: string;
}
interface MenuItem {
path: string;
label: string;
@@ -28,29 +35,31 @@ interface MenuItem {
export default function ProfilePage() {
const navigate = useNavigate();
const { isOpen, onOpen, onOpenChange } = useDisclosure();
const [username, setUsername] = useState('');
const [username, setUsername] = useState("");
const [isAdmin, setIsAdmin] = useState(false);
const [passwordLoading, setPasswordLoading] = useState(false);
const [passwordForm, setPasswordForm] = useState<PasswordForm>({
newUsername: '',
currentPassword: '',
newPassword: '',
confirmPassword: ''
newUsername: "",
currentPassword: "",
newPassword: "",
confirmPassword: "",
});
useEffect(() => {
// 获取用户信息
const name = localStorage.getItem('name') || 'Admin';
const name = localStorage.getItem("name") || "Admin";
// 兼容处理:如果没有admin字段,根据role_id判断(0为管理员)
let adminFlag = localStorage.getItem('admin') === 'true';
if (localStorage.getItem('admin') === null) {
const roleId = parseInt(localStorage.getItem('role_id') || '1', 10);
let adminFlag = localStorage.getItem("admin") === "true";
if (localStorage.getItem("admin") === null) {
const roleId = parseInt(localStorage.getItem("role_id") || "1", 10);
adminFlag = roleId === 0;
// 补充设置admin字段,避免下次再次判断
localStorage.setItem('admin', adminFlag.toString());
localStorage.setItem("admin", adminFlag.toString());
}
setUsername(name);
setIsAdmin(adminFlag);
}, []);
@@ -58,72 +67,89 @@ export default function ProfilePage() {
// 管理员菜单项
const adminMenuItems: MenuItem[] = [
{
path: '/limit',
label: '限速管理',
path: "/limit",
label: "限速管理",
icon: (
<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>
),
color: 'bg-orange-100 dark:bg-orange-500/20 text-orange-600 dark:text-orange-400',
description: '管理用户限速策略'
color:
"bg-orange-100 dark:bg-orange-500/20 text-orange-600 dark:text-orange-400",
description: "管理用户限速策略",
},
{
path: '/user',
label: '用户管理',
path: "/user",
label: "用户管理",
icon: (
<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" />
</svg>
),
color: 'bg-blue-100 dark:bg-blue-500/20 text-blue-600 dark:text-blue-400',
description: '管理系统用户'
color: "bg-blue-100 dark:bg-blue-500/20 text-blue-600 dark:text-blue-400",
description: "管理系统用户",
},
{
path: '/config',
label: '网站配置',
path: "/config",
label: "网站配置",
icon: (
<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>
),
color: 'bg-purple-100 dark:bg-purple-500/20 text-purple-600 dark:text-purple-400',
description: '配置网站设置'
}
color:
"bg-purple-100 dark:bg-purple-500/20 text-purple-600 dark:text-purple-400",
description: "配置网站设置",
},
];
// 退出登录
const handleLogout = () => {
safeLogout();
navigate('/', { replace: true });
navigate("/", { replace: true });
};
// 密码表单验证
const validatePasswordForm = (): boolean => {
if (!passwordForm.newUsername.trim()) {
toast.error('请输入新用户名');
toast.error("请输入新用户名");
return false;
}
if (passwordForm.newUsername.length < 3) {
toast.error('用户名长度至少3位');
toast.error("用户名长度至少3位");
return false;
}
if (!passwordForm.currentPassword) {
toast.error('请输入当前密码');
toast.error("请输入当前密码");
return false;
}
if (!passwordForm.newPassword) {
toast.error('请输入新密码');
toast.error("请输入新密码");
return false;
}
if (passwordForm.newPassword.length < 6) {
toast.error('新密码长度不能少于6位');
toast.error("新密码长度不能少于6位");
return false;
}
if (passwordForm.newPassword !== passwordForm.confirmPassword) {
toast.error('两次输入密码不一致');
toast.error("两次输入密码不一致");
return false;
}
return true;
};
@@ -134,16 +160,17 @@ export default function ProfilePage() {
setPasswordLoading(true);
try {
const response = await updatePassword(passwordForm);
if (response.code === 0) {
toast.success('密码修改成功,请重新登录');
toast.success("密码修改成功,请重新登录");
onOpenChange();
handleLogout();
} else {
toast.error(response.msg || '密码修改失败');
toast.error(response.msg || "密码修改失败");
}
} catch (error) {
toast.error('修改密码时发生错误');
console.error('修改密码错误:', error);
toast.error("修改密码时发生错误");
console.error("修改密码错误:", error);
} finally {
setPasswordLoading(false);
}
@@ -152,38 +179,49 @@ export default function ProfilePage() {
// 重置密码表单
const resetPasswordForm = () => {
setPasswordForm({
newUsername: '',
currentPassword: '',
newPassword: '',
confirmPassword: ''
newUsername: "",
currentPassword: "",
newPassword: "",
confirmPassword: "",
});
};
return (
<div className="px-3 lg:px-6 py-8 flex flex-col h-full">
<div className="space-y-6 flex-1">
{/* 用户信息卡片 */}
<Card className="border border-gray-200 dark:border-default-200 shadow-md hover:shadow-lg transition-shadow">
<CardBody className="p-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">
<svg className="w-6 h-6 text-primary" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clipRule="evenodd" />
<svg
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>
</div>
<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">
<span className={`px-2 py-1 rounded-md text-xs font-medium ${
isAdmin
? '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 ? '管理员' : '普通用户'}
<span
className={`px-2 py-1 rounded-md text-xs font-medium ${
isAdmin
? "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 ? "管理员" : "普通用户"}
</span>
<span className="text-xs text-default-500">
{new Date().toLocaleDateString('zh-CN')}
{new Date().toLocaleDateString("zh-CN")}
</span>
</div>
</div>
@@ -196,119 +234,161 @@ export default function ProfilePage() {
<CardBody className="p-4">
<div className="grid grid-cols-3 gap-3">
{/* 管理员功能 */}
{isAdmin && adminMenuItems.map((item) => (
<button
key={item.path}
onClick={() => navigate(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"
>
<div className={`w-10 h-10 ${item.color} rounded-full flex items-center justify-center mb-2`}>
{item.icon}
</div>
<span className="text-xs text-foreground text-center">{item.label}</span>
</button>
))}
{isAdmin &&
adminMenuItems.map((item) => (
<button
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"
onClick={() => navigate(item.path)}
>
<div
className={`w-10 h-10 ${item.color} rounded-full flex items-center justify-center mb-2`}
>
{item.icon}
</div>
<span className="text-xs text-foreground text-center">
{item.label}
</span>
</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"
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">
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<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" />
<svg
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>
</div>
<span className="text-xs text-foreground text-center">修改密码</span>
<span className="text-xs text-foreground text-center">
修改密码
</span>
</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"
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">
<svg className="w-5 h-5" 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="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>
</div>
<span className="text-xs text-foreground text-center">退出登录</span>
<span className="text-xs text-foreground text-center">
退出登录
</span>
</button>
</div>
</CardBody>
</Card>
<div className="fixed inset-x-0 bottom-20 text-center py-4">
<p className="text-xs text-gray-400 dark:text-gray-500">
Powered by{' '}
<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"
>
flux-panel
</a>
</p>
<p className="text-xs text-gray-400 dark:text-gray-500 mt-1">
v{ isWebViewFunc() ? siteConfig.app_version : siteConfig.version}
</p>
</div>
<p className="text-xs text-gray-400 dark:text-gray-500">
Powered by{" "}
<a
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
</a>
</p>
<p className="text-xs text-gray-400 dark:text-gray-500 mt-1">
v{isWebViewFunc() ? siteConfig.app_version : siteConfig.version}
</p>
</div>
</div>
{/* 修改密码弹窗 */}
<Modal
isOpen={isOpen}
<Modal
backdrop="blur"
isOpen={isOpen}
placement="center"
scrollBehavior="outside"
size="2xl"
onOpenChange={() => {
onOpenChange();
resetPasswordForm();
}}
size="2xl"
scrollBehavior="outside"
backdrop="blur"
placement="center"
>
<ModalContent>
{(onClose: () => void) => (
<>
<ModalHeader className="flex flex-col gap-1">修改密码</ModalHeader>
<ModalHeader className="flex flex-col gap-1">
修改密码
</ModalHeader>
<ModalBody>
<div className="space-y-4">
<Input
label="新用户名"
placeholder="请输入新用户名(至少3位)"
value={passwordForm.newUsername}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setPasswordForm(prev => ({ ...prev, newUsername: e.target.value }))}
variant="bordered"
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setPasswordForm((prev) => ({
...prev,
newUsername: e.target.value,
}))
}
/>
<Input
label="当前密码"
type="password"
placeholder="请输入当前密码"
type="password"
value={passwordForm.currentPassword}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setPasswordForm(prev => ({ ...prev, currentPassword: e.target.value }))}
variant="bordered"
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setPasswordForm((prev) => ({
...prev,
currentPassword: e.target.value,
}))
}
/>
<Input
label="新密码"
type="password"
placeholder="请输入新密码(至少6位)"
type="password"
value={passwordForm.newPassword}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setPasswordForm(prev => ({ ...prev, newPassword: e.target.value }))}
variant="bordered"
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setPasswordForm((prev) => ({
...prev,
newPassword: e.target.value,
}))
}
/>
<Input
label="确认密码"
type="password"
placeholder="请再次输入新密码"
type="password"
value={passwordForm.confirmPassword}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setPasswordForm(prev => ({ ...prev, confirmPassword: e.target.value }))}
variant="bordered"
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setPasswordForm((prev) => ({
...prev,
confirmPassword: e.target.value,
}))
}
/>
</div>
</ModalBody>
@@ -316,10 +396,10 @@ export default function ProfilePage() {
<Button color="default" variant="light" onPress={onClose}>
取消
</Button>
<Button
color="primary"
onPress={handlePasswordSubmit}
<Button
color="primary"
isLoading={passwordLoading}
onPress={handlePasswordSubmit}
>
确定
</Button>