feat(frontend): adapt routes, menus, Users page, and business pages for RBAC
This commit is contained in:
@@ -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 = () => {
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
<Route path="/" element={<PrivateRoute><MainLayout /></PrivateRoute>}>
|
||||
<Route index element={<Navigate to="/dashboard" />} />
|
||||
<Route path="dashboard" element={<DashboardPage />} />
|
||||
<Route path="students" element={<StudentsPage />} />
|
||||
<Route path="rooms" element={<RoomsPage />} />
|
||||
<Route path="occupancies" element={<OccupanciesPage />} />
|
||||
<Route path="expenses" element={<ExpensesPage />} />
|
||||
<Route path="deposits" element={<DepositsPage />} />
|
||||
<Route path="bills" element={<BillsPage />} />
|
||||
<Route path="room-visual" element={<RoomVisualPage />} />
|
||||
<Route path="operation-logs" element={<OperationLogsPage />} />
|
||||
<Route path="users" element={<UsersPage />} />
|
||||
<Route path="classrooms" element={<ClassroomsPage />} />
|
||||
<Route path="tenants" element={<TenantsPage />} />
|
||||
<Route path="classroom-rentals" element={<ClassroomRentalsPage />} />
|
||||
<Route path="classroom-schedule" element={<ClassroomSchedulePage />} />
|
||||
<Route path="dashboard" element={<PermissionRoute permission="dashboard:view"><DashboardPage /></PermissionRoute>} />
|
||||
<Route path="room-visual" element={<PermissionRoute permission="room:view"><RoomVisualPage /></PermissionRoute>} />
|
||||
<Route path="students" element={<PermissionRoute permission="student:view"><StudentsPage /></PermissionRoute>} />
|
||||
<Route path="rooms" element={<PermissionRoute permission="room:view"><RoomsPage /></PermissionRoute>} />
|
||||
<Route path="occupancies" element={<PermissionRoute permission="occupancy:view"><OccupanciesPage /></PermissionRoute>} />
|
||||
<Route path="expenses" element={<PermissionRoute permission="expense:view"><ExpensesPage /></PermissionRoute>} />
|
||||
<Route path="deposits" element={<PermissionRoute permission="deposit:view"><DepositsPage /></PermissionRoute>} />
|
||||
<Route path="bills" element={<PermissionRoute permission="bill:view"><BillsPage /></PermissionRoute>} />
|
||||
<Route path="operation-logs" element={<PermissionRoute permission="log:view"><OperationLogsPage /></PermissionRoute>} />
|
||||
<Route path="roles" element={<PermissionRoute permission="role:view"><RolesPage /></PermissionRoute>} />
|
||||
<Route path="permissions" element={<PermissionRoute permission="role:view"><PermissionsPage /></PermissionRoute>} />
|
||||
<Route path="users" element={<PermissionRoute permission="user:view"><UsersPage /></PermissionRoute>} />
|
||||
<Route path="classrooms" element={<PermissionRoute permission="classroom:view"><ClassroomsPage /></PermissionRoute>} />
|
||||
<Route path="tenants" element={<PermissionRoute permission="tenant:view"><TenantsPage /></PermissionRoute>} />
|
||||
<Route path="classroom-rentals" element={<PermissionRoute permission="rental:view"><ClassroomRentalsPage /></PermissionRoute>} />
|
||||
<Route path="classroom-schedule" element={<PermissionRoute permission="classroom:view"><ClassroomSchedulePage /></PermissionRoute>} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
|
||||
@@ -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: <DashboardOutlined />, label: '数据面板', permission: 'dashboard:view' },
|
||||
{ key: '/room-visual', icon: <AppstoreOutlined />, label: '宿舍总览', permission: 'room:view' },
|
||||
{ key: '/students', icon: <TeamOutlined />, label: '学生管理', permission: 'student:view' },
|
||||
{ key: '/rooms', icon: <HomeOutlined />, label: '宿舍管理', permission: 'room:view' },
|
||||
{ key: '/occupancies', icon: <SwapOutlined />, label: '入住管理', permission: 'occupancy:view' },
|
||||
{ key: '/expenses', icon: <DollarOutlined />, label: '费用录入', permission: 'expense:view' },
|
||||
{ key: '/deposits', icon: <WalletOutlined />, label: '押金管理', permission: 'deposit:view' },
|
||||
{ key: '/bills', icon: <FileTextOutlined />, label: '账单管理', permission: 'bill:view' },
|
||||
{
|
||||
key: 'classroom-group',
|
||||
icon: <ReadOutlined />,
|
||||
label: '教室管理',
|
||||
permission: 'classroom:view',
|
||||
children: [
|
||||
{ key: '/classroom-schedule', icon: <CalendarOutlined />, label: '排期总览', permission: 'classroom:view' },
|
||||
{ key: '/classrooms', icon: <ReadOutlined />, label: '教室列表', permission: 'classroom:view' },
|
||||
{ key: '/classroom-rentals', icon: <FileProtectOutlined />, label: '租赁订单', permission: 'rental:view' },
|
||||
{ key: '/tenants', icon: <TagsOutlined />, label: '租赁方', permission: 'tenant:view' },
|
||||
],
|
||||
},
|
||||
{ key: '/operation-logs', icon: <AuditOutlined />, label: '操作日志', permission: 'log:view' },
|
||||
{ key: '/roles', icon: <SafetyOutlined />, label: '角色管理', permission: 'role:view' },
|
||||
{ key: '/permissions', icon: <KeyOutlined />, label: '权限一览', permission: 'role:view' },
|
||||
{ key: '/users', icon: <SettingOutlined />, 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: <DashboardOutlined />, label: '数据面板' },
|
||||
{ key: '/room-visual', icon: <AppstoreOutlined />, label: '宿舍总览' },
|
||||
{ key: '/students', icon: <TeamOutlined />, label: '学生管理' },
|
||||
{ key: '/rooms', icon: <HomeOutlined />, label: '宿舍管理' },
|
||||
{ key: '/occupancies', icon: <SwapOutlined />, label: '入住管理' },
|
||||
{ key: '/expenses', icon: <DollarOutlined />, label: '费用录入' },
|
||||
{ key: '/deposits', icon: <WalletOutlined />, label: '押金管理' },
|
||||
{ key: '/bills', icon: <FileTextOutlined />, label: '账单管理' },
|
||||
{
|
||||
key: 'classroom-group',
|
||||
icon: <ReadOutlined />,
|
||||
label: '教室管理',
|
||||
children: [
|
||||
{ key: '/classroom-schedule', icon: <CalendarOutlined />, label: '排期总览' },
|
||||
{ key: '/classrooms', icon: <ReadOutlined />, label: '教室列表' },
|
||||
{ key: '/classroom-rentals', icon: <FileProtectOutlined />, label: '租赁订单' },
|
||||
{ key: '/tenants', icon: <TagsOutlined />, label: '租赁方' },
|
||||
],
|
||||
},
|
||||
{ key: '/operation-logs', icon: <AuditOutlined />, label: '操作日志' },
|
||||
{ key: '/users', icon: <SettingOutlined />, 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 = (
|
||||
<Menu
|
||||
theme="light"
|
||||
mode="inline"
|
||||
selectedKeys={[location.pathname]}
|
||||
items={menuItems}
|
||||
items={transformToMenuItems(menuItems)}
|
||||
onClick={({ key }) => handleMenuClick(key)}
|
||||
style={{ border: 'none' }}
|
||||
/>
|
||||
@@ -108,7 +128,6 @@ const MainLayout: React.FC = () => {
|
||||
|
||||
return (
|
||||
<Layout style={{ minHeight: '100vh' }}>
|
||||
{/* 桌面端侧边栏 */}
|
||||
{!isMobile && (
|
||||
<Sider trigger={null} collapsible collapsed={collapsed} theme="light" style={{ background: '#fff', borderRight: '1px solid #e5e5e7' }}>
|
||||
<div style={{ height: 64, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#1d1d1f', fontSize: collapsed ? 16 : 17, fontWeight: 600, borderBottom: '1px solid #e5e5e7' }}>
|
||||
@@ -117,16 +136,8 @@ const MainLayout: React.FC = () => {
|
||||
{menuContent}
|
||||
</Sider>
|
||||
)}
|
||||
{/* 移动端抽屉 */}
|
||||
{isMobile && (
|
||||
<Drawer
|
||||
placement="left"
|
||||
open={drawerOpen}
|
||||
onClose={() => setDrawerOpen(false)}
|
||||
width={240}
|
||||
styles={{ body: { padding: 0 } }}
|
||||
title="恭学教育基地"
|
||||
>
|
||||
<Drawer placement="left" open={drawerOpen} onClose={() => setDrawerOpen(false)} width={240} styles={{ body: { padding: 0 } }} title="恭学教育基地">
|
||||
{menuContent}
|
||||
</Drawer>
|
||||
)}
|
||||
|
||||
@@ -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) => (
|
||||
<Space>
|
||||
<Button size="small" type="link" onClick={() => showDetail(record.id)}>详情</Button>
|
||||
{record.status === 'draft' && <Button size="small" onClick={() => updateStatus(record.id, 'confirmed')}>确认</Button>}
|
||||
{record.status === 'confirmed' && <Button size="small" type="primary" onClick={() => updateStatus(record.id, 'paid')}>标记已付</Button>}
|
||||
<Button size="small" icon={<FilePdfOutlined />} onClick={() => handleExportPdf(record.id)}>PDF</Button>
|
||||
<Popconfirm title="确定删除此账单?" onConfirm={() => handleDelete(record.id)} okText="删除" cancelText="取消">
|
||||
<Button size="small" danger icon={<DeleteOutlined />}>删除</Button>
|
||||
</Popconfirm>
|
||||
<PermissionButton permission="bill:view" size="small" type="link" onClick={() => showDetail(record.id)}>详情</PermissionButton>
|
||||
{record.status === 'draft' && <PermissionButton permission="bill:confirm" size="small" onClick={() => updateStatus(record.id, 'confirmed')}>确认</PermissionButton>}
|
||||
{record.status === 'confirmed' && <PermissionButton permission="bill:confirm" size="small" type="primary" onClick={() => updateStatus(record.id, 'paid')}>标记已付</PermissionButton>}
|
||||
<PermissionButton permission="bill:export-pdf" size="small" icon={<FilePdfOutlined />} onClick={() => handleExportPdf(record.id)}>PDF</PermissionButton>
|
||||
<PermissionButton permission="bill:delete">
|
||||
<Popconfirm title="确定删除此账单?" onConfirm={() => handleDelete(record.id)} okText="删除" cancelText="取消">
|
||||
<Button size="small" danger icon={<DeleteOutlined />}>删除</Button>
|
||||
</Popconfirm>
|
||||
</PermissionButton>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
@@ -215,17 +218,19 @@ const BillsPage: React.FC = () => {
|
||||
{ value: 'paid', label: '已支付' },
|
||||
]}
|
||||
/>
|
||||
<Button onClick={() => batchUpdateStatus('confirmed')} disabled={selectedRows.length === 0}>批量确认</Button>
|
||||
<Button type="primary" onClick={() => batchUpdateStatus('paid')} disabled={selectedRows.length === 0}>批量标记已付</Button>
|
||||
<Popconfirm title={`确定删除选中的 ${selectedRows.length} 条账单?`} onConfirm={batchDelete} okText="删除" cancelText="取消" disabled={selectedRows.length === 0}>
|
||||
<Button danger disabled={selectedRows.length === 0} icon={<DeleteOutlined />}>批量删除</Button>
|
||||
</Popconfirm>
|
||||
<PermissionButton permission="bill:confirm" onClick={() => batchUpdateStatus('confirmed')} disabled={selectedRows.length === 0}>批量确认</PermissionButton>
|
||||
<PermissionButton permission="bill:confirm" type="primary" onClick={() => batchUpdateStatus('paid')} disabled={selectedRows.length === 0}>批量标记已付</PermissionButton>
|
||||
<PermissionButton permission="bill:delete">
|
||||
<Popconfirm title={`确定删除选中的 ${selectedRows.length} 条账单?`} onConfirm={batchDelete} okText="删除" cancelText="取消" disabled={selectedRows.length === 0}>
|
||||
<Button danger disabled={selectedRows.length === 0} icon={<DeleteOutlined />}>批量删除</Button>
|
||||
</Popconfirm>
|
||||
</PermissionButton>
|
||||
</Space>
|
||||
<Space>
|
||||
<Button type="primary" icon={<FileTextOutlined />} onClick={() => { generateForm.resetFields(); setGenerateModal(true); }}>
|
||||
<PermissionButton permission="bill:generate" type="primary" icon={<FileTextOutlined />} onClick={() => { generateForm.resetFields(); setGenerateModal(true); }}>
|
||||
生成账单
|
||||
</Button>
|
||||
<Button icon={<DownloadOutlined />} onClick={handleExportExcel}>导出Excel</Button>
|
||||
</PermissionButton>
|
||||
<PermissionButton permission="bill:export-excel" icon={<DownloadOutlined />} onClick={handleExportExcel}>导出Excel</PermissionButton>
|
||||
</Space>
|
||||
</div>
|
||||
<Table
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Table, Button, Modal, Form, Select, DatePicker, InputNumber, Input, Spa
|
||||
import { PlusOutlined, UploadOutlined, DeleteOutlined, FileTextOutlined } from '@ant-design/icons';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import api from '../../api';
|
||||
import PermissionButton from '../../components/PermissionButton';
|
||||
|
||||
const ClassroomRentalsPage: React.FC = () => {
|
||||
const [data, setData] = useState<any[]>([]);
|
||||
@@ -189,10 +190,12 @@ const ClassroomRentalsPage: React.FC = () => {
|
||||
title: '操作', width: 150,
|
||||
render: (_: any, record: any) => (
|
||||
<Space>
|
||||
<Button size="small" onClick={() => openEdit(record)}>编辑</Button>
|
||||
<Popconfirm title="确定删除该租赁订单?合同文件将一并删除。" onConfirm={() => handleDelete(record.id)}>
|
||||
<Button size="small" danger>删除</Button>
|
||||
</Popconfirm>
|
||||
<PermissionButton permission="rental:edit" size="small" onClick={() => openEdit(record)}>编辑</PermissionButton>
|
||||
<PermissionButton permission="rental:delete">
|
||||
<Popconfirm title="确定删除该租赁订单?合同文件将一并删除。" onConfirm={() => handleDelete(record.id)}>
|
||||
<Button size="small" danger>删除</Button>
|
||||
</Popconfirm>
|
||||
</PermissionButton>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
@@ -211,9 +214,9 @@ const ClassroomRentalsPage: React.FC = () => {
|
||||
/>
|
||||
<DatePicker picker="month" placeholder="按月筛选" value={filterMonth} onChange={setFilterMonth} allowClear format="YYYY-MM" />
|
||||
</Space>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => { setEditing(null); form.resetFields(); setModalOpen(true); }}>
|
||||
<PermissionButton permission="rental:create" type="primary" icon={<PlusOutlined />} onClick={() => { setEditing(null); form.resetFields(); setModalOpen(true); }}>
|
||||
新增租赁
|
||||
</Button>
|
||||
</PermissionButton>
|
||||
</div>
|
||||
<Table columns={columns} dataSource={filteredData} rowKey="id" loading={loading} pagination={{ pageSize: 15, showTotal: (total) => `共 ${total} 条` }} scroll={{ x: 1200 }} />
|
||||
|
||||
|
||||
@@ -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<string, { text: string; color: string }> = {
|
||||
available: { text: '可用', color: 'green' },
|
||||
@@ -106,15 +107,19 @@ const ClassroomsPage: React.FC = () => {
|
||||
render: (_: any, record: any) => (
|
||||
<Space>
|
||||
{record.status === 'archived' ? (
|
||||
<Popconfirm title="确定恢复此教室?" onConfirm={() => handleRestore(record.id)}>
|
||||
<Button size="small" icon={<UndoOutlined />} type="link">恢复</Button>
|
||||
</Popconfirm>
|
||||
<PermissionButton permission="classroom:edit">
|
||||
<Popconfirm title="确定恢复此教室?" onConfirm={() => handleRestore(record.id)}>
|
||||
<Button size="small" icon={<UndoOutlined />} type="link">恢复</Button>
|
||||
</Popconfirm>
|
||||
</PermissionButton>
|
||||
) : (
|
||||
<>
|
||||
<Button size="small" onClick={() => { setEditing(record); form.setFieldsValue(record); setModalOpen(true); }}>编辑</Button>
|
||||
<Popconfirm title="归档后数据保留,可随时恢复。存在进行中的租赁将无法归档。" onConfirm={() => handleArchive(record.id)} okText="归档" cancelText="取消">
|
||||
<Button size="small" icon={<InboxOutlined />}>归档</Button>
|
||||
</Popconfirm>
|
||||
<PermissionButton permission="classroom:edit" size="small" onClick={() => { setEditing(record); form.setFieldsValue(record); setModalOpen(true); }}>编辑</PermissionButton>
|
||||
<PermissionButton permission="classroom:delete">
|
||||
<Popconfirm title="归档后数据保留,可随时恢复。存在进行中的租赁将无法归档。" onConfirm={() => handleArchive(record.id)} okText="归档" cancelText="取消">
|
||||
<Button size="small" icon={<InboxOutlined />}>归档</Button>
|
||||
</Popconfirm>
|
||||
</PermissionButton>
|
||||
</>
|
||||
)}
|
||||
</Space>
|
||||
@@ -138,26 +143,28 @@ const ClassroomsPage: React.FC = () => {
|
||||
</Button>
|
||||
</Space>
|
||||
<Space wrap>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => { setEditing(null); form.resetFields(); setModalOpen(true); }}>
|
||||
<PermissionButton permission="classroom:create" type="primary" icon={<PlusOutlined />} onClick={() => { setEditing(null); form.resetFields(); setModalOpen(true); }}>
|
||||
添加教室
|
||||
</Button>
|
||||
<Upload
|
||||
accept=".xlsx,.xls"
|
||||
showUploadList={false}
|
||||
customRequest={async ({ file, onSuccess, onError }: any) => {
|
||||
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); }
|
||||
}}
|
||||
>
|
||||
<Button icon={<UploadOutlined />}>导入Excel</Button>
|
||||
</Upload>
|
||||
<Button icon={<DownloadOutlined />} onClick={handleDownloadTemplate}>下载模板</Button>
|
||||
</PermissionButton>
|
||||
<PermissionButton permission="classroom:import">
|
||||
<Upload
|
||||
accept=".xlsx,.xls"
|
||||
showUploadList={false}
|
||||
customRequest={async ({ file, onSuccess, onError }: any) => {
|
||||
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); }
|
||||
}}
|
||||
>
|
||||
<Button icon={<UploadOutlined />}>导入Excel</Button>
|
||||
</Upload>
|
||||
</PermissionButton>
|
||||
<PermissionButton permission="classroom:view" icon={<DownloadOutlined />} onClick={handleDownloadTemplate}>下载模板</PermissionButton>
|
||||
</Space>
|
||||
</div>
|
||||
<Table columns={columns} dataSource={filteredData} rowKey="id" loading={loading} pagination={{ pageSize: 20, showTotal: (total) => `共 ${total} 条` }} />
|
||||
|
||||
@@ -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<string, { text: string; color: string }> = {
|
||||
paid: { text: '已缴', color: 'green' },
|
||||
@@ -98,16 +99,18 @@ const DepositsPage: React.FC = () => {
|
||||
render: (_: any, record: any) => (
|
||||
<Space>
|
||||
{record.status === 'paid' && (
|
||||
<Button size="small" type="primary" onClick={() => {
|
||||
<PermissionButton permission="deposit:edit" size="small" type="primary" onClick={() => {
|
||||
setRefundModal(record);
|
||||
refundForm.setFieldsValue({ refundDate: dayjs(), deductionAmount: 0 });
|
||||
}}>退还</Button>
|
||||
}}>退还</PermissionButton>
|
||||
)}
|
||||
<Popconfirm title="确定删除?" onConfirm={async () => {
|
||||
try { await api.delete(`/deposits/${record.id}`); message.success('删除成功'); fetchData(); } catch (e: any) { message.error(e?.message || '删除失败'); }
|
||||
}}>
|
||||
<Button size="small" danger icon={<DeleteOutlined />} />
|
||||
</Popconfirm>
|
||||
<PermissionButton permission="deposit:delete">
|
||||
<Popconfirm title="确定删除?" onConfirm={async () => {
|
||||
try { await api.delete(`/deposits/${record.id}`); message.success('删除成功'); fetchData(); } catch (e: any) { message.error(e?.message || '删除失败'); }
|
||||
}}>
|
||||
<Button size="small" danger icon={<DeleteOutlined />} />
|
||||
</Popconfirm>
|
||||
</PermissionButton>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
@@ -138,9 +141,9 @@ const DepositsPage: React.FC = () => {
|
||||
]}
|
||||
/>
|
||||
</Space>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => { createForm.resetFields(); createForm.setFieldsValue({ amount: 500, paidDate: dayjs() }); setCreateModal(true); }}>
|
||||
<PermissionButton permission="deposit:create" type="primary" icon={<PlusOutlined />} onClick={() => { createForm.resetFields(); createForm.setFieldsValue({ amount: 500, paidDate: dayjs() }); setCreateModal(true); }}>
|
||||
收取押金
|
||||
</Button>
|
||||
</PermissionButton>
|
||||
</div>
|
||||
<Table columns={columns} dataSource={filteredData} rowKey="id" loading={loading} pagination={{ pageSize: 15, showTotal: (total) => `共 ${total} 条` }} />
|
||||
|
||||
|
||||
@@ -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) => (
|
||||
<Space>
|
||||
<Button size="small" icon={<EditOutlined />} onClick={() => {
|
||||
<PermissionButton permission="expense:edit" size="small" icon={<EditOutlined />} onClick={() => {
|
||||
setEditingRoom(record);
|
||||
roomForm.setFieldsValue({
|
||||
roomId: record.roomId,
|
||||
@@ -175,10 +176,12 @@ const ExpensesPage: React.FC = () => {
|
||||
description: record.description,
|
||||
});
|
||||
setRoomModal(true);
|
||||
}} />
|
||||
<Popconfirm title="确定删除?" onConfirm={async () => { await api.delete(`/expenses/room/${record.id}`); message.success('删除成功'); fetchData(); }}>
|
||||
<Button size="small" danger icon={<DeleteOutlined />} />
|
||||
</Popconfirm>
|
||||
}}>{''}</PermissionButton>
|
||||
<PermissionButton permission="expense:delete">
|
||||
<Popconfirm title="确定删除?" onConfirm={async () => { await api.delete(`/expenses/room/${record.id}`); message.success('删除成功'); fetchData(); }}>
|
||||
<Button size="small" danger icon={<DeleteOutlined />} />
|
||||
</Popconfirm>
|
||||
</PermissionButton>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
@@ -194,7 +197,7 @@ const ExpensesPage: React.FC = () => {
|
||||
title: '操作', width: 120,
|
||||
render: (_: any, record: any) => (
|
||||
<Space>
|
||||
<Button size="small" icon={<EditOutlined />} onClick={() => {
|
||||
<PermissionButton permission="expense:edit" size="small" icon={<EditOutlined />} onClick={() => {
|
||||
setEditingPersonal(record);
|
||||
personalForm.setFieldsValue({
|
||||
studentId: record.studentId,
|
||||
@@ -205,10 +208,12 @@ const ExpensesPage: React.FC = () => {
|
||||
description: record.description,
|
||||
});
|
||||
setPersonalModal(true);
|
||||
}} />
|
||||
<Popconfirm title="确定删除?" onConfirm={async () => { await api.delete(`/expenses/personal/${record.id}`); message.success('删除成功'); fetchData(); }}>
|
||||
<Button size="small" danger icon={<DeleteOutlined />} />
|
||||
</Popconfirm>
|
||||
}}>{''}</PermissionButton>
|
||||
<PermissionButton permission="expense:delete">
|
||||
<Popconfirm title="确定删除?" onConfirm={async () => { await api.delete(`/expenses/personal/${record.id}`); message.success('删除成功'); fetchData(); }}>
|
||||
<Button size="small" danger icon={<DeleteOutlined />} />
|
||||
</Popconfirm>
|
||||
</PermissionButton>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
@@ -239,27 +244,29 @@ const ExpensesPage: React.FC = () => {
|
||||
onChange={v => setRoomTypeFilter(v)}
|
||||
options={expenseTypeOptions}
|
||||
/>
|
||||
<Upload
|
||||
accept=".xlsx,.xls"
|
||||
showUploadList={false}
|
||||
customRequest={async ({ file, onSuccess, onError }: any) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
try {
|
||||
const res: any = await api.post('/expenses/utility/import', 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);
|
||||
}
|
||||
onSuccess?.(res);
|
||||
fetchData();
|
||||
} catch (e: any) { message.error(e?.message || '导入失败'); onError?.(e); }
|
||||
}}
|
||||
>
|
||||
<Button icon={<UploadOutlined />}>导入水电费Excel</Button>
|
||||
</Upload>
|
||||
<Button icon={<DownloadOutlined />} onClick={() => {
|
||||
<PermissionButton permission="expense:import">
|
||||
<Upload
|
||||
accept=".xlsx,.xls"
|
||||
showUploadList={false}
|
||||
customRequest={async ({ file, onSuccess, onError }: any) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
try {
|
||||
const res: any = await api.post('/expenses/utility/import', 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);
|
||||
}
|
||||
onSuccess?.(res);
|
||||
fetchData();
|
||||
} catch (e: any) { message.error(e?.message || '导入失败'); onError?.(e); }
|
||||
}}
|
||||
>
|
||||
<Button icon={<UploadOutlined />}>导入水电费Excel</Button>
|
||||
</Upload>
|
||||
</PermissionButton>
|
||||
<PermissionButton permission="expense:view" icon={<DownloadOutlined />} 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('下载失败'));
|
||||
}}>下载水电费模板</Button>
|
||||
}}>下载水电费模板</PermissionButton>
|
||||
</Space>
|
||||
<Space>
|
||||
<Popconfirm title={`确定删除选中的 ${selectedRoomKeys.length} 条费用?`} onConfirm={handleBatchDeleteRoom} okText="删除" cancelText="取消" disabled={selectedRoomKeys.length === 0}>
|
||||
<Button danger icon={<DeleteOutlined />} disabled={selectedRoomKeys.length === 0}>批量删除</Button>
|
||||
</Popconfirm>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => { setEditingRoom(null); roomForm.resetFields(); setRoomModal(true); }}>录入宿舍费用</Button>
|
||||
<PermissionButton permission="expense:delete">
|
||||
<Popconfirm title={`确定删除选中的 ${selectedRoomKeys.length} 条费用?`} onConfirm={handleBatchDeleteRoom} okText="删除" cancelText="取消" disabled={selectedRoomKeys.length === 0}>
|
||||
<Button danger icon={<DeleteOutlined />} disabled={selectedRoomKeys.length === 0}>批量删除</Button>
|
||||
</Popconfirm>
|
||||
</PermissionButton>
|
||||
<PermissionButton permission="expense:create" type="primary" icon={<PlusOutlined />} onClick={() => { setEditingRoom(null); roomForm.resetFields(); setRoomModal(true); }}>录入宿舍费用</PermissionButton>
|
||||
</Space>
|
||||
</div>
|
||||
<Table columns={roomColumns} dataSource={filteredRoomExpenses} rowKey="id" loading={loading} pagination={{ pageSize: 15, showTotal: (total) => `共 ${total} 条` }}
|
||||
@@ -310,24 +319,26 @@ const ExpensesPage: React.FC = () => {
|
||||
onChange={v => setPersonalTypeFilter(v)}
|
||||
options={personalExpenseTypeOptions}
|
||||
/>
|
||||
<Upload
|
||||
accept=".xlsx,.xls"
|
||||
showUploadList={false}
|
||||
customRequest={async ({ file, onSuccess, onError }: any) => {
|
||||
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); }
|
||||
}}
|
||||
>
|
||||
<Button icon={<UploadOutlined />}>导入个人附加费</Button>
|
||||
</Upload>
|
||||
<Button icon={<DownloadOutlined />} onClick={() => {
|
||||
<PermissionButton permission="expense:import">
|
||||
<Upload
|
||||
accept=".xlsx,.xls"
|
||||
showUploadList={false}
|
||||
customRequest={async ({ file, onSuccess, onError }: any) => {
|
||||
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); }
|
||||
}}
|
||||
>
|
||||
<Button icon={<UploadOutlined />}>导入个人附加费</Button>
|
||||
</Upload>
|
||||
</PermissionButton>
|
||||
<PermissionButton permission="expense:view" icon={<DownloadOutlined />} 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('下载失败'));
|
||||
}}>下载模板</Button>
|
||||
<Button icon={<ExportOutlined />} onClick={() => {
|
||||
}}>下载模板</PermissionButton>
|
||||
<PermissionButton permission="expense:view" icon={<ExportOutlined />} 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/export`, { headers: { Authorization: `Bearer ${token}` } })
|
||||
@@ -356,13 +367,15 @@ const ExpensesPage: React.FC = () => {
|
||||
URL.revokeObjectURL(url);
|
||||
})
|
||||
.catch(() => message.error('导出失败'));
|
||||
}}>导出</Button>
|
||||
}}>导出</PermissionButton>
|
||||
</Space>
|
||||
<Space>
|
||||
<Popconfirm title={`确定删除选中的 ${selectedPersonalKeys.length} 条个人费用?`} onConfirm={handleBatchDeletePersonal} okText="删除" cancelText="取消" disabled={selectedPersonalKeys.length === 0}>
|
||||
<Button danger icon={<DeleteOutlined />} disabled={selectedPersonalKeys.length === 0}>批量删除</Button>
|
||||
</Popconfirm>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => { setEditingPersonal(null); personalForm.resetFields(); setPersonalModal(true); }}>录入个人费用</Button>
|
||||
<PermissionButton permission="expense:delete">
|
||||
<Popconfirm title={`确定删除选中的 ${selectedPersonalKeys.length} 条个人费用?`} onConfirm={handleBatchDeletePersonal} okText="删除" cancelText="取消" disabled={selectedPersonalKeys.length === 0}>
|
||||
<Button danger icon={<DeleteOutlined />} disabled={selectedPersonalKeys.length === 0}>批量删除</Button>
|
||||
</Popconfirm>
|
||||
</PermissionButton>
|
||||
<PermissionButton permission="expense:create" type="primary" icon={<PlusOutlined />} onClick={() => { setEditingPersonal(null); personalForm.resetFields(); setPersonalModal(true); }}>录入个人费用</PermissionButton>
|
||||
</Space>
|
||||
</div>
|
||||
<Table columns={personalColumns} dataSource={filteredPersonalExpenses} rowKey="id" loading={loading} pagination={{ pageSize: 15, showTotal: (total) => `共 ${total} 条` }}
|
||||
|
||||
@@ -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<any[]>([]);
|
||||
@@ -136,15 +137,17 @@ const OccupanciesPage: React.FC = () => {
|
||||
title: '操作', width: 200,
|
||||
render: (_: any, record: any) => !record.checkOutDate ? (
|
||||
<Space>
|
||||
<Button size="small" icon={<LogoutOutlined />} onClick={() => { setCheckOutModal(record); checkOutForm.setFieldsValue({ checkOutDate: dayjs() }); }}>退宿</Button>
|
||||
<Button size="small" icon={<SwapOutlined />} onClick={() => { setTransferModal(record); transferForm.setFieldsValue({ transferDate: dayjs() }); }}>换房</Button>
|
||||
<PermissionButton permission="occupancy:checkout" size="small" icon={<LogoutOutlined />} onClick={() => { setCheckOutModal(record); checkOutForm.setFieldsValue({ checkOutDate: dayjs() }); }}>退宿</PermissionButton>
|
||||
<PermissionButton permission="occupancy:transfer" size="small" icon={<SwapOutlined />} onClick={() => { setTransferModal(record); transferForm.setFieldsValue({ transferDate: dayjs() }); }}>换房</PermissionButton>
|
||||
</Space>
|
||||
) : (
|
||||
<Space>
|
||||
<Tag>已退宿</Tag>
|
||||
<Popconfirm title="确定删除此记录?" onConfirm={async () => { try { await api.delete(`/occupancies/${record.id}`); message.success('删除成功'); fetchData(); } catch (e: any) { message.error(e?.message || '删除失败'); } }}>
|
||||
<Button size="small" danger icon={<DeleteOutlined />} />
|
||||
</Popconfirm>
|
||||
<PermissionButton permission="occupancy:delete">
|
||||
<Popconfirm title="确定删除此记录?" onConfirm={async () => { try { await api.delete(`/occupancies/${record.id}`); message.success('删除成功'); fetchData(); } catch (e: any) { message.error(e?.message || '删除失败'); } }}>
|
||||
<Button size="small" danger icon={<DeleteOutlined />} />
|
||||
</Popconfirm>
|
||||
</PermissionButton>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
@@ -174,37 +177,39 @@ const OccupanciesPage: React.FC = () => {
|
||||
<Input.Search placeholder="搜索学生姓名或房间号" onSearch={setSearchText} allowClear style={{ width: 200 }} />
|
||||
</Space>
|
||||
<Space wrap>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => { checkInForm.resetFields(); checkInForm.setFieldsValue({ checkInDate: dayjs() }); setCheckInModal(true); }}>
|
||||
<PermissionButton permission="occupancy:checkin" type="primary" icon={<PlusOutlined />} onClick={() => { checkInForm.resetFields(); checkInForm.setFieldsValue({ checkInDate: dayjs() }); setCheckInModal(true); }}>
|
||||
入住登记
|
||||
</Button>
|
||||
<Upload
|
||||
accept=".xlsx,.xls"
|
||||
showUploadList={false}
|
||||
customRequest={async ({ file, onSuccess, onError }: any) => {
|
||||
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);
|
||||
</PermissionButton>
|
||||
<PermissionButton permission="occupancy:import">
|
||||
<Upload
|
||||
accept=".xlsx,.xls"
|
||||
showUploadList={false}
|
||||
customRequest={async ({ file, onSuccess, onError }: any) => {
|
||||
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); }
|
||||
}}
|
||||
>
|
||||
<Tooltip title="导入时自动创建学生、宿舍和入住记录">
|
||||
<Button type="primary" ghost icon={<UploadOutlined />}>导入入住名单</Button>
|
||||
</Tooltip>
|
||||
</Upload>
|
||||
<Button icon={<DownloadOutlined />} onClick={() => {
|
||||
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);
|
||||
}
|
||||
onSuccess?.(res);
|
||||
fetchData();
|
||||
} catch (e: any) { message.error(e?.message || '导入失败'); onError?.(e); }
|
||||
}}
|
||||
>
|
||||
<Tooltip title="导入时自动创建学生、宿舍和入住记录">
|
||||
<Button type="primary" ghost icon={<UploadOutlined />}>导入入住名单</Button>
|
||||
</Tooltip>
|
||||
</Upload>
|
||||
</PermissionButton>
|
||||
<PermissionButton permission="occupancy:view" icon={<DownloadOutlined />} 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('下载失败'));
|
||||
}}>下载模板</Button>
|
||||
<Button icon={<ExportOutlined />} onClick={() => {
|
||||
}}>下载模板</PermissionButton>
|
||||
<PermissionButton permission="occupancy:view" icon={<ExportOutlined />} onClick={() => {
|
||||
const baseURL = import.meta.env.PROD ? '/api' : `http://localhost:${import.meta.env.VITE_API_PORT || 3002}/api`;
|
||||
const token = localStorage.getItem('token');
|
||||
const params = showActive ? '?active=true' : '';
|
||||
@@ -234,7 +239,7 @@ const OccupanciesPage: React.FC = () => {
|
||||
URL.revokeObjectURL(url);
|
||||
})
|
||||
.catch(() => message.error('导出失败'));
|
||||
}}>导出记录</Button>
|
||||
}}>导出记录</PermissionButton>
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 13 }}>
|
||||
<Switch size="small" checked={autoDeposit} onChange={setAutoDeposit} />
|
||||
导入时自动收押金
|
||||
@@ -248,11 +253,13 @@ const OccupanciesPage: React.FC = () => {
|
||||
<span>
|
||||
已选 <strong>{selectedRowKeys.length}</strong> 条记录
|
||||
{showActive ? (
|
||||
<Button type="primary" size="small" icon={<LogoutOutlined />} onClick={() => { batchCheckOutForm.resetFields(); batchCheckOutForm.setFieldsValue({ checkOutDate: dayjs() }); setBatchCheckOutModal(true); }} style={{ marginLeft: 12 }}>批量退宿</Button>
|
||||
<PermissionButton permission="occupancy:checkout" type="primary" size="small" icon={<LogoutOutlined />} onClick={() => { batchCheckOutForm.resetFields(); batchCheckOutForm.setFieldsValue({ checkOutDate: dayjs() }); setBatchCheckOutModal(true); }} style={{ marginLeft: 12 }}>批量退宿</PermissionButton>
|
||||
) : (
|
||||
<Popconfirm title={`确定删除选中的 ${selectedRowKeys.length} 条入住记录?在住记录会自动跳过`} onConfirm={handleBatchDelete} okText="删除" cancelText="取消">
|
||||
<Button danger size="small" icon={<DeleteOutlined />} style={{ marginLeft: 12 }}>批量删除</Button>
|
||||
</Popconfirm>
|
||||
<PermissionButton permission="occupancy:delete">
|
||||
<Popconfirm title={`确定删除选中的 ${selectedRowKeys.length} 条入住记录?在住记录会自动跳过`} onConfirm={handleBatchDelete} okText="删除" cancelText="取消">
|
||||
<Button danger size="small" icon={<DeleteOutlined />} style={{ marginLeft: 12 }}>批量删除</Button>
|
||||
</Popconfirm>
|
||||
</PermissionButton>
|
||||
)}
|
||||
<Button size="small" onClick={() => setSelectedRowKeys([])} style={{ marginLeft: 8 }}>取消选择</Button>
|
||||
</span>
|
||||
|
||||
@@ -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<string, { text: string; color: string }> = {
|
||||
available: { text: '可入住', color: 'green' },
|
||||
@@ -159,16 +160,20 @@ const RoomsPage: React.FC = () => {
|
||||
render: (_: any, record: any) => (
|
||||
<Space>
|
||||
{record.status === 'archived' ? (
|
||||
<Popconfirm title="确定恢复此宿舍?恢复后将重新出现在宿舍总览中。" onConfirm={() => handleRestore(record.id)} okText="恢复" cancelText="取消">
|
||||
<Button size="small" icon={<UndoOutlined />} type="link">恢复</Button>
|
||||
</Popconfirm>
|
||||
<PermissionButton permission="room:edit">
|
||||
<Popconfirm title="确定恢复此宿舍?恢复后将重新出现在宿舍总览中。" onConfirm={() => handleRestore(record.id)} okText="恢复" cancelText="取消">
|
||||
<Button size="small" icon={<UndoOutlined />} type="link">恢复</Button>
|
||||
</Popconfirm>
|
||||
</PermissionButton>
|
||||
) : (
|
||||
<>
|
||||
<Button size="small" type="link" onClick={() => showDetail(record.id)}>查看住户</Button>
|
||||
<Button size="small" onClick={() => { setEditing(record); form.setFieldsValue(record); setModalOpen(true); }}>编辑</Button>
|
||||
<Popconfirm title="归档后不会删除数据,可随时恢复。有在住人员将无法归档。" onConfirm={() => handleArchive(record.id)} okText="归档" cancelText="取消">
|
||||
<Button size="small" icon={<InboxOutlined />}>归档</Button>
|
||||
</Popconfirm>
|
||||
<PermissionButton permission="room:view" size="small" type="link" onClick={() => showDetail(record.id)}>查看住户</PermissionButton>
|
||||
<PermissionButton permission="room:edit" size="small" onClick={() => { setEditing(record); form.setFieldsValue(record); setModalOpen(true); }}>编辑</PermissionButton>
|
||||
<PermissionButton permission="room:delete">
|
||||
<Popconfirm title="归档后不会删除数据,可随时恢复。有在住人员将无法归档。" onConfirm={() => handleArchive(record.id)} okText="归档" cancelText="取消">
|
||||
<Button size="small" icon={<InboxOutlined />}>归档</Button>
|
||||
</Popconfirm>
|
||||
</PermissionButton>
|
||||
</>
|
||||
)}
|
||||
</Space>
|
||||
@@ -203,30 +208,34 @@ const RoomsPage: React.FC = () => {
|
||||
</Button>
|
||||
</Space>
|
||||
<Space wrap>
|
||||
<Popconfirm title={`确定批量归档选中的 ${selectedRowKeys.length} 间宿舍?(有在住人员的会跳过)`} onConfirm={handleBatchDelete} okText="归档" cancelText="取消" disabled={selectedRowKeys.length === 0}>
|
||||
<Button danger icon={<DeleteOutlined />} disabled={selectedRowKeys.length === 0}>批量归档</Button>
|
||||
</Popconfirm>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => { setEditing(null); form.resetFields(); setModalOpen(true); }}>
|
||||
<PermissionButton permission="room:delete">
|
||||
<Popconfirm title={`确定批量归档选中的 ${selectedRowKeys.length} 间宿舍?(有在住人员的会跳过)`} onConfirm={handleBatchDelete} okText="归档" cancelText="取消" disabled={selectedRowKeys.length === 0}>
|
||||
<Button danger icon={<DeleteOutlined />} disabled={selectedRowKeys.length === 0}>批量归档</Button>
|
||||
</Popconfirm>
|
||||
</PermissionButton>
|
||||
<PermissionButton permission="room:create" type="primary" icon={<PlusOutlined />} onClick={() => { setEditing(null); form.resetFields(); setModalOpen(true); }}>
|
||||
添加宿舍
|
||||
</Button>
|
||||
<Upload
|
||||
accept=".xlsx,.xls"
|
||||
showUploadList={false}
|
||||
customRequest={async ({ file, onSuccess, onError }: any) => {
|
||||
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); }
|
||||
}}
|
||||
>
|
||||
<Button icon={<UploadOutlined />}>导入Excel</Button>
|
||||
</Upload>
|
||||
<Button icon={<DownloadOutlined />} onClick={handleDownloadTemplate}>下载模板</Button>
|
||||
<Button icon={<ExportOutlined />} onClick={handleExport}>导出列表</Button>
|
||||
</PermissionButton>
|
||||
<PermissionButton permission="room:import">
|
||||
<Upload
|
||||
accept=".xlsx,.xls"
|
||||
showUploadList={false}
|
||||
customRequest={async ({ file, onSuccess, onError }: any) => {
|
||||
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); }
|
||||
}}
|
||||
>
|
||||
<Button icon={<UploadOutlined />}>导入Excel</Button>
|
||||
</Upload>
|
||||
</PermissionButton>
|
||||
<PermissionButton permission="room:view" icon={<DownloadOutlined />} onClick={handleDownloadTemplate}>下载模板</PermissionButton>
|
||||
<PermissionButton permission="room:view" icon={<ExportOutlined />} onClick={handleExport}>导出列表</PermissionButton>
|
||||
</Space>
|
||||
</div>
|
||||
<Table
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react';
|
||||
import { Table, Button, Modal, Form, Input, Select, Space, message, Tag, Popconfirm, Upload } from 'antd';
|
||||
import { PlusOutlined, UploadOutlined, DownloadOutlined, UndoOutlined, InboxOutlined, ExportOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||
import api from '../../api';
|
||||
import PermissionButton from '../../components/PermissionButton';
|
||||
|
||||
const statusMap: Record<string, { text: string; color: string }> = {
|
||||
active: { text: '在读', color: 'green' },
|
||||
@@ -131,15 +132,19 @@ const StudentsPage: React.FC = () => {
|
||||
render: (_: any, record: any) => (
|
||||
<Space>
|
||||
{record.status === 'archived' ? (
|
||||
<Popconfirm title="确定恢复此学生?恢复后将重新出现在学生列表中。" onConfirm={() => handleRestore(record.id)} okText="恢复" cancelText="取消">
|
||||
<Button size="small" icon={<UndoOutlined />} type="link">恢复</Button>
|
||||
</Popconfirm>
|
||||
<PermissionButton permission="student:edit">
|
||||
<Popconfirm title="确定恢复此学生?恢复后将重新出现在学生列表中。" onConfirm={() => handleRestore(record.id)} okText="恢复" cancelText="取消">
|
||||
<Button size="small" icon={<UndoOutlined />} type="link">恢复</Button>
|
||||
</Popconfirm>
|
||||
</PermissionButton>
|
||||
) : (
|
||||
<>
|
||||
<Button size="small" onClick={() => { setEditing(record); form.setFieldsValue(record); setModalOpen(true); }}>编辑</Button>
|
||||
<Popconfirm title="归档后不会删除数据,可随时恢复。确定归档?" onConfirm={() => handleArchive(record.id)} okText="归档" cancelText="取消">
|
||||
<Button size="small" icon={<InboxOutlined />}>归档</Button>
|
||||
</Popconfirm>
|
||||
<PermissionButton permission="student:edit" size="small" onClick={() => { setEditing(record); form.setFieldsValue(record); setModalOpen(true); }}>编辑</PermissionButton>
|
||||
<PermissionButton permission="student:delete">
|
||||
<Popconfirm title="归档后不会删除数据,可随时恢复。确定归档?" onConfirm={() => handleArchive(record.id)} okText="归档" cancelText="取消">
|
||||
<Button size="small" icon={<InboxOutlined />}>归档</Button>
|
||||
</Popconfirm>
|
||||
</PermissionButton>
|
||||
</>
|
||||
)}
|
||||
</Space>
|
||||
@@ -157,30 +162,34 @@ const StudentsPage: React.FC = () => {
|
||||
</Button>
|
||||
</Space>
|
||||
<Space>
|
||||
<Popconfirm title={`确定批量归档选中的 ${selectedRowKeys.length} 名学生?(数据保留,可恢复)`} onConfirm={handleBatchDelete} okText="归档" cancelText="取消" disabled={selectedRowKeys.length === 0}>
|
||||
<Button danger icon={<DeleteOutlined />} disabled={selectedRowKeys.length === 0}>批量归档</Button>
|
||||
</Popconfirm>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => { setEditing(null); form.resetFields(); setModalOpen(true); }}>
|
||||
<PermissionButton permission="student:delete">
|
||||
<Popconfirm title={`确定批量归档选中的 ${selectedRowKeys.length} 名学生?(数据保留,可恢复)`} onConfirm={handleBatchDelete} okText="归档" cancelText="取消" disabled={selectedRowKeys.length === 0}>
|
||||
<Button danger icon={<DeleteOutlined />} disabled={selectedRowKeys.length === 0}>批量归档</Button>
|
||||
</Popconfirm>
|
||||
</PermissionButton>
|
||||
<PermissionButton permission="student:create" type="primary" icon={<PlusOutlined />} onClick={() => { setEditing(null); form.resetFields(); setModalOpen(true); }}>
|
||||
添加学生
|
||||
</Button>
|
||||
<Upload
|
||||
accept=".xlsx,.xls"
|
||||
showUploadList={false}
|
||||
customRequest={async ({ file, onSuccess, onError }: any) => {
|
||||
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); }
|
||||
}}
|
||||
>
|
||||
<Button icon={<UploadOutlined />}>导入Excel</Button>
|
||||
</Upload>
|
||||
<Button icon={<DownloadOutlined />} onClick={handleDownloadTemplate}>下载模板</Button>
|
||||
<Button icon={<ExportOutlined />} onClick={handleExport}>导出名单</Button>
|
||||
</PermissionButton>
|
||||
<PermissionButton permission="student:import">
|
||||
<Upload
|
||||
accept=".xlsx,.xls"
|
||||
showUploadList={false}
|
||||
customRequest={async ({ file, onSuccess, onError }: any) => {
|
||||
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); }
|
||||
}}
|
||||
>
|
||||
<Button icon={<UploadOutlined />}>导入Excel</Button>
|
||||
</Upload>
|
||||
</PermissionButton>
|
||||
<PermissionButton permission="student:view" icon={<DownloadOutlined />} onClick={handleDownloadTemplate}>下载模板</PermissionButton>
|
||||
<PermissionButton permission="student:export" icon={<ExportOutlined />} onClick={handleExport}>导出名单</PermissionButton>
|
||||
</Space>
|
||||
</div>
|
||||
<Table
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useEffect, useState, useMemo } from 'react';
|
||||
import { Table, Button, Modal, Form, Input, Space, message, Tag, Popconfirm } from 'antd';
|
||||
import { PlusOutlined, InboxOutlined } from '@ant-design/icons';
|
||||
import api from '../../api';
|
||||
import PermissionButton from '../../components/PermissionButton';
|
||||
|
||||
const PRESET_COLORS = [
|
||||
'#ff7875', '#ffa940', '#ffc53d', '#73d13d', '#36cfc9',
|
||||
@@ -75,10 +76,12 @@ const TenantsPage: React.FC = () => {
|
||||
title: '操作', width: 150,
|
||||
render: (_: any, record: any) => (
|
||||
<Space>
|
||||
<Button size="small" onClick={() => { setEditing(record); form.setFieldsValue(record); setModalOpen(true); }}>编辑</Button>
|
||||
<Popconfirm title="归档后仍可查看历史租赁" onConfirm={() => handleArchive(record.id)} okText="归档" cancelText="取消">
|
||||
<Button size="small" icon={<InboxOutlined />}>归档</Button>
|
||||
</Popconfirm>
|
||||
<PermissionButton permission="tenant:edit" size="small" onClick={() => { setEditing(record); form.setFieldsValue(record); setModalOpen(true); }}>编辑</PermissionButton>
|
||||
<PermissionButton permission="tenant:delete">
|
||||
<Popconfirm title="归档后仍可查看历史租赁" onConfirm={() => handleArchive(record.id)} okText="归档" cancelText="取消">
|
||||
<Button size="small" icon={<InboxOutlined />}>归档</Button>
|
||||
</Popconfirm>
|
||||
</PermissionButton>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
@@ -94,9 +97,9 @@ const TenantsPage: React.FC = () => {
|
||||
onSearch={v => setSearchText(v)}
|
||||
onChange={e => { if (!e.target.value) setSearchText(''); }}
|
||||
/>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => { setEditing(null); form.resetFields(); setModalOpen(true); }}>
|
||||
<PermissionButton permission="tenant:create" type="primary" icon={<PlusOutlined />} onClick={() => { setEditing(null); form.resetFields(); setModalOpen(true); }}>
|
||||
添加租赁方
|
||||
</Button>
|
||||
</PermissionButton>
|
||||
</div>
|
||||
<Table columns={columns} dataSource={filteredData} rowKey="id" loading={loading} pagination={{ pageSize: 20, showTotal: (total) => `共 ${total} 条` }} />
|
||||
|
||||
|
||||
@@ -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<any[]>([]);
|
||||
const [roles, setRoles] = useState<any[]>([]);
|
||||
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<any[]>,
|
||||
api.get('/rbac/roles') as Promise<any[]>,
|
||||
]);
|
||||
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) => <Tag color={v === 'admin' ? 'red' : 'blue'}>{v === 'admin' ? '管理员' : '操作员'}</Tag>,
|
||||
},
|
||||
{
|
||||
title: '可见菜单', dataIndex: 'allowedMenus', width: 200, ellipsis: true,
|
||||
render: (v: string[], record: any) => record.role === 'admin' ? <Tag color="red">全部权限</Tag> : (v && v.length > 0 ? v.map((m: string) => <Tag key={m} style={{ marginBottom: 2 }}>{MENU_OPTIONS.find(o => o.value === m)?.label || m}</Tag>) : <Tag color="orange">仅基础</Tag>),
|
||||
title: '角色', dataIndex: 'roles', width: 200,
|
||||
render: (v: any[]) => v && v.length > 0
|
||||
? v.map((r: any) => <Tag key={r.id} color="blue">{r.name}</Tag>)
|
||||
: <Tag color="default">无角色</Tag>,
|
||||
},
|
||||
{
|
||||
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) => (
|
||||
<Space>
|
||||
<Button type="link" size="small" icon={<EditOutlined />} onClick={() => handleEdit(record)}>编辑</Button>
|
||||
<Button type="link" size="small" icon={<KeyOutlined />} onClick={() => handleResetPwd(record)}>重置密码</Button>
|
||||
<PermissionButton permission="user:edit" type="link" size="small" icon={<EditOutlined />} onClick={() => handleEdit(record)}>编辑</PermissionButton>
|
||||
<PermissionButton permission="user:reset-password" type="link" size="small" icon={<KeyOutlined />} onClick={() => handleResetPwd(record)}>重置密码</PermissionButton>
|
||||
{record.username !== 'admin' && (
|
||||
<Popconfirm title="确认删除该用户?" onConfirm={() => handleDelete(record.id)}>
|
||||
<Button type="link" size="small" danger icon={<DeleteOutlined />}>删除</Button>
|
||||
</Popconfirm>
|
||||
<PermissionButton permission="user:delete">
|
||||
<Popconfirm title="确认删除该用户?" onConfirm={() => handleDelete(record.id)}>
|
||||
<Button type="link" size="small" danger icon={<DeleteOutlined />}>删除</Button>
|
||||
</Popconfirm>
|
||||
</PermissionButton>
|
||||
)}
|
||||
</Space>
|
||||
),
|
||||
@@ -136,9 +130,9 @@ const UsersPage: React.FC = () => {
|
||||
<div>
|
||||
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<h2 style={{ margin: 0 }}>账号管理</h2>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>新增账号</Button>
|
||||
<PermissionButton permission="user:create" type="primary" icon={<PlusOutlined />} onClick={handleAdd}>新增账号</PermissionButton>
|
||||
</div>
|
||||
<Table columns={columns} dataSource={data} rowKey="id" loading={loading} scroll={{ x: 900 }} pagination={false} />
|
||||
<Table columns={columns} dataSource={data} rowKey="id" loading={loading} scroll={{ x: 1000 }} pagination={false} />
|
||||
|
||||
<Modal title={editing ? '编辑账号' : '新增账号'} open={modalOpen} onOk={handleSubmit} onCancel={() => setModalOpen(false)} destroyOnClose>
|
||||
<Form form={form} layout="vertical">
|
||||
@@ -154,17 +148,16 @@ const UsersPage: React.FC = () => {
|
||||
<Input />
|
||||
</Form.Item>
|
||||
{editing && (
|
||||
<>
|
||||
<Form.Item name="role" label="角色">
|
||||
<Select options={[{ value: 'admin', label: '管理员' }, { value: 'operator', label: '操作员' }]} />
|
||||
</Form.Item>
|
||||
<Form.Item name="isActive" label="状态" valuePropName="checked">
|
||||
<Switch checkedChildren="启用" unCheckedChildren="禁用" />
|
||||
</Form.Item>
|
||||
</>
|
||||
<Form.Item name="isActive" label="状态" valuePropName="checked">
|
||||
<Switch checkedChildren="启用" unCheckedChildren="禁用" />
|
||||
</Form.Item>
|
||||
)}
|
||||
<Form.Item name="allowedMenus" label="可见菜单" tooltip="管理员拥有全部权限,此设置仅对操作员生效。不勾选则仅显示基础面板。">
|
||||
<Checkbox.Group options={MENU_OPTIONS} />
|
||||
<Form.Item name="roleIds" label="角色分配" rules={[{ required: !editing, message: '请至少选择一个角色' }]}>
|
||||
<Select
|
||||
mode="multiple"
|
||||
placeholder="选择角色"
|
||||
options={roles.filter((r: any) => r.status !== 0).map((r: any) => ({ value: r.id, label: `${r.name}${r.isSystem ? ' (系统)' : ''}` }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
Reference in New Issue
Block a user