From 6722752d5e56e87dc4d2b2bf870563823622f2a5 Mon Sep 17 00:00:00 2001 From: zhxiao1124 Date: Wed, 1 Jul 2026 13:56:20 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E6=9D=83=E9=99=90?= =?UTF-8?q?=E6=8E=A7=E5=88=B6=E7=9B=B8=E5=85=B3=E5=8A=9F=E8=83=BD=EF=BC=8C?= =?UTF-8?q?=E9=87=8D=E6=9E=84=E8=8F=9C=E5=8D=95=E4=B8=8E=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E8=AE=BF=E9=97=AE=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 新增基于权限的菜单过滤和页面路由控制 2. 重构usePageData钩子支持按需加载 3. 调整个人中心页面布局与密码修改功能 4. 优化表格列展示与顶部用户菜单样式 --- src/App.css | 20 +++++ src/App.tsx | 237 +++++++++++++++++++++++++++++++++++----------------- 2 files changed, 179 insertions(+), 78 deletions(-) diff --git a/src/App.css b/src/App.css index b036253..89a6896 100644 --- a/src/App.css +++ b/src/App.css @@ -210,6 +210,22 @@ font-size: 12px; } +.user-menu-trigger { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 4px 8px; + border: 0; + border-radius: 6px; + background: transparent; + color: #1d2129; + cursor: pointer; +} + +.user-menu-trigger:hover { + background: #f2f3f5; +} + .content { padding: 20px; } @@ -272,6 +288,10 @@ align-items: start; } +.profile-grid .section-card:first-child { + grid-row: span 2; +} + .profile-summary { display: flex; align-items: center; diff --git a/src/App.tsx b/src/App.tsx index 015f0dc..c0be9bc 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,6 +4,7 @@ import { Avatar, Button, Card, + Dropdown, Form, Input, Layout, @@ -67,6 +68,7 @@ type AdminProfile = { status: string; isSuperAdmin: boolean; roles: Role[]; + permissions: Permission[] | ['*']; createdAt: string; }; @@ -151,7 +153,7 @@ async function fetchPage(url: string): Promise> { return response.data.data; } -function usePageData(url: string) { +function usePageData(url: string, enabled = true) { const [loading, setLoading] = useState(false); const [data, setData] = useState>({ list: [], @@ -159,6 +161,9 @@ function usePageData(url: string) { }); const load = async () => { + if (!enabled) { + return; + } setLoading(true); try { setData(await fetchPage(url)); @@ -168,8 +173,10 @@ function usePageData(url: string) { }; useEffect(() => { - void load(); - }, [url]); + if (enabled) { + void load(); + } + }, [url, enabled]); return { data, loading, reload: load }; } @@ -244,6 +251,7 @@ type MenuConfig = { key: string; label: string; icon?: ReactNode; + requiredPermissions?: string[]; children?: MenuConfig[]; }; @@ -251,25 +259,56 @@ const SYSTEM_MENU_KEY = '/system'; const menuItems: MenuConfig[] = [ { key: '/dashboard', label: '工作台', icon: }, - { key: '/dramas', label: '短剧管理', icon: }, - { key: '/users', label: '用户管理', icon: }, + { key: '/dramas', label: '短剧管理', icon: , requiredPermissions: ['drama:read'] }, + { key: '/users', label: '用户管理', icon: , requiredPermissions: ['user:read'] }, { key: SYSTEM_MENU_KEY, label: '系统管理', icon: , children: [ - { key: '/permissions', label: '权限管理', icon: }, - { key: '/roles', label: '角色管理', icon: }, - { key: '/personnel', label: '人员管理', icon: }, + { key: '/permissions', label: '权限管理', icon: , requiredPermissions: ['permission:read'] }, + { key: '/roles', label: '角色管理', icon: , requiredPermissions: ['role:read'] }, + { key: '/personnel', label: '人员管理', icon: , requiredPermissions: ['admin-user:read'] }, + { key: '/profile', label: '个人中心', icon: }, + { key: '/logs', label: '日志管理', icon: , requiredPermissions: ['log:read'] }, ], }, - { key: '/logs', label: '日志管理', icon: }, ]; -function findCurrentMenu(pathname: string) { - if (pathname.startsWith('/profile')) { - return { item: { key: '/profile', label: '个人中心' } }; +function getPermissionCodes(profile: AdminProfile | null) { + if (!profile) { + return []; } + if (profile.isSuperAdmin || profile.permissions[0] === '*') { + return ['*']; + } + return (profile.permissions as Permission[]).map((permission) => permission.code); +} + +function hasPermissions(profile: AdminProfile | null, requiredPermissions?: string[]) { + if (!requiredPermissions?.length) { + return true; + } + const codes = getPermissionCodes(profile); + if (codes.includes('*')) { + return true; + } + return requiredPermissions.every((permission) => codes.includes(permission)); +} + +function filterMenuItems(items: MenuConfig[], profile: AdminProfile | null): MenuConfig[] { + return items + .map((item) => { + if (item.children) { + const children = filterMenuItems(item.children, profile); + return children.length ? { ...item, children } : null; + } + return hasPermissions(profile, item.requiredPermissions) ? item : null; + }) + .filter((item): item is MenuConfig => Boolean(item)); +} + +function findCurrentMenu(pathname: string) { for (const item of menuItems) { if (item.children) { const child = item.children.find((childItem) => pathname.startsWith(childItem.key)); @@ -296,14 +335,39 @@ function renderMenuItem(item: MenuConfig) { return {item.icon}{item.label}; } +function NoAccessPage() { + return ( + + 当前账号没有访问该功能的权限,请联系系统管理员分配角色。 + + ); +} + +function PermissionRoute({ + profile, + requiredPermissions, + children, +}: { + profile: AdminProfile | null; + requiredPermissions?: string[]; + children: ReactElement; +}) { + if (!profile) { + return ; + } + if (!hasPermissions(profile, requiredPermissions)) { + return ; + } + return children; +} + function AdminLayout() { const navigate = useNavigate(); const location = useLocation(); const [collapsed, setCollapsed] = useState(false); const [profile, setProfile] = useState(null); - const [passwordVisible, setPasswordVisible] = useState(false); - const [changingPassword, setChangingPassword] = useState(false); const current = useMemo(() => findCurrentMenu(location.pathname), [location.pathname]); + const visibleMenuItems = useMemo(() => filterMenuItems(menuItems, profile), [profile]); const [openMenuKeys, setOpenMenuKeys] = useState(current?.parent ? [current.parent.key] : []); useEffect(() => { @@ -326,26 +390,6 @@ function AdminLayout() { navigate('/login', { replace: true }); }; - const changePassword = async (values: { oldPassword: string; newPassword: string; confirmPassword: string }) => { - if (values.newPassword !== values.confirmPassword) { - Message.error('两次输入的新密码不一致'); - return; - } - setChangingPassword(true); - try { - await api.patch('/api/admin/v1/auth/password', { - oldPassword: values.oldPassword, - newPassword: values.newPassword, - }); - Message.success('密码修改成功,请重新登录'); - setPasswordVisible(false); - localStorage.removeItem(TOKEN_KEY); - navigate('/login', { replace: true }); - } finally { - setChangingPassword(false); - } - }; - return ( @@ -359,7 +403,7 @@ function AdminLayout() { onClickMenuItem={(key) => navigate(key)} onClickSubMenu={(_, openKeys) => setOpenMenuKeys(openKeys)} > - {menuItems.map((item) => renderMenuItem(item))} + {visibleMenuItems.map((item) => renderMenuItem(item))} @@ -371,62 +415,63 @@ function AdminLayout() { 后端 API: {API_BASE_URL} - - {profile?.username?.slice(0, 1).toUpperCase() || 'A'} - {profile?.nickname || profile?.username || '管理员'} - - - - + + navigate('/profile')}>个人中心 + 退出登录 + + )} + > + + - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> } /> - setPasswordVisible(false)} footer={null}> -
- - - - - - - - - - -
-
); } -function DashboardPage() { - const dramas = usePageData('/api/admin/v1/dramas'); - const users = usePageData('/api/admin/v1/users'); - const logs = usePageData('/api/admin/v1/logs/requests'); +function DashboardPage({ profile }: { profile: AdminProfile | null }) { + const canReadDramas = hasPermissions(profile, ['drama:read']); + const canReadUsers = hasPermissions(profile, ['user:read']); + const canReadLogs = hasPermissions(profile, ['log:read']); + const dramas = usePageData('/api/admin/v1/dramas', canReadDramas); + const users = usePageData('/api/admin/v1/users', canReadUsers); + const logs = usePageData('/api/admin/v1/logs/requests', canReadLogs); + const hasAnyDataModule = canReadDramas || canReadUsers || canReadLogs; return ( <>
- - - + {canReadDramas && } + {canReadUsers && } + {canReadLogs && }
- - - + {canReadLogs ? ( + + + + ) : null} + {!hasAnyDataModule ? : null} ); } @@ -497,7 +542,7 @@ function UsersPage() { function PermissionsPage() { const { data, loading } = usePageData('/api/admin/v1/permissions'); - return ; + return
; } type PermissionTreeNode = { @@ -553,8 +598,7 @@ function getPermissionAction(code: string) { } function getPermissionTitle(permission: Permission, module?: PermissionModuleConfig) { - const action = actionNames[getPermissionAction(permission.code)] || permission.name; - return module ? `${action}${module.subject}` : permission.name; + return permission.name || (module ? `${actionNames[getPermissionAction(permission.code)]}${module.subject}` : permission.code); } function buildPermissionModuleNode( @@ -723,7 +767,7 @@ function RolesPage() { { title: 'ID', dataIndex: 'id', width: 80 }, { title: '编码', dataIndex: 'code' }, { title: '名称', dataIndex: 'name' }, - { title: '权限列表', render: (_, record: Role) => {record.permissions.map((item) => {item.code})} }, + { title: '权限列表', render: (_, record: Role) => {record.permissions.map((item) => {item.name})} }, { title: '操作', width: 140, render: (_, record: Role) => }, ]} /> setCreateVisible(false)} footer={null}> @@ -908,9 +952,12 @@ function PersonnelPage() { } function ProfilePage() { + const navigate = useNavigate(); const [form] = Form.useForm(); + const [passwordForm] = Form.useForm(); const [loading, setLoading] = useState(false); const [saving, setSaving] = useState(false); + const [changingPassword, setChangingPassword] = useState(false); const [profile, setProfile] = useState(null); const loadProfile = async () => { @@ -942,6 +989,26 @@ function ProfilePage() { } }; + const changePassword = async (values: { oldPassword: string; newPassword: string; confirmPassword: string }) => { + if (values.newPassword !== values.confirmPassword) { + Message.error('两次输入的新密码不一致'); + return; + } + setChangingPassword(true); + try { + await api.patch('/api/admin/v1/auth/password', { + oldPassword: values.oldPassword, + newPassword: values.newPassword, + }); + Message.success('密码修改成功,请重新登录'); + passwordForm.resetFields(); + localStorage.removeItem(TOKEN_KEY); + navigate('/login', { replace: true }); + } finally { + setChangingPassword(false); + } + }; + return (
@@ -970,6 +1037,20 @@ function ProfilePage() { + +
+ + + + + + + + + + + +
); }