diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index ec939b1..6c46d93 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -18,6 +18,9 @@ import ClassroomsPage from './pages/Classrooms'; import TenantsPage from './pages/Tenants'; import ClassroomRentalsPage from './pages/ClassroomRentals'; import ClassroomSchedulePage from './pages/ClassroomSchedule'; +import RolesPage from './pages/Roles'; +import PermissionsPage from './pages/Permissions'; +import PermissionRoute from './components/PermissionRoute'; const PrivateRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => { const token = localStorage.getItem('token'); @@ -33,20 +36,22 @@ const App: React.FC = () => { } /> }> } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> diff --git a/frontend/src/layouts/MainLayout.tsx b/frontend/src/layouts/MainLayout.tsx index afa5ac1..b1d7850 100644 --- a/frontend/src/layouts/MainLayout.tsx +++ b/frontend/src/layouts/MainLayout.tsx @@ -20,10 +20,48 @@ import { TagsOutlined, FileProtectOutlined, CalendarOutlined, + SafetyOutlined, + KeyOutlined, } from '@ant-design/icons'; +import { usePermission } from '../hooks/usePermission'; const { Header, Sider, Content } = Layout; +interface MenuItemType { + key: string; + icon: React.ReactNode; + label: string; + permission?: string; + children?: MenuItemType[]; +} + +const allMenuItems: MenuItemType[] = [ + { key: '/dashboard', icon: , label: '数据面板', permission: 'dashboard:view' }, + { key: '/room-visual', icon: , label: '宿舍总览', permission: 'room:view' }, + { key: '/students', icon: , label: '学生管理', permission: 'student:view' }, + { key: '/rooms', icon: , label: '宿舍管理', permission: 'room:view' }, + { key: '/occupancies', icon: , label: '入住管理', permission: 'occupancy:view' }, + { key: '/expenses', icon: , label: '费用录入', permission: 'expense:view' }, + { key: '/deposits', icon: , label: '押金管理', permission: 'deposit:view' }, + { key: '/bills', icon: , label: '账单管理', permission: 'bill:view' }, + { + key: 'classroom-group', + icon: , + label: '教室管理', + permission: 'classroom:view', + children: [ + { key: '/classroom-schedule', icon: , label: '排期总览', permission: 'classroom:view' }, + { key: '/classrooms', icon: , label: '教室列表', permission: 'classroom:view' }, + { key: '/classroom-rentals', icon: , label: '租赁订单', permission: 'rental:view' }, + { key: '/tenants', icon: , label: '租赁方', permission: 'tenant:view' }, + ], + }, + { key: '/operation-logs', icon: , label: '操作日志', permission: 'log:view' }, + { key: '/roles', icon: , label: '角色管理', permission: 'role:view' }, + { key: '/permissions', icon: , label: '权限一览', permission: 'role:view' }, + { key: '/users', icon: , label: '账号管理', permission: 'user:view' }, +]; + const MainLayout: React.FC = () => { const [collapsed, setCollapsed] = useState(false); const [isMobile, setIsMobile] = useState(window.innerWidth < 768); @@ -31,6 +69,7 @@ const MainLayout: React.FC = () => { const navigate = useNavigate(); const location = useLocation(); const user = JSON.parse(localStorage.getItem('user') || '{}'); + const { hasPermission } = usePermission(); useEffect(() => { const handleResize = () => setIsMobile(window.innerWidth < 768); @@ -38,55 +77,27 @@ const MainLayout: React.FC = () => { return () => window.removeEventListener('resize', handleResize); }, []); - const allMenuItems: any[] = [ - { key: '/dashboard', icon: , label: '数据面板' }, - { key: '/room-visual', icon: , label: '宿舍总览' }, - { key: '/students', icon: , label: '学生管理' }, - { key: '/rooms', icon: , label: '宿舍管理' }, - { key: '/occupancies', icon: , label: '入住管理' }, - { key: '/expenses', icon: , label: '费用录入' }, - { key: '/deposits', icon: , label: '押金管理' }, - { key: '/bills', icon: , label: '账单管理' }, - { - key: 'classroom-group', - icon: , - label: '教室管理', - children: [ - { key: '/classroom-schedule', icon: , label: '排期总览' }, - { key: '/classrooms', icon: , label: '教室列表' }, - { key: '/classroom-rentals', icon: , label: '租赁订单' }, - { key: '/tenants', icon: , label: '租赁方' }, - ], - }, - { key: '/operation-logs', icon: , label: '操作日志' }, - { key: '/users', icon: , label: '账号管理' }, - ]; - - // 权限过滤:admin看全部,operator按allowedMenus过滤 - const filterByAllowed = (items: any[], allowed: string[] | null): any[] => { + // 按 permission 过滤菜单 + const filterByPermission = (items: MenuItemType[]): MenuItemType[] => { return items .map(item => { if (item.children) { - const kids = filterByAllowed(item.children, allowed); + const kids = filterByPermission(item.children); if (kids.length === 0) return null; return { ...item, children: kids }; } - const k = item.key.startsWith('/') ? item.key.slice(1) : item.key; - if (!allowed) return item; - return allowed.includes(k) ? item : null; + if (!item.permission) return item; + return hasPermission(item.permission) ? item : null; }) - .filter(Boolean); + .filter(Boolean) as MenuItemType[]; }; - const menuItems = user.role === 'admin' - ? allMenuItems - : user.allowedMenus && user.allowedMenus.length > 0 - ? filterByAllowed(allMenuItems, user.allowedMenus) - : filterByAllowed(allMenuItems, ['dashboard', 'room-visual']); + const menuItems = filterByPermission(allMenuItems); const handleLogout = () => { localStorage.removeItem('token'); localStorage.removeItem('user'); + localStorage.removeItem('permissions'); navigate('/login'); }; @@ -95,12 +106,21 @@ const MainLayout: React.FC = () => { if (isMobile) setDrawerOpen(false); }; + const transformToMenuItems = (items: MenuItemType[]): any[] => { + return items.map(item => ({ + key: item.key, + icon: item.icon, + label: item.label, + children: item.children ? transformToMenuItems(item.children) : undefined, + })); + }; + const menuContent = ( handleMenuClick(key)} style={{ border: 'none' }} /> @@ -108,7 +128,6 @@ const MainLayout: React.FC = () => { return ( - {/* 桌面端侧边栏 */} {!isMobile && (
@@ -117,16 +136,8 @@ const MainLayout: React.FC = () => { {menuContent} )} - {/* 移动端抽屉 */} {isMobile && ( - setDrawerOpen(false)} - width={240} - styles={{ body: { padding: 0 } }} - title="恭学教育基地" - > + setDrawerOpen(false)} width={240} styles={{ body: { padding: 0 } }} title="恭学教育基地"> {menuContent} )} diff --git a/frontend/src/pages/Bills/index.tsx b/frontend/src/pages/Bills/index.tsx index 3f19e82..e38e2cc 100644 --- a/frontend/src/pages/Bills/index.tsx +++ b/frontend/src/pages/Bills/index.tsx @@ -3,6 +3,7 @@ import { Table, Button, Modal, Form, DatePicker, Space, message, Tag, Descriptio import { FileTextOutlined, DeleteOutlined, DownloadOutlined, FilePdfOutlined } from '@ant-design/icons'; import dayjs from 'dayjs'; import api from '../../api'; +import PermissionButton from '../../components/PermissionButton'; const { RangePicker } = DatePicker; @@ -180,13 +181,15 @@ const BillsPage: React.FC = () => { title: '操作', width: 320, render: (_: any, record: any) => ( - - {record.status === 'draft' && } - {record.status === 'confirmed' && } - - handleDelete(record.id)} okText="删除" cancelText="取消"> - - + showDetail(record.id)}>详情 + {record.status === 'draft' && updateStatus(record.id, 'confirmed')}>确认} + {record.status === 'confirmed' && updateStatus(record.id, 'paid')}>标记已付} + } onClick={() => handleExportPdf(record.id)}>PDF + + handleDelete(record.id)} okText="删除" cancelText="取消"> + + + ), }, @@ -215,17 +218,19 @@ const BillsPage: React.FC = () => { { value: 'paid', label: '已支付' }, ]} /> - - - - - + batchUpdateStatus('confirmed')} disabled={selectedRows.length === 0}>批量确认 + batchUpdateStatus('paid')} disabled={selectedRows.length === 0}>批量标记已付 + + + + + - - + + } onClick={handleExportExcel}>导出Excel
{ const [data, setData] = useState([]); @@ -189,10 +190,12 @@ const ClassroomRentalsPage: React.FC = () => { title: '操作', width: 150, render: (_: any, record: any) => ( - - handleDelete(record.id)}> - - + openEdit(record)}>编辑 + + handleDelete(record.id)}> + + + ), }, @@ -211,9 +214,9 @@ const ClassroomRentalsPage: React.FC = () => { /> - +
`共 ${total} 条` }} scroll={{ x: 1200 }} /> diff --git a/frontend/src/pages/Classrooms/index.tsx b/frontend/src/pages/Classrooms/index.tsx index 1ef4249..ffd037b 100644 --- a/frontend/src/pages/Classrooms/index.tsx +++ b/frontend/src/pages/Classrooms/index.tsx @@ -2,6 +2,7 @@ import React, { useEffect, useState, useMemo } from 'react'; import { Table, Button, Modal, Form, Input, InputNumber, Select, Space, message, Tag, Popconfirm, Upload } from 'antd'; import { PlusOutlined, UploadOutlined, DownloadOutlined, UndoOutlined, InboxOutlined } from '@ant-design/icons'; import api from '../../api'; +import PermissionButton from '../../components/PermissionButton'; const statusMap: Record = { available: { text: '可用', color: 'green' }, @@ -106,15 +107,19 @@ const ClassroomsPage: React.FC = () => { render: (_: any, record: any) => ( {record.status === 'archived' ? ( - handleRestore(record.id)}> - - + + handleRestore(record.id)}> + + + ) : ( <> - - handleArchive(record.id)} okText="归档" cancelText="取消"> - - + { setEditing(record); form.setFieldsValue(record); setModalOpen(true); }}>编辑 + + handleArchive(record.id)} okText="归档" cancelText="取消"> + + + )} @@ -138,26 +143,28 @@ const ClassroomsPage: React.FC = () => { - - { - const formData = new FormData(); - formData.append('file', file); - try { - const res: any = await api.post('/classrooms/import', formData, { headers: { 'Content-Type': 'multipart/form-data' } }); - message.success(res.message); - onSuccess?.(res); - fetchData(); - } catch (e: any) { message.error(e?.message || '导入失败'); onError?.(e); } - }} - > - - - + + + { + const formData = new FormData(); + formData.append('file', file); + try { + const res: any = await api.post('/classrooms/import', formData, { headers: { 'Content-Type': 'multipart/form-data' } }); + message.success(res.message); + onSuccess?.(res); + fetchData(); + } catch (e: any) { message.error(e?.message || '导入失败'); onError?.(e); } + }} + > + + + + } onClick={handleDownloadTemplate}>下载模板
`共 ${total} 条` }} /> diff --git a/frontend/src/pages/Deposits/index.tsx b/frontend/src/pages/Deposits/index.tsx index 762f420..9912d8c 100644 --- a/frontend/src/pages/Deposits/index.tsx +++ b/frontend/src/pages/Deposits/index.tsx @@ -3,6 +3,7 @@ import { Table, Button, Modal, Form, Select, DatePicker, InputNumber, Input, Spa import { PlusOutlined, DeleteOutlined } from '@ant-design/icons'; import dayjs from 'dayjs'; import api from '../../api'; +import PermissionButton from '../../components/PermissionButton'; const statusMap: Record = { paid: { text: '已缴', color: 'green' }, @@ -98,16 +99,18 @@ const DepositsPage: React.FC = () => { render: (_: any, record: any) => ( {record.status === 'paid' && ( - + }}>退还 )} - { - try { await api.delete(`/deposits/${record.id}`); message.success('删除成功'); fetchData(); } catch (e: any) { message.error(e?.message || '删除失败'); } - }}> - +
`共 ${total} 条` }} /> diff --git a/frontend/src/pages/Expenses/index.tsx b/frontend/src/pages/Expenses/index.tsx index 90c87be..7d0e408 100644 --- a/frontend/src/pages/Expenses/index.tsx +++ b/frontend/src/pages/Expenses/index.tsx @@ -3,6 +3,7 @@ import { Table, Button, Modal, Form, Select, DatePicker, InputNumber, Input, Spa import { PlusOutlined, DeleteOutlined, EditOutlined, UploadOutlined, DownloadOutlined, ExportOutlined } from '@ant-design/icons'; import dayjs from 'dayjs'; import api from '../../api'; +import PermissionButton from '../../components/PermissionButton'; const { RangePicker } = DatePicker; @@ -165,7 +166,7 @@ const ExpensesPage: React.FC = () => { title: '操作', width: 120, render: (_: any, record: any) => ( - - - + + + } onClick={() => { const baseURL = import.meta.env.PROD ? '/api' : `http://localhost:${import.meta.env.VITE_API_PORT || 3002}/api`; const token = localStorage.getItem('token'); fetch(`${baseURL}/expenses/utility/template`, { headers: { Authorization: `Bearer ${token}` } }) @@ -273,13 +280,15 @@ const ExpensesPage: React.FC = () => { URL.revokeObjectURL(url); }) .catch(() => message.error('下载失败')); - }}>下载水电费模板 + }}>下载水电费模板 - - - - + + + + + + } onClick={() => { setEditingRoom(null); roomForm.resetFields(); setRoomModal(true); }}>录入宿舍费用
`共 ${total} 条` }} @@ -310,24 +319,26 @@ const ExpensesPage: React.FC = () => { onChange={v => setPersonalTypeFilter(v)} options={personalExpenseTypeOptions} /> - { - try { - const formData = new FormData(); - formData.append('file', file); - const res: any = await api.post('/expenses/personal/import', formData); - message.success(res.message || '导入完成'); - if (res.errors?.length) res.errors.forEach((e: string) => message.warning(e)); - fetchData(); - onSuccess?.(res); - } catch (e: any) { message.error(e?.message || '导入失败'); onError?.(e); } - }} - > - - - + + + } onClick={() => { const baseURL = import.meta.env.PROD ? '/api' : `http://localhost:${import.meta.env.VITE_API_PORT || 3002}/api`; const token = localStorage.getItem('token'); fetch(`${baseURL}/expenses/personal/template`, { headers: { Authorization: `Bearer ${token}` } }) @@ -341,8 +352,8 @@ const ExpensesPage: React.FC = () => { URL.revokeObjectURL(url); }) .catch(() => message.error('下载失败')); - }}>下载模板 - + }}>导出 - - - - + + + + + + } onClick={() => { setEditingPersonal(null); personalForm.resetFields(); setPersonalModal(true); }}>录入个人费用
`共 ${total} 条` }} diff --git a/frontend/src/pages/Occupancies/index.tsx b/frontend/src/pages/Occupancies/index.tsx index dad20cc..38dd93d 100644 --- a/frontend/src/pages/Occupancies/index.tsx +++ b/frontend/src/pages/Occupancies/index.tsx @@ -3,6 +3,7 @@ import { Table, Button, Modal, Form, Select, DatePicker, Input, InputNumber, Spa import { PlusOutlined, SwapOutlined, LogoutOutlined, DeleteOutlined, UploadOutlined, DownloadOutlined, ExportOutlined } from '@ant-design/icons'; import dayjs from 'dayjs'; import api from '../../api'; +import PermissionButton from '../../components/PermissionButton'; const OccupanciesPage: React.FC = () => { const [data, setData] = useState([]); @@ -136,15 +137,17 @@ const OccupanciesPage: React.FC = () => { title: '操作', width: 200, render: (_: any, record: any) => !record.checkOutDate ? ( - - + } onClick={() => { setCheckOutModal(record); checkOutForm.setFieldsValue({ checkOutDate: dayjs() }); }}>退宿 + } onClick={() => { setTransferModal(record); transferForm.setFieldsValue({ transferDate: dayjs() }); }}>换房 ) : ( 已退宿 - { try { await api.delete(`/occupancies/${record.id}`); message.success('删除成功'); fetchData(); } catch (e: any) { message.error(e?.message || '删除失败'); } }}> - - { - const formData = new FormData(); - formData.append('file', file); - const params = new URLSearchParams(); - if (autoDeposit) { - params.set('autoDeposit', 'true'); - params.set('depositAmount', String(depositAmount)); - } - try { - const res: any = await api.post(`/occupancies/import?${params.toString()}`, formData, { headers: { 'Content-Type': 'multipart/form-data' } }); - if (res.errors?.length > 0) { - Modal.warning({ title: res.message, content: res.errors.join('\n'), width: 500 }); - } else { - message.success(res.message); + + + { + const formData = new FormData(); + formData.append('file', file); + const params = new URLSearchParams(); + if (autoDeposit) { + params.set('autoDeposit', 'true'); + params.set('depositAmount', String(depositAmount)); } - onSuccess?.(res); - fetchData(); - } catch (e: any) { message.error(e?.message || '导入失败'); onError?.(e); } - }} - > - - - - - + + + + } onClick={() => { const baseURL = import.meta.env.PROD ? '/api' : `http://localhost:${import.meta.env.VITE_API_PORT || 3002}/api`; const token = localStorage.getItem('token'); fetch(`${baseURL}/occupancies/template`, { headers: { Authorization: `Bearer ${token}` } }) @@ -218,8 +223,8 @@ const OccupanciesPage: React.FC = () => { URL.revokeObjectURL(url); }) .catch(() => message.error('下载失败')); - }}>下载模板 - + }}>导出记录 导入时自动收押金 @@ -248,11 +253,13 @@ const OccupanciesPage: React.FC = () => { 已选 {selectedRowKeys.length} 条记录 {showActive ? ( - + } onClick={() => { batchCheckOutForm.resetFields(); batchCheckOutForm.setFieldsValue({ checkOutDate: dayjs() }); setBatchCheckOutModal(true); }} style={{ marginLeft: 12 }}>批量退宿 ) : ( - - - + + + + + )} diff --git a/frontend/src/pages/Rooms/index.tsx b/frontend/src/pages/Rooms/index.tsx index 640ce38..5c8b4df 100644 --- a/frontend/src/pages/Rooms/index.tsx +++ b/frontend/src/pages/Rooms/index.tsx @@ -2,6 +2,7 @@ import React, { useEffect, useState, useMemo } from 'react'; import { Table, Button, Modal, Form, Input, InputNumber, Select, Space, message, Tag, Popconfirm, Badge, Upload } from 'antd'; import { PlusOutlined, UploadOutlined, DownloadOutlined, UndoOutlined, InboxOutlined, SearchOutlined, ExportOutlined, DeleteOutlined } from '@ant-design/icons'; import api from '../../api'; +import PermissionButton from '../../components/PermissionButton'; const statusMap: Record = { available: { text: '可入住', color: 'green' }, @@ -159,16 +160,20 @@ const RoomsPage: React.FC = () => { render: (_: any, record: any) => ( {record.status === 'archived' ? ( - handleRestore(record.id)} okText="恢复" cancelText="取消"> - - + + handleRestore(record.id)} okText="恢复" cancelText="取消"> + + + ) : ( <> - - - handleArchive(record.id)} okText="归档" cancelText="取消"> - - + showDetail(record.id)}>查看住户 + { setEditing(record); form.setFieldsValue(record); setModalOpen(true); }}>编辑 + + handleArchive(record.id)} okText="归档" cancelText="取消"> + + + )} @@ -203,30 +208,34 @@ const RoomsPage: React.FC = () => { - - - - + + + } onClick={() => { setEditing(null); form.resetFields(); setModalOpen(true); }}> 添加宿舍 - - { - const formData = new FormData(); - formData.append('file', file); - try { - const res: any = await api.post('/rooms/import', formData, { headers: { 'Content-Type': 'multipart/form-data' } }); - message.success(res.message); - onSuccess?.(res); - fetchData(); - } catch (e: any) { message.error(e?.message || '导入失败'); onError?.(e); } - }} - > - - - - + + + { + const formData = new FormData(); + formData.append('file', file); + try { + const res: any = await api.post('/rooms/import', formData, { headers: { 'Content-Type': 'multipart/form-data' } }); + message.success(res.message); + onSuccess?.(res); + fetchData(); + } catch (e: any) { message.error(e?.message || '导入失败'); onError?.(e); } + }} + > + + + + } onClick={handleDownloadTemplate}>下载模板 + } onClick={handleExport}>导出列表
= { active: { text: '在读', color: 'green' }, @@ -131,15 +132,19 @@ const StudentsPage: React.FC = () => { render: (_: any, record: any) => ( {record.status === 'archived' ? ( - handleRestore(record.id)} okText="恢复" cancelText="取消"> - - + + handleRestore(record.id)} okText="恢复" cancelText="取消"> + + + ) : ( <> - - handleArchive(record.id)} okText="归档" cancelText="取消"> - - + { setEditing(record); form.setFieldsValue(record); setModalOpen(true); }}>编辑 + + handleArchive(record.id)} okText="归档" cancelText="取消"> + + + )} @@ -157,30 +162,34 @@ const StudentsPage: React.FC = () => { - - - - + + + } onClick={() => { setEditing(null); form.resetFields(); setModalOpen(true); }}> 添加学生 - - { - const formData = new FormData(); - formData.append('file', file); - try { - const res: any = await api.post('/students/import', formData, { headers: { 'Content-Type': 'multipart/form-data' } }); - message.success(res.message); - onSuccess?.(res); - fetchData(); - } catch (e: any) { message.error(e?.message || '导入失败'); onError?.(e); } - }} - > - - - - + + + { + const formData = new FormData(); + formData.append('file', file); + try { + const res: any = await api.post('/students/import', formData, { headers: { 'Content-Type': 'multipart/form-data' } }); + message.success(res.message); + onSuccess?.(res); + fetchData(); + } catch (e: any) { message.error(e?.message || '导入失败'); onError?.(e); } + }} + > + + + + } onClick={handleDownloadTemplate}>下载模板 + } onClick={handleExport}>导出名单
{ title: '操作', width: 150, render: (_: any, record: any) => ( - - handleArchive(record.id)} okText="归档" cancelText="取消"> - - + { setEditing(record); form.setFieldsValue(record); setModalOpen(true); }}>编辑 + + handleArchive(record.id)} okText="归档" cancelText="取消"> + + + ), }, @@ -94,9 +97,9 @@ const TenantsPage: React.FC = () => { onSearch={v => setSearchText(v)} onChange={e => { if (!e.target.value) setSearchText(''); }} /> - +
`共 ${total} 条` }} /> diff --git a/frontend/src/pages/Users/index.tsx b/frontend/src/pages/Users/index.tsx index 98d46c3..e6b974a 100644 --- a/frontend/src/pages/Users/index.tsx +++ b/frontend/src/pages/Users/index.tsx @@ -1,28 +1,13 @@ import React, { useEffect, useState } from 'react'; -import { Table, Button, Modal, Form, Input, Select, Switch, Space, Tag, Popconfirm, message, Checkbox } from 'antd'; +import { Table, Button, Modal, Form, Input, Select, Switch, Space, Tag, Popconfirm, message } from 'antd'; import { PlusOutlined, EditOutlined, DeleteOutlined, KeyOutlined } from '@ant-design/icons'; import dayjs from 'dayjs'; import api from '../../api'; - -const MENU_OPTIONS = [ - { value: 'dashboard', label: '数据面板' }, - { value: 'room-visual', label: '宿舍总览' }, - { value: 'students', label: '学生管理' }, - { value: 'rooms', label: '宿舍管理' }, - { value: 'occupancies', label: '入住管理' }, - { value: 'expenses', label: '费用录入' }, - { value: 'deposits', label: '押金管理' }, - { value: 'bills', label: '账单管理' }, - { value: 'classroom-schedule', label: '教室排期总览' }, - { value: 'classrooms', label: '教室列表' }, - { value: 'classroom-rentals', label: '教室租赁订单' }, - { value: 'tenants', label: '租赁方管理' }, - { value: 'operation-logs', label: '操作日志' }, - { value: 'users', label: '账号管理' }, -]; +import PermissionButton from '../../components/PermissionButton'; const UsersPage: React.FC = () => { const [data, setData] = useState([]); + const [roles, setRoles] = useState([]); const [loading, setLoading] = useState(false); const [modalOpen, setModalOpen] = useState(false); const [pwdModalOpen, setPwdModalOpen] = useState(false); @@ -34,8 +19,12 @@ const UsersPage: React.FC = () => { const fetchData = async () => { setLoading(true); try { - const res: any = await api.get('/auth/users'); - setData(res); + const [users, rolesRes] = await Promise.all([ + api.get('/rbac/users') as Promise, + api.get('/rbac/roles') as Promise, + ]); + setData(users); + setRoles(rolesRes); } catch (e) { console.error(e); } setLoading(false); }; @@ -50,7 +39,12 @@ const UsersPage: React.FC = () => { const handleEdit = (record: any) => { setEditing(record); - form.setFieldsValue({ username: record.username, name: record.name, role: record.role, isActive: record.isActive, allowedMenus: record.allowedMenus || [] }); + form.setFieldsValue({ + username: record.username, + name: record.name, + isActive: record.isActive, + roleIds: record.roles?.map((r: any) => r.id) || [], + }); setModalOpen(true); }; @@ -58,10 +52,10 @@ const UsersPage: React.FC = () => { const values = await form.validateFields(); try { if (editing) { - await api.put(`/auth/users/${editing.id}`, { username: values.username, name: values.name, role: values.role, isActive: values.isActive, allowedMenus: values.allowedMenus || [] }); + await api.put(`/rbac/users/${editing.id}`, { username: values.username, name: values.name, isActive: values.isActive, roleIds: values.roleIds || [] }); message.success('更新成功'); } else { - await api.post('/auth/register', { username: values.username, password: values.password, name: values.name, allowedMenus: values.allowedMenus || [] }); + await api.post('/rbac/users', { username: values.username, password: values.password, name: values.name, roleIds: values.roleIds || [] }); message.success('创建成功'); } setModalOpen(false); @@ -71,7 +65,7 @@ const UsersPage: React.FC = () => { const handleDelete = async (id: number) => { try { - await api.delete(`/auth/users/${id}`); + await api.delete(`/rbac/users/${id}`); message.success('已删除'); fetchData(); } catch (e: any) { message.error(e.message || '删除失败'); } @@ -86,7 +80,7 @@ const UsersPage: React.FC = () => { const handlePwdSubmit = async () => { const values = await pwdForm.validateFields(); try { - await api.put(`/auth/users/${resetTarget.id}/password`, { password: values.password }); + await api.put(`/rbac/users/${resetTarget.id}/password`, { password: values.password }); message.success('密码已重置'); setPwdModalOpen(false); } catch (e: any) { message.error(e.message || '操作失败'); } @@ -97,12 +91,10 @@ const UsersPage: React.FC = () => { { title: '用户名', dataIndex: 'username', width: 120 }, { title: '姓名', dataIndex: 'name', width: 120 }, { - title: '角色', dataIndex: 'role', width: 100, - render: (v: string) => {v === 'admin' ? '管理员' : '操作员'}, - }, - { - title: '可见菜单', dataIndex: 'allowedMenus', width: 200, ellipsis: true, - render: (v: string[], record: any) => record.role === 'admin' ? 全部权限 : (v && v.length > 0 ? v.map((m: string) => {MENU_OPTIONS.find(o => o.value === m)?.label || m}) : 仅基础), + title: '角色', dataIndex: 'roles', width: 200, + render: (v: any[]) => v && v.length > 0 + ? v.map((r: any) => {r.name}) + : 无角色, }, { title: '状态', dataIndex: 'isActive', width: 80, @@ -117,15 +109,17 @@ const UsersPage: React.FC = () => { render: (v: string) => dayjs(v).format('YYYY-MM-DD HH:mm:ss'), }, { - title: '操作', width: 200, fixed: 'right' as const, + title: '操作', width: 220, fixed: 'right' as const, render: (_: any, record: any) => ( - - + } onClick={() => handleEdit(record)}>编辑 + } onClick={() => handleResetPwd(record)}>重置密码 {record.username !== 'admin' && ( - handleDelete(record.id)}> - - + + handleDelete(record.id)}> + + + )} ), @@ -136,9 +130,9 @@ const UsersPage: React.FC = () => {

账号管理

- + } onClick={handleAdd}>新增账号
-
+
setModalOpen(false)} destroyOnClose>
@@ -154,17 +148,16 @@ const UsersPage: React.FC = () => { {editing && ( - <> - - r.status !== 0).map((r: any) => ({ value: r.id, label: `${r.name}${r.isSystem ? ' (系统)' : ''}` }))} + />