From 4f4cee157a6890a1b3de89a005f8930b20c43679 Mon Sep 17 00:00:00 2001 From: wangziqi Date: Thu, 2 Jul 2026 15:55:09 +0800 Subject: [PATCH] chore: commit oxfmt formatting changes and verify artifacts --- apps/admin/README.md | 8 +- apps/admin/src/App.css | 6 +- apps/admin/src/App.tsx | 180 ++++- .../admin/src/components/PermissionButton.tsx | 6 +- apps/admin/src/components/PermissionRoute.tsx | 8 +- apps/admin/src/hooks/usePermission.ts | 4 +- apps/admin/src/index.css | 3 +- apps/admin/src/layouts/MainLayout.tsx | 109 ++- apps/admin/src/pages/Bills/index.tsx | 347 +++++++-- .../src/pages/ClassroomRentals/index.tsx | 265 +++++-- .../src/pages/ClassroomSchedule/index.tsx | 256 +++++-- apps/admin/src/pages/Classrooms/index.tsx | 211 ++++-- apps/admin/src/pages/Dashboard/index.tsx | 148 +++- apps/admin/src/pages/Deposits/index.tsx | 163 ++++- apps/admin/src/pages/Expenses/index.tsx | 663 ++++++++++++------ apps/admin/src/pages/Login/index.tsx | 31 +- apps/admin/src/pages/Occupancies/index.tsx | 494 ++++++++++--- apps/admin/src/pages/OperationLogs/index.tsx | 120 +++- apps/admin/src/pages/Permissions/index.tsx | 60 +- apps/admin/src/pages/Roles/index.tsx | 161 ++++- apps/admin/src/pages/RoomVisual/index.tsx | 127 +++- apps/admin/src/pages/Rooms/index.tsx | 268 +++++-- apps/admin/src/pages/Students/index.tsx | 207 ++++-- apps/admin/src/pages/Tenants/index.tsx | 185 ++++- apps/admin/src/pages/Users/index.tsx | 181 ++++- apps/admin/tsconfig.json | 5 +- apps/admin/vite.config.ts | 6 +- apps/server/src/app.module.ts | 46 +- apps/server/src/auth/auth.controller.ts | 22 +- apps/server/src/auth/auth.service.ts | 2 +- .../src/auth/guards/permission.guard.ts | 10 +- apps/server/src/bills/bills-export.service.ts | 89 ++- apps/server/src/bills/bills.controller.ts | 117 +++- apps/server/src/bills/bills.module.ts | 12 +- apps/server/src/bills/bills.service.ts | 66 +- .../classroom-rentals.controller.ts | 110 ++- .../classroom-rentals.service.ts | 89 ++- .../src/classroom-rentals/dto/rental.dto.ts | 10 +- .../src/classrooms/classrooms.controller.ts | 118 +++- .../src/classrooms/classrooms.service.ts | 48 +- apps/server/src/common/request-utils.ts | 6 +- .../server/src/dashboard/dashboard.service.ts | 24 +- .../src/deposits/deposits.controller.ts | 53 +- apps/server/src/deposits/deposits.service.ts | 27 +- apps/server/src/entities/bill.entity.ts | 10 +- .../src/entities/classroom-rental.entity.ts | 11 +- apps/server/src/entities/deposit.entity.ts | 9 +- apps/server/src/entities/occupancy.entity.ts | 9 +- .../src/entities/personal-expense.entity.ts | 9 +- apps/server/src/entities/role.entity.ts | 10 +- .../src/entities/room-expense.entity.ts | 9 +- apps/server/src/entities/student.entity.ts | 9 +- apps/server/src/entities/tenant.entity.ts | 8 +- apps/server/src/entities/user.entity.ts | 10 +- .../src/expenses/expenses.controller.ts | 214 +++++- apps/server/src/expenses/expenses.module.ts | 5 +- apps/server/src/expenses/expenses.service.ts | 190 +++-- .../src/occupancies/occupancies.controller.ts | 146 +++- .../src/occupancies/occupancies.service.ts | 193 +++-- .../operation-logs/operation-logs.service.ts | 18 +- apps/server/src/rbac/rbac.controller.ts | 96 ++- apps/server/src/rbac/rbac.module.ts | 5 +- apps/server/src/rbac/rbac.service.ts | 34 +- apps/server/src/rooms/rooms.controller.ts | 146 +++- apps/server/src/rooms/rooms.service.ts | 89 ++- .../src/students/students.controller.ts | 147 +++- apps/server/src/students/students.service.ts | 69 +- apps/server/src/tenants/tenants.controller.ts | 53 +- apps/server/src/tenants/tenants.service.ts | 12 +- apps/server/test/app.e2e-spec.ts | 5 +- .../2026-07-02-migrate-to-turborepo-design.md | 116 +++ .../migrate-to-turborepo/.openspec.yaml | 2 + .../changes/migrate-to-turborepo/design.md | 112 +++ .../changes/migrate-to-turborepo/proposal.md | 31 + .../specs/monorepo-structure/spec.md | 45 ++ .../specs/oxlint-oxfmt-toolchain/spec.md | 45 ++ .../specs/turborepo-pipeline/spec.md | 38 + 77 files changed, 5576 insertions(+), 1400 deletions(-) create mode 100644 docs/superpowers/specs/2026-07-02-migrate-to-turborepo-design.md create mode 100644 openspec/changes/migrate-to-turborepo/.openspec.yaml create mode 100644 openspec/changes/migrate-to-turborepo/design.md create mode 100644 openspec/changes/migrate-to-turborepo/proposal.md create mode 100644 openspec/changes/migrate-to-turborepo/specs/monorepo-structure/spec.md create mode 100644 openspec/changes/migrate-to-turborepo/specs/oxlint-oxfmt-toolchain/spec.md create mode 100644 openspec/changes/migrate-to-turborepo/specs/turborepo-pipeline/spec.md diff --git a/apps/admin/README.md b/apps/admin/README.md index 7dbf7eb..e54697d 100644 --- a/apps/admin/README.md +++ b/apps/admin/README.md @@ -40,15 +40,15 @@ export default defineConfig([ // other options... }, }, -]) +]); ``` You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: ```js // eslint.config.js -import reactX from 'eslint-plugin-react-x' -import reactDom from 'eslint-plugin-react-dom' +import reactX from 'eslint-plugin-react-x'; +import reactDom from 'eslint-plugin-react-dom'; export default defineConfig([ globalIgnores(['dist']), @@ -69,5 +69,5 @@ export default defineConfig([ // other options... }, }, -]) +]); ``` diff --git a/apps/admin/src/App.css b/apps/admin/src/App.css index f90339d..d6c7c9a 100644 --- a/apps/admin/src/App.css +++ b/apps/admin/src/App.css @@ -42,8 +42,7 @@ z-index: 1; top: 34px; height: 28px; - transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg) - scale(1.4); + transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg) scale(1.4); } .vite { @@ -51,8 +50,7 @@ top: 107px; height: 26px; width: auto; - transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg) - scale(0.8); + transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg) scale(0.8); } } diff --git a/apps/admin/src/App.tsx b/apps/admin/src/App.tsx index 6c46d93..a4d4489 100644 --- a/apps/admin/src/App.tsx +++ b/apps/admin/src/App.tsx @@ -29,32 +29,162 @@ const PrivateRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => const App: React.FC = () => { return ( - + - - - } /> - }> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - - + + + } /> + + + + } + > + } /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + ); diff --git a/apps/admin/src/components/PermissionButton.tsx b/apps/admin/src/components/PermissionButton.tsx index 49ad1f9..26c2c0a 100644 --- a/apps/admin/src/components/PermissionButton.tsx +++ b/apps/admin/src/components/PermissionButton.tsx @@ -8,7 +8,11 @@ interface PermissionButtonProps extends ButtonProps { children: React.ReactNode; } -const PermissionButton: React.FC = ({ permission, children, ...btnProps }) => { +const PermissionButton: React.FC = ({ + permission, + children, + ...btnProps +}) => { const { hasPermission } = usePermission(); if (!hasPermission(permission)) return null; return ; diff --git a/apps/admin/src/components/PermissionRoute.tsx b/apps/admin/src/components/PermissionRoute.tsx index 848c52c..0e24234 100644 --- a/apps/admin/src/components/PermissionRoute.tsx +++ b/apps/admin/src/components/PermissionRoute.tsx @@ -10,13 +10,7 @@ interface PermissionRouteProps { const PermissionRoute: React.FC = ({ permission, children }) => { const { hasPermission } = usePermission(); if (!hasPermission(permission)) { - return ( - - ); + return ; } return <>{children}; }; diff --git a/apps/admin/src/hooks/usePermission.ts b/apps/admin/src/hooks/usePermission.ts index 214a00f..bec42d8 100644 --- a/apps/admin/src/hooks/usePermission.ts +++ b/apps/admin/src/hooks/usePermission.ts @@ -12,10 +12,10 @@ export function usePermission() { const hasPermission = (code: string): boolean => permissions.includes(code); const hasAnyPermission = (...codes: string[]): boolean => - codes.some(c => permissions.includes(c)); + codes.some((c) => permissions.includes(c)); const hasAllPermissions = (...codes: string[]): boolean => - codes.every(c => permissions.includes(c)); + codes.every((c) => permissions.includes(c)); return { permissions, hasPermission, hasAnyPermission, hasAllPermissions }; } diff --git a/apps/admin/src/index.css b/apps/admin/src/index.css index fde2c98..c922edd 100644 --- a/apps/admin/src/index.css +++ b/apps/admin/src/index.css @@ -5,7 +5,8 @@ } body { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + font-family: + -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } diff --git a/apps/admin/src/layouts/MainLayout.tsx b/apps/admin/src/layouts/MainLayout.tsx index b1d7850..0fda368 100644 --- a/apps/admin/src/layouts/MainLayout.tsx +++ b/apps/admin/src/layouts/MainLayout.tsx @@ -36,7 +36,12 @@ interface MenuItemType { } const allMenuItems: MenuItemType[] = [ - { key: '/dashboard', icon: , label: '数据面板', permission: 'dashboard:view' }, + { + 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' }, @@ -50,9 +55,24 @@ const allMenuItems: MenuItemType[] = [ 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: '/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' }, ], }, @@ -80,7 +100,7 @@ const MainLayout: React.FC = () => { // 按 permission 过滤菜单 const filterByPermission = (items: MenuItemType[]): MenuItemType[] => { return items - .map(item => { + .map((item) => { if (item.children) { const kids = filterByPermission(item.children); if (kids.length === 0) return null; @@ -107,7 +127,7 @@ const MainLayout: React.FC = () => { }; const transformToMenuItems = (items: MenuItemType[]): any[] => { - return items.map(item => ({ + return items.map((item) => ({ key: item.key, icon: item.icon, label: item.label, @@ -129,33 +149,94 @@ const MainLayout: React.FC = () => { return ( {!isMobile && ( - -
+ +
{collapsed ? '恭' : '恭学教育基地'}
{menuContent}
)} {isMobile && ( - setDrawerOpen(false)} width={240} styles={{ body: { padding: 0 } }} title="恭学教育基地"> + setDrawerOpen(false)} + width={240} + styles={{ body: { padding: 0 } }} + title="恭学教育基地" + > {menuContent} )} -
+
- + diff --git a/apps/admin/src/pages/Bills/index.tsx b/apps/admin/src/pages/Bills/index.tsx index e38e2cc..c628e5d 100644 --- a/apps/admin/src/pages/Bills/index.tsx +++ b/apps/admin/src/pages/Bills/index.tsx @@ -1,6 +1,25 @@ import React, { useEffect, useState, useMemo } from 'react'; -import { Table, Button, Modal, Form, DatePicker, Space, message, Tag, Descriptions, Popconfirm, Input, Select, Tooltip } from 'antd'; -import { FileTextOutlined, DeleteOutlined, DownloadOutlined, FilePdfOutlined } from '@ant-design/icons'; +import { + Table, + Button, + Modal, + Form, + DatePicker, + Space, + message, + Tag, + Descriptions, + Popconfirm, + Input, + Select, + Tooltip, +} from 'antd'; +import { + FileTextOutlined, + DeleteOutlined, + DownloadOutlined, + FilePdfOutlined, +} from '@ant-design/icons'; import dayjs from 'dayjs'; import api from '../../api'; import PermissionButton from '../../components/PermissionButton'; @@ -13,7 +32,14 @@ const statusMap: Record = { paid: { text: '已支付', color: 'green' }, }; -const typeMap: Record = { water: '水费', electricity: '电费', cleaning: '保洁费', damage: '损坏赔偿', penalty: '罚款', other: '其他' }; +const typeMap: Record = { + water: '水费', + electricity: '电费', + cleaning: '保洁费', + damage: '损坏赔偿', + penalty: '罚款', + other: '其他', +}; const BillsPage: React.FC = () => { const [bills, setBills] = useState([]); @@ -30,11 +56,15 @@ const BillsPage: React.FC = () => { try { const res: any = await api.get('/bills'); setBills(res); - } catch (e) { console.error(e); } + } catch (e) { + console.error(e); + } setLoading(false); }; - useEffect(() => { fetchData(); }, []); + useEffect(() => { + fetchData(); + }, []); const filteredBills = useMemo(() => { return bills.filter((b: any) => { @@ -60,14 +90,18 @@ const BillsPage: React.FC = () => { setGenerateModal(false); generateForm.resetFields(); fetchData(); - } catch (e: any) { message.error(e?.message || '生成失败'); } + } catch (e: any) { + message.error(e?.message || '生成失败'); + } }; const showDetail = async (id: number) => { try { const res = await api.get(`/bills/${id}`); setDetailModal(res); - } catch (e) { console.error(e); } + } catch (e) { + console.error(e); + } }; const updateStatus = async (id: number, status: string) => { @@ -78,7 +112,9 @@ const BillsPage: React.FC = () => { if (detailModal?.id === id) { setDetailModal({ ...detailModal, status }); } - } catch (e: any) { message.error(e?.message || '操作失败'); } + } catch (e: any) { + message.error(e?.message || '操作失败'); + } }; const batchUpdateStatus = async (status: string) => { @@ -88,7 +124,9 @@ const BillsPage: React.FC = () => { message.success(`已批量更新 ${selectedRows.length} 条账单`); setSelectedRows([]); fetchData(); - } catch (e: any) { message.error(e?.message || '操作失败'); } + } catch (e: any) { + message.error(e?.message || '操作失败'); + } }; const handleDelete = async (id: number) => { @@ -96,7 +134,9 @@ const BillsPage: React.FC = () => { await api.delete(`/bills/${id}`); message.success('账单已删除'); fetchData(); - } catch (e: any) { message.error(e?.message || '删除失败'); } + } catch (e: any) { + message.error(e?.message || '删除失败'); + } }; const batchDelete = async () => { @@ -106,18 +146,22 @@ const BillsPage: React.FC = () => { message.success(`已删除 ${selectedRows.length} 条账单`); setSelectedRows([]); fetchData(); - } catch (e: any) { message.error(e?.message || '操作失败'); } + } catch (e: any) { + message.error(e?.message || '操作失败'); + } }; const handleExportExcel = () => { - const baseURL = import.meta.env.PROD ? '/api' : `http://localhost:${import.meta.env.VITE_API_PORT || 3002}/api`; + const baseURL = import.meta.env.PROD + ? '/api' + : `http://localhost:${import.meta.env.VITE_API_PORT || 3002}/api`; const token = localStorage.getItem('token'); const url = `${baseURL}/bills/export/excel`; const a = document.createElement('a'); // 使用 fetch 来携带 token fetch(url, { headers: { Authorization: `Bearer ${token}` } }) - .then(res => res.blob()) - .then(blob => { + .then((res) => res.blob()) + .then((blob) => { const blobUrl = URL.createObjectURL(blob); a.href = blobUrl; a.download = `账单导出_${dayjs().format('YYYYMMDD_HHmmss')}.xlsx`; @@ -129,11 +173,15 @@ const BillsPage: React.FC = () => { }; const handleExportPdf = (billId: number) => { - const baseURL = import.meta.env.PROD ? '/api' : `http://localhost:${import.meta.env.VITE_API_PORT || 3002}/api`; + const baseURL = import.meta.env.PROD + ? '/api' + : `http://localhost:${import.meta.env.VITE_API_PORT || 3002}/api`; const token = localStorage.getItem('token'); - fetch(`${baseURL}/bills/export/pdf/${billId}`, { headers: { Authorization: `Bearer ${token}` } }) - .then(res => res.blob()) - .then(blob => { + fetch(`${baseURL}/bills/export/pdf/${billId}`, { + headers: { Authorization: `Bearer ${token}` }, + }) + .then((res) => res.blob()) + .then((blob) => { const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; @@ -147,15 +195,30 @@ const BillsPage: React.FC = () => { const columns = [ { title: '学生', render: (_: any, r: any) => r.student?.name || '-' }, { title: '账单周期', render: (_: any, r: any) => `${r.periodStart} ~ ${r.periodEnd}` }, - { title: '分摊费用', dataIndex: 'sharedAmount', render: (v: number) => `¥${Number(v).toFixed(2)}` }, - { title: '个人费用', dataIndex: 'personalAmount', render: (v: number) => `¥${Number(v).toFixed(2)}` }, - { title: '总计', dataIndex: 'totalAmount', render: (v: number) => ¥{Number(v).toFixed(2)} }, + { + title: '分摊费用', + dataIndex: 'sharedAmount', + render: (v: number) => `¥${Number(v).toFixed(2)}`, + }, + { + title: '个人费用', + dataIndex: 'personalAmount', + render: (v: number) => `¥${Number(v).toFixed(2)}`, + }, + { + title: '总计', + dataIndex: 'totalAmount', + render: (v: number) => ¥{Number(v).toFixed(2)}, + }, { title: '可用押金', dataIndex: 'availableDeposit', - render: (v: number) => v > 0 - ? ¥{Number(v).toFixed(2)} - : -, + render: (v: number) => + v > 0 ? ( + ¥{Number(v).toFixed(2)} + ) : ( + - + ), }, { title: '抵扣后应付', @@ -173,21 +236,65 @@ const BillsPage: React.FC = () => { }, }, { - title: '状态', dataIndex: 'status', + title: '状态', + dataIndex: 'status', render: (s: string) => {statusMap[s]?.text}, }, - { title: '生成时间', dataIndex: 'generatedAt', render: (v: string) => dayjs(v).format('YYYY-MM-DD HH:mm') }, { - title: '操作', width: 320, + title: '生成时间', + dataIndex: 'generatedAt', + render: (v: string) => dayjs(v).format('YYYY-MM-DD HH:mm'), + }, + { + title: '操作', + width: 320, render: (_: any, record: any) => ( - showDetail(record.id)}>详情 - {record.status === 'draft' && updateStatus(record.id, 'confirmed')}>确认} - {record.status === 'confirmed' && updateStatus(record.id, 'paid')}>标记已付} - } onClick={() => handleExportPdf(record.id)}>PDF + 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="取消"> - + handleDelete(record.id)} + okText="删除" + cancelText="取消" + > + @@ -197,40 +304,85 @@ const BillsPage: React.FC = () => { return (
-
+
setSearchText(v)} - onChange={e => { if (!e.target.value) setSearchText(''); }} + onSearch={(v) => setSearchText(v)} + onChange={(e) => { + if (!e.target.value) setSearchText(''); + }} /> ({ value: c.id, label: `${c.building ? c.building + ' · ' : ''}${c.name}(${c.roomType})` }))} + options={classrooms.map((c) => ({ + value: c.id, + label: `${c.building ? c.building + ' · ' : ''}${c.name}(${c.roomType})`, + }))} /> @@ -235,11 +352,15 @@ const ClassroomRentalsPage: React.FC = () => { showSearch optionFilterProp="label" placeholder="选择租赁方" - options={tenants.map(t => ({ value: t.id, label: t.name }))} + options={tenants.map((t) => ({ value: t.id, label: t.name }))} /> - + @@ -247,7 +368,9 @@ const ClassroomRentalsPage: React.FC = () => { - + + +
diff --git a/apps/admin/src/pages/ClassroomSchedule/index.tsx b/apps/admin/src/pages/ClassroomSchedule/index.tsx index be37dd1..6c8d48e 100644 --- a/apps/admin/src/pages/ClassroomSchedule/index.tsx +++ b/apps/admin/src/pages/ClassroomSchedule/index.tsx @@ -1,5 +1,18 @@ import React, { useEffect, useState, useMemo } from 'react'; -import { DatePicker, Card, Row, Col, Statistic, Tag, Space, Button, Modal, Spin, Empty, Tooltip } from 'antd'; +import { + DatePicker, + Card, + Row, + Col, + Statistic, + Tag, + Space, + Button, + Modal, + Spin, + Empty, + Tooltip, +} from 'antd'; import { CalendarOutlined, FileTextOutlined } from '@ant-design/icons'; import dayjs, { Dayjs } from 'dayjs'; import api from '../../api'; @@ -11,7 +24,10 @@ interface ScheduleData { classrooms: any[]; tenants: any[]; matrix: Record>; - summary: Record; + summary: Record< + number, + { totalDays: number; rentedDays: number; idleDays: number; occupancyRate: number } + >; } const ClassroomSchedulePage: React.FC = () => { @@ -27,11 +43,15 @@ const ClassroomSchedulePage: React.FC = () => { params: { year: month.year(), month: month.month() + 1 }, }); setData(res); - } catch (e) { console.error(e); } + } catch (e) { + console.error(e); + } setLoading(false); }; - useEffect(() => { fetchData(); }, [month]); + useEffect(() => { + fetchData(); + }, [month]); // 按楼栋+楼层分组教室 const groups = useMemo(() => { @@ -64,15 +84,21 @@ const ClassroomSchedulePage: React.FC = () => { try { const res: any = await api.get(`/classroom-rentals/${rentalId}`); setDetailModal(res); - } catch (e) { console.error(e); } + } catch (e) { + console.error(e); + } }; const handleDownloadContract = (id: number, filename?: string) => { - const baseURL = import.meta.env.PROD ? '/api' : `http://localhost:${import.meta.env.VITE_API_PORT || 3002}/api`; + const baseURL = import.meta.env.PROD + ? '/api' + : `http://localhost:${import.meta.env.VITE_API_PORT || 3002}/api`; const token = localStorage.getItem('token'); - fetch(`${baseURL}/classroom-rentals/${id}/contract`, { headers: { Authorization: `Bearer ${token}` } }) - .then(res => res.blob()) - .then(blob => { + fetch(`${baseURL}/classroom-rentals/${id}/contract`, { + headers: { Authorization: `Bearer ${token}` }, + }) + .then((res) => res.blob()) + .then((blob) => { const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; @@ -84,33 +110,80 @@ const ClassroomSchedulePage: React.FC = () => { return (
-
+

教室排期总览

- v && setMonth(v)} allowClear={false} placeholder="选择月份" format="YYYY年M月" /> + v && setMonth(v)} + allowClear={false} + placeholder="选择月份" + format="YYYY年M月" + /> - +
{/* 统计卡片 */} - - - - 70 ? '#cf1322' : overall.rate > 40 ? '#fa8c16' : '#3f8600' }} /> + + + + + + + + + + + + + + + + + + 70 ? '#cf1322' : overall.rate > 40 ? '#fa8c16' : '#3f8600', + }} + /> + + {/* 租赁方图例 */} {data && data.tenants.length > 0 && ( - {data.tenants.map(t => ( - {t.name} + {data.tenants.map((t) => ( + + {t.name} + ))} @@ -121,7 +194,7 @@ const ClassroomSchedulePage: React.FC = () => { ) : (
- {groups.map(group => ( + {groups.map((group) => ( { - - - - {Array.from({ length: data.days }, (_, i) => i + 1).map(d => ( - + + + + {Array.from({ length: data.days }, (_, i) => i + 1).map((d) => ( + ))} - {group.classrooms.map(c => { - const sum = data.summary[c.id] || { rentedDays: 0, totalDays: data.days, occupancyRate: 0 }; + {group.classrooms.map((c) => { + const sum = data.summary[c.id] || { + rentedDays: 0, + totalDays: data.days, + occupancyRate: 0, + }; return ( - - - + + - {Array.from({ length: data.days }, (_, i) => i + 1).map(d => { + {Array.from({ length: data.days }, (_, i) => i + 1).map((d) => { const cell = data.matrix[c.id]?.[d]; return (
教室类型占用率{d} + 教室 + + 类型 + + 占用率 + + {d} +
{c.name}{c.roomType} 0.7 ? '#cf1322' : sum.occupancyRate > 0.4 ? '#fa8c16' : '#3f8600' }}> + + {c.name} + + {c.roomType} + 0.7 + ? '#cf1322' + : sum.occupancyRate > 0.4 + ? '#fa8c16' + : '#3f8600', + }} + > {Math.round(sum.occupancyRate * 100)}% { }} > {cell && ( - + {cell.hasContract ? '📄' : ''} @@ -195,28 +333,64 @@ const ClassroomSchedulePage: React.FC = () => { > {detailModal && (
-
教室:{detailModal.classroom?.building} · {detailModal.classroom?.name}({detailModal.classroom?.roomType})
-
租赁方: - +
+ 教室: + {detailModal.classroom?.building} · {detailModal.classroom?.name}( + {detailModal.classroom?.roomType}) +
+
+ 租赁方: + {detailModal.tenant?.name}
-
联系人:{detailModal.tenant?.contact || '-'} {detailModal.tenant?.phone || ''}
-
起止日期:{detailModal.startDate} ~ {detailModal.endDate}({dayjs(detailModal.endDate).diff(dayjs(detailModal.startDate), 'day') + 1}天)
- {detailModal.dailyRate &&
日租金:¥{detailModal.dailyRate}
} - {detailModal.totalAmount &&
合同总额:¥{detailModal.totalAmount}
} - {detailModal.notes &&
备注:{detailModal.notes}
} +
+ 联系人: + {detailModal.tenant?.contact || '-'} {detailModal.tenant?.phone || ''} +
+
+ 起止日期: + {detailModal.startDate} ~ {detailModal.endDate}( + {dayjs(detailModal.endDate).diff(dayjs(detailModal.startDate), 'day') + 1}天) +
+ {detailModal.dailyRate && ( +
+ 日租金:¥{detailModal.dailyRate} +
+ )} + {detailModal.totalAmount && ( +
+ 合同总额:¥{detailModal.totalAmount} +
+ )} + {detailModal.notes && ( +
+ 备注: + {detailModal.notes} +
+ )}
合同文件: {detailModal.contractPath ? ( - ) : '未上传'} + ) : ( + '未上传' + )}
)} diff --git a/apps/admin/src/pages/Classrooms/index.tsx b/apps/admin/src/pages/Classrooms/index.tsx index 96e6f7e..9c37ad9 100644 --- a/apps/admin/src/pages/Classrooms/index.tsx +++ b/apps/admin/src/pages/Classrooms/index.tsx @@ -1,6 +1,25 @@ 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 { + 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'; @@ -27,7 +46,9 @@ const ClassroomsPage: React.FC = () => { const filteredData = useMemo(() => { if (!searchText) return data; const s = searchText.toLowerCase(); - return data.filter((d: any) => d.name?.toLowerCase().includes(s) || d.building?.toLowerCase().includes(s)); + return data.filter( + (d: any) => d.name?.toLowerCase().includes(s) || d.building?.toLowerCase().includes(s), + ); }, [data, searchText]); const fetchData = async () => { @@ -35,11 +56,15 @@ const ClassroomsPage: React.FC = () => { try { const res: any = await api.get('/classrooms', { params: { includeArchived: showArchived } }); setData(res); - } catch (e) { console.error(e); } + } catch (e) { + console.error(e); + } setLoading(false); }; - useEffect(() => { fetchData(); }, [showArchived]); + useEffect(() => { + fetchData(); + }, [showArchived]); const handleSave = async () => { const values = await form.validateFields(); @@ -55,7 +80,9 @@ const ClassroomsPage: React.FC = () => { form.resetFields(); setEditing(null); fetchData(); - } catch (e: any) { message.error(e?.message || '操作失败'); } + } catch (e: any) { + message.error(e?.message || '操作失败'); + } }; const handleArchive = async (id: number) => { @@ -63,7 +90,9 @@ const ClassroomsPage: React.FC = () => { await api.delete(`/classrooms/${id}`); message.success('已归档'); fetchData(); - } catch (e: any) { message.error(e?.message || '归档失败'); } + } catch (e: any) { + message.error(e?.message || '归档失败'); + } }; const handleRestore = async (id: number) => { @@ -71,15 +100,19 @@ const ClassroomsPage: React.FC = () => { await api.put(`/classrooms/${id}/restore`); message.success('已恢复'); fetchData(); - } catch (e: any) { message.error(e?.message || '恢复失败'); } + } catch (e: any) { + message.error(e?.message || '恢复失败'); + } }; const handleDownloadTemplate = () => { - const baseURL = import.meta.env.PROD ? '/api' : `http://localhost:${import.meta.env.VITE_API_PORT || 3002}/api`; + const baseURL = import.meta.env.PROD + ? '/api' + : `http://localhost:${import.meta.env.VITE_API_PORT || 3002}/api`; const token = localStorage.getItem('token'); fetch(`${baseURL}/classrooms/template`, { headers: { Authorization: `Bearer ${token}` } }) - .then(res => res.blob()) - .then(blob => { + .then((res) => res.blob()) + .then((blob) => { const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; @@ -91,33 +124,62 @@ const ClassroomsPage: React.FC = () => { }; const columns = [ - { title: '教室名', dataIndex: 'name', sorter: (a: any, b: any) => a.name.localeCompare(b.name) }, + { + title: '教室名', + dataIndex: 'name', + sorter: (a: any, b: any) => a.name.localeCompare(b.name), + }, { title: '楼栋', dataIndex: 'building' }, { title: '楼层', dataIndex: 'floor' }, - { title: '类型', dataIndex: 'roomType', render: (v: string) => {v || '-'} }, + { + title: '类型', + dataIndex: 'roomType', + render: (v: string) => {v || '-'}, + }, { title: '容量', dataIndex: 'capacity' }, { title: '课程类型', dataIndex: 'courseType', render: (v: string) => v || '-' }, { title: '负责人', dataIndex: 'supervisor', render: (v: string) => v || '-' }, { - title: '状态', dataIndex: 'status', + title: '状态', + dataIndex: 'status', render: (s: string) => {statusMap[s]?.text || s}, }, { - title: '操作', width: 180, + title: '操作', + width: 180, render: (_: any, record: any) => ( {record.status === 'archived' ? ( handleRestore(record.id)}> - + ) : ( <> - { setEditing(record); form.setFieldsValue(record); setModalOpen(true); }}>编辑 + { + setEditing(record); + form.setFieldsValue(record); + setModalOpen(true); + }} + > + 编辑 + - handleArchive(record.id)} okText="归档" cancelText="取消"> - + handleArchive(record.id)} + okText="归档" + cancelText="取消" + > + @@ -129,21 +191,43 @@ const ClassroomsPage: React.FC = () => { return (
-
+
setSearchText(v)} - onChange={e => { if (!e.target.value) setSearchText(''); }} + onSearch={(v) => setSearchText(v)} + onChange={(e) => { + if (!e.target.value) setSearchText(''); + }} /> - - } onClick={() => { setEditing(null); form.resetFields(); setModalOpen(true); }}> + } + onClick={() => { + setEditing(null); + form.resetFields(); + setModalOpen(true); + }} + > 添加教室 @@ -154,37 +238,80 @@ 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' } }); + 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); } + } catch (e: any) { + message.error(e?.message || '导入失败'); + onError?.(e); + } }} > - } onClick={handleDownloadTemplate}>下载模板 + } + onClick={handleDownloadTemplate} + > + 下载模板 +
- `共 ${total} 条` }} /> +
`共 ${total} 条` }} + /> - { setModalOpen(false); setEditing(null); }} okText="保存"> + { + setModalOpen(false); + setEditing(null); + }} + okText="保存" + >
- - - - - + + + + + + + + + + + + + + + - - - -
diff --git a/apps/admin/src/pages/Dashboard/index.tsx b/apps/admin/src/pages/Dashboard/index.tsx index 1f49123..d5b5639 100644 --- a/apps/admin/src/pages/Dashboard/index.tsx +++ b/apps/admin/src/pages/Dashboard/index.tsx @@ -7,7 +7,16 @@ import api from '../../api'; const { RangePicker } = DatePicker; -const COLORS = ['#007AFF', '#34C759', '#FF9500', '#FF3B30', '#5AC8FA', '#AF52DE', '#FF2D55', '#FFCC00']; +const COLORS = [ + '#007AFF', + '#34C759', + '#FF9500', + '#FF3B30', + '#5AC8FA', + '#AF52DE', + '#FF2D55', + '#FFCC00', +]; const DashboardPage: React.FC = () => { const [stats, setStats] = useState(null); @@ -26,21 +35,37 @@ const DashboardPage: React.FC = () => { const [s, g, e, r] = await Promise.all([ api.get('/dashboard/stats'), api.get('/dashboard/gantt', { params: { periodStart: period[0], periodEnd: period[1] } }), - api.get('/dashboard/expense-stats', { params: { periodStart: period[0], periodEnd: period[1] } }), - api.get('/dashboard/room-ranking', { params: { periodStart: period[0], periodEnd: period[1] } }), + api.get('/dashboard/expense-stats', { + params: { periodStart: period[0], periodEnd: period[1] }, + }), + api.get('/dashboard/room-ranking', { + params: { periodStart: period[0], periodEnd: period[1] }, + }), ]); setStats(s); setGanttData(g as any); setExpenseStats(e as any); setRoomRanking(r as any); - } catch (e) { console.error(e); } + } catch (e) { + console.error(e); + } setLoading(false); }; - useEffect(() => { fetchData(); }, [period]); + useEffect(() => { + fetchData(); + }, [period]); const expenseTypeMap: Record = { - water: '水费', electricity: '电费', cleaning: '保洁费', damage: '损坏赔偿', penalty: '罚款', key: '钥匙费', remote: '空调遥控器', deposit_deduction: '押金扣除', other: '其他', + water: '水费', + electricity: '电费', + cleaning: '保洁费', + damage: '损坏赔偿', + penalty: '罚款', + key: '钥匙费', + remote: '空调遥控器', + deposit_deduction: '押金扣除', + other: '其他', }; // 甘特图配置 @@ -73,22 +98,24 @@ const DashboardPage: React.FC = () => { grid: { left: 80, right: 30, top: 20, bottom: 30 }, xAxis: { type: 'time', min: pStart, max: pEnd }, yAxis: { type: 'category', data: rooms, inverse: true }, - series: [{ - type: 'custom', - renderItem: (_params: any, api: any) => { - const catIdx = api.value(0); - const start = api.coord([api.value(1), catIdx]); - const end = api.coord([api.value(2), catIdx]); - const height = api.size([0, 1])[1] * 0.6; - return { - type: 'rect', - shape: { x: start[0], y: start[1] - height / 2, width: end[0] - start[0], height }, - style: { ...api.style(), fill: api.visual('color'), stroke: '#fff', lineWidth: 1 }, - }; + series: [ + { + type: 'custom', + renderItem: (_params: any, api: any) => { + const catIdx = api.value(0); + const start = api.coord([api.value(1), catIdx]); + const end = api.coord([api.value(2), catIdx]); + const height = api.size([0, 1])[1] * 0.6; + return { + type: 'rect', + shape: { x: start[0], y: start[1] - height / 2, width: end[0] - start[0], height }, + style: { ...api.style(), fill: api.visual('color'), stroke: '#fff', lineWidth: 1 }, + }; + }, + encode: { x: [1, 2], y: 0 }, + data, }, - encode: { x: [1, 2], y: 0 }, - data, - }], + ], }; }; @@ -96,14 +123,16 @@ const DashboardPage: React.FC = () => { const pieOption = { tooltip: { trigger: 'item' }, legend: { bottom: 0 }, - series: [{ - type: 'pie', - radius: ['40%', '70%'], - data: expenseStats.map((e) => ({ - name: expenseTypeMap[e.type] || e.type, - value: Number(e.total), - })), - }], + series: [ + { + type: 'pie', + radius: ['40%', '70%'], + data: expenseStats.map((e) => ({ + name: expenseTypeMap[e.type] || e.type, + value: Number(e.total), + })), + }, + ], }; // 宿舍费用排行 @@ -111,15 +140,33 @@ const DashboardPage: React.FC = () => { tooltip: {}, grid: { left: 80, right: 20, bottom: 30, top: 10 }, xAxis: { type: 'value' }, - yAxis: { type: 'category', data: roomRanking.map((r) => r.roomNumber).reverse(), inverse: false }, - series: [{ type: 'bar', data: roomRanking.map((r) => Number(r.total)).reverse(), itemStyle: { color: '#007AFF', borderRadius: [0, 4, 4, 0] } }], + yAxis: { + type: 'category', + data: roomRanking.map((r) => r.roomNumber).reverse(), + inverse: false, + }, + series: [ + { + type: 'bar', + data: roomRanking.map((r) => Number(r.total)).reverse(), + itemStyle: { color: '#007AFF', borderRadius: [0, 4, 4, 0] }, + }, + ], }; - if (loading && !stats) return ; + if (loading && !stats) + return ; return (
-
+

数据面板

{
- } /> + + } /> + - } /> + + } + /> + - } /> + + } + /> + - } /> + + } + /> + {ganttData.length > 0 ? ( - + ) : (
暂无入住数据
)} diff --git a/apps/admin/src/pages/Deposits/index.tsx b/apps/admin/src/pages/Deposits/index.tsx index 9912d8c..8c66b55 100644 --- a/apps/admin/src/pages/Deposits/index.tsx +++ b/apps/admin/src/pages/Deposits/index.tsx @@ -1,5 +1,18 @@ import React, { useEffect, useState, useMemo } from 'react'; -import { Table, Button, Modal, Form, Select, DatePicker, InputNumber, Input, Space, message, Tag, Popconfirm } from 'antd'; +import { + Table, + Button, + Modal, + Form, + Select, + DatePicker, + InputNumber, + Input, + Space, + message, + Tag, + Popconfirm, +} from 'antd'; import { PlusOutlined, DeleteOutlined } from '@ant-design/icons'; import dayjs from 'dayjs'; import api from '../../api'; @@ -26,17 +39,18 @@ const DepositsPage: React.FC = () => { const fetchData = async () => { setLoading(true); try { - const [d, s]: any[] = await Promise.all([ - api.get('/deposits'), - api.get('/students'), - ]); + const [d, s]: any[] = await Promise.all([api.get('/deposits'), api.get('/students')]); setData(d); setStudents(s); - } catch (e) { console.error(e); } + } catch (e) { + console.error(e); + } setLoading(false); }; - useEffect(() => { fetchData(); }, []); + useEffect(() => { + fetchData(); + }, []); const filteredData = useMemo(() => { return data.filter((d: any) => { @@ -62,7 +76,9 @@ const DepositsPage: React.FC = () => { setCreateModal(false); createForm.resetFields(); fetchData(); - } catch (e: any) { message.error(e?.message || '操作失败'); } + } catch (e: any) { + message.error(e?.message || '操作失败'); + } }; const handleRefund = async () => { @@ -78,7 +94,9 @@ const DepositsPage: React.FC = () => { setRefundModal(null); refundForm.resetFields(); fetchData(); - } catch (e: any) { message.error(e?.message || '操作失败'); } + } catch (e: any) { + message.error(e?.message || '操作失败'); + } }; const columns = [ @@ -86,28 +104,54 @@ const DepositsPage: React.FC = () => { { title: '押金金额', dataIndex: 'amount', render: (v: number) => `¥${Number(v).toFixed(2)}` }, { title: '缴纳日期', dataIndex: 'paidDate' }, { - title: '状态', dataIndex: 'status', + title: '状态', + dataIndex: 'status', render: (s: string) => {statusMap[s]?.text || s}, }, - { title: '退还金额', dataIndex: 'refundAmount', render: (v: any) => v != null ? `¥${Number(v).toFixed(2)}` : '-' }, - { title: '扣除金额', dataIndex: 'deductionAmount', render: (v: any) => v > 0 ? `¥${Number(v).toFixed(2)}` : '-' }, + { + title: '退还金额', + dataIndex: 'refundAmount', + render: (v: any) => (v != null ? `¥${Number(v).toFixed(2)}` : '-'), + }, + { + title: '扣除金额', + dataIndex: 'deductionAmount', + render: (v: any) => (v > 0 ? `¥${Number(v).toFixed(2)}` : '-'), + }, { title: '扣除原因', dataIndex: 'deductionReason', render: (v: any) => v || '-' }, { title: '退还日期', dataIndex: 'refundDate', render: (v: any) => v || '-' }, { title: '备注', dataIndex: 'notes', render: (v: any) => v || '-' }, { - title: '操作', width: 160, + title: '操作', + width: 160, render: (_: any, record: any) => ( {record.status === 'paid' && ( - { - setRefundModal(record); - refundForm.setFieldsValue({ refundDate: dayjs(), deductionAmount: 0 }); - }}>退还 + { + setRefundModal(record); + refundForm.setFieldsValue({ refundDate: dayjs(), deductionAmount: 0 }); + }} + > + 退还 + )} - { - try { await api.delete(`/deposits/${record.id}`); message.success('删除成功'); fetchData(); } catch (e: any) { message.error(e?.message || '删除失败'); } - }}> + { + try { + await api.delete(`/deposits/${record.id}`); + message.success('删除成功'); + fetchData(); + } catch (e: any) { + message.error(e?.message || '删除失败'); + } + }} + >
`共 ${total} 条` }} /> +
`共 ${total} 条` }} + /> - setCreateModal(false)} okText="确认"> + setCreateModal(false)} + okText="确认" + >
- - s.status === 'active') + .map((s: any) => ({ + value: s.id, + label: `${s.name} (${s.idNumber || s.phone || ''})`, + }))} + /> @@ -165,7 +253,13 @@ const DepositsPage: React.FC = () => {
- setRefundModal(null)} okText="确认退还"> + setRefundModal(null)} + okText="确认退还" + >
押金金额: ¥{Number(refundModal?.amount || 0).toFixed(2)} @@ -174,7 +268,12 @@ const DepositsPage: React.FC = () => { - + diff --git a/apps/admin/src/pages/Expenses/index.tsx b/apps/admin/src/pages/Expenses/index.tsx index 723ddb3..71e31f4 100644 --- a/apps/admin/src/pages/Expenses/index.tsx +++ b/apps/admin/src/pages/Expenses/index.tsx @@ -1,6 +1,28 @@ import React, { useEffect, useState, useMemo } from 'react'; -import { Table, Button, Modal, Form, Select, DatePicker, InputNumber, Input, Space, message, Tag, Tabs, Popconfirm, Upload } from 'antd'; -import { PlusOutlined, DeleteOutlined, EditOutlined, UploadOutlined, DownloadOutlined, ExportOutlined } from '@ant-design/icons'; +import { + Table, + Button, + Modal, + Form, + Select, + DatePicker, + InputNumber, + Input, + Space, + message, + Tag, + Tabs, + Popconfirm, + Upload, +} from 'antd'; +import { + PlusOutlined, + DeleteOutlined, + EditOutlined, + UploadOutlined, + DownloadOutlined, + ExportOutlined, +} from '@ant-design/icons'; import dayjs from 'dayjs'; import api from '../../api'; import PermissionButton from '../../components/PermissionButton'; @@ -25,7 +47,17 @@ const personalExpenseTypeOptions = [ { value: 'other', label: '其他' }, ]; -const typeMap: Record = { water: '水费', electricity: '电费', cleaning: '保洁费', damage: '损坏赔偿', penalty: '罚款', key: '钥匙费', remote: '空调遥控器', deposit_deduction: '押金扣除', other: '其他' }; +const typeMap: Record = { + water: '水费', + electricity: '电费', + cleaning: '保洁费', + damage: '损坏赔偿', + penalty: '罚款', + key: '钥匙费', + remote: '空调遥控器', + deposit_deduction: '押金扣除', + other: '其他', +}; const ExpensesPage: React.FC = () => { const [roomExpenses, setRoomExpenses] = useState([]); @@ -52,16 +84,22 @@ const ExpensesPage: React.FC = () => { message.success(res?.message || `已删除 ${selectedRoomKeys.length} 条`); setSelectedRoomKeys([]); fetchData(); - } catch (e: any) { message.error(e?.message || '批量删除失败'); } + } catch (e: any) { + message.error(e?.message || '批量删除失败'); + } }; const handleBatchDeletePersonal = async () => { try { - const res: any = await api.post('/expenses/personal/batch-delete', { ids: selectedPersonalKeys }); + const res: any = await api.post('/expenses/personal/batch-delete', { + ids: selectedPersonalKeys, + }); message.success(res?.message || `已删除 ${selectedPersonalKeys.length} 条`); setSelectedPersonalKeys([]); fetchData(); - } catch (e: any) { message.error(e?.message || '批量删除失败'); } + } catch (e: any) { + message.error(e?.message || '批量删除失败'); + } }; const fetchData = async () => { @@ -77,11 +115,15 @@ const ExpensesPage: React.FC = () => { setPersonalExpenses(pe); setRooms(rm); setStudents(st); - } catch (e) { console.error(e); } + } catch (e) { + console.error(e); + } setLoading(false); }; - useEffect(() => { fetchData(); }, []); + useEffect(() => { + fetchData(); + }, []); const filteredRoomExpenses = useMemo(() => { return roomExpenses.filter((r: any) => { @@ -127,7 +169,9 @@ const ExpensesPage: React.FC = () => { setEditingRoom(null); roomForm.resetFields(); fetchData(); - } catch (e: any) { message.error(e?.message || '操作失败'); } + } catch (e: any) { + message.error(e?.message || '操作失败'); + } }; const handlePersonalExpense = async () => { @@ -152,33 +196,58 @@ const ExpensesPage: React.FC = () => { setEditingPersonal(null); personalForm.resetFields(); fetchData(); - } catch (e: any) { message.error(e?.message || '操作失败'); } + } catch (e: any) { + message.error(e?.message || '操作失败'); + } }; const roomColumns = [ { title: '宿舍', render: (_: any, r: any) => r.room?.roomNumber || '-' }, - { title: '费用类型', dataIndex: 'expenseType', render: (v: string) => {typeMap[v] || v} }, + { + title: '费用类型', + dataIndex: 'expenseType', + render: (v: string) => {typeMap[v] || v}, + }, { title: '金额', dataIndex: 'amount', render: (v: number) => `¥${Number(v).toFixed(2)}` }, { title: '账单周期', render: (_: any, r: any) => `${r.periodStart} ~ ${r.periodEnd}` }, { title: '说明', dataIndex: 'description' }, - { title: '录入时间', dataIndex: 'createdAt', render: (v: string) => dayjs(v).format('YYYY-MM-DD HH:mm') }, { - title: '操作', width: 120, + title: '录入时间', + dataIndex: 'createdAt', + render: (v: string) => dayjs(v).format('YYYY-MM-DD HH:mm'), + }, + { + title: '操作', + width: 120, render: (_: any, record: any) => ( - } onClick={() => { - setEditingRoom(record); - roomForm.setFieldsValue({ - roomId: record.roomId, - expenseType: record.expenseType, - amount: Number(record.amount), - period: [dayjs(record.periodStart), dayjs(record.periodEnd)], - description: record.description, - }); - setRoomModal(true); - }}>{''} + } + onClick={() => { + setEditingRoom(record); + roomForm.setFieldsValue({ + roomId: record.roomId, + expenseType: record.expenseType, + amount: Number(record.amount), + period: [dayjs(record.periodStart), dayjs(record.periodEnd)], + description: record.description, + }); + setRoomModal(true); + }} + > + {''} + - { await api.delete(`/expenses/room/${record.id}`); message.success('删除成功'); fetchData(); }}> + { + await api.delete(`/expenses/room/${record.id}`); + message.success('删除成功'); + fetchData(); + }} + >
`共 ${total} 条` }} - rowSelection={{ selectedRowKeys: selectedRoomKeys, onChange: (keys) => setSelectedRoomKeys(keys as number[]) }} - /> - - ), - }, - { - key: 'personal', - label: '个人附加费', - children: ( - <> -
- - setPersonalSearch(v)} - onChange={e => { if (!e.target.value) setPersonalSearch(''); }} - /> - setRoomTypeFilter(v)} + options={expenseTypeOptions} + /> + + { 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); } + 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); + } + }} + > + + + + } + 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}` }, + }) + .then((res) => res.blob()) + .then((blob) => { + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = '水电费导入模板.xlsx'; + a.click(); + URL.revokeObjectURL(url); + }) + .catch(() => message.error('下载失败')); }} > - - - - } 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}` } }) - .then(res => res.blob()) - .then(blob => { - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = '个人附加费导入模板.xlsx'; - a.click(); - URL.revokeObjectURL(url); - }) - .catch(() => message.error('下载失败')); - }}>下载模板 - } 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}` } }) - .then(res => res.blob()) - .then(blob => { - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = '个人附加费导出.xlsx'; - a.click(); - URL.revokeObjectURL(url); - }) - .catch(() => message.error('导出失败')); - }}>导出 - - - - - - - - } onClick={() => { setEditingPersonal(null); personalForm.resetFields(); setPersonalModal(true); }}>录入个人费用 - -
-
`共 ${total} 条` }} - rowSelection={{ selectedRowKeys: selectedPersonalKeys, onChange: (keys) => setSelectedPersonalKeys(keys as number[]) }} - /> - - ), - }, - ]} /> + 下载水电费模板 + + + + + + + + + } + onClick={() => { + setEditingRoom(null); + roomForm.resetFields(); + setRoomModal(true); + }} + > + 录入宿舍费用 + + + +
`共 ${total} 条` }} + rowSelection={{ + selectedRowKeys: selectedRoomKeys, + onChange: (keys) => setSelectedRoomKeys(keys as number[]), + }} + /> + + ), + }, + { + key: 'personal', + label: '个人附加费', + children: ( + <> +
+ + setPersonalSearch(v)} + onChange={(e) => { + if (!e.target.value) setPersonalSearch(''); + }} + /> +
`共 ${total} 条` }} + rowSelection={{ + selectedRowKeys: selectedPersonalKeys, + onChange: (keys) => setSelectedPersonalKeys(keys as number[]), + }} + /> + + ), + }, + ]} + /> - { setRoomModal(false); setEditingRoom(null); }} okText={editingRoom ? '保存' : '确认录入'}> + { + setRoomModal(false); + setEditingRoom(null); + }} + okText={editingRoom ? '保存' : '确认录入'} + > - ({ + value: r.id, + label: `${r.roomNumber} (${r.building || ''})`, + }))} + /> ({ value: s.id, label: s.name }))} /> + ({ value: r.id, label: r.roomNumber }))} /> + diff --git a/apps/admin/src/pages/Login/index.tsx b/apps/admin/src/pages/Login/index.tsx index 6bd4044..dfa85f4 100644 --- a/apps/admin/src/pages/Login/index.tsx +++ b/apps/admin/src/pages/Login/index.tsx @@ -27,10 +27,27 @@ const LoginPage: React.FC = () => { }; return ( -
- +
+
- 恭学教育基地管理系统 + + 恭学教育基地管理系统 +

水电费精准计费平台

@@ -41,7 +58,13 @@ const LoginPage: React.FC = () => { } placeholder="密码" /> - diff --git a/apps/admin/src/pages/Occupancies/index.tsx b/apps/admin/src/pages/Occupancies/index.tsx index cc46fc3..3a5754c 100644 --- a/apps/admin/src/pages/Occupancies/index.tsx +++ b/apps/admin/src/pages/Occupancies/index.tsx @@ -1,6 +1,31 @@ import React, { useEffect, useState, useMemo } from 'react'; -import { Table, Button, Modal, Form, Select, DatePicker, Input, InputNumber, Space, message, Tag, Popconfirm, Upload, Switch, Alert, Tooltip } from 'antd'; -import { PlusOutlined, SwapOutlined, LogoutOutlined, DeleteOutlined, UploadOutlined, DownloadOutlined, ExportOutlined } from '@ant-design/icons'; +import { + Table, + Button, + Modal, + Form, + Select, + DatePicker, + Input, + InputNumber, + Space, + message, + Tag, + Popconfirm, + Upload, + Switch, + Alert, + Tooltip, +} from 'antd'; +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'; @@ -35,18 +60,24 @@ const OccupanciesPage: React.FC = () => { setData(occ); setStudents(stu); setRooms(rm); - } catch (e) { console.error(e); } + } catch (e) { + console.error(e); + } setLoading(false); }; - useEffect(() => { fetchData(); setSelectedRowKeys([]); }, [showActive]); + useEffect(() => { + fetchData(); + setSelectedRowKeys([]); + }, [showActive]); const filteredData = useMemo(() => { if (!searchText) return data; const keyword = searchText.toLowerCase(); - return data.filter((r: any) => - r.student?.name?.toLowerCase().includes(keyword) || - r.room?.roomNumber?.toLowerCase().includes(keyword) + return data.filter( + (r: any) => + r.student?.name?.toLowerCase().includes(keyword) || + r.room?.roomNumber?.toLowerCase().includes(keyword), ); }, [data, searchText]); @@ -64,7 +95,9 @@ const OccupanciesPage: React.FC = () => { setCheckInModal(false); checkInForm.resetFields(); fetchData(); - } catch (e: any) { message.error(e?.message || '操作失败'); } + } catch (e: any) { + message.error(e?.message || '操作失败'); + } }; const handleCheckOut = async () => { @@ -79,7 +112,9 @@ const OccupanciesPage: React.FC = () => { setCheckOutModal(null); checkOutForm.resetFields(); fetchData(); - } catch (e: any) { message.error(e?.message || '操作失败'); } + } catch (e: any) { + message.error(e?.message || '操作失败'); + } }; const handleTransfer = async () => { @@ -96,7 +131,9 @@ const OccupanciesPage: React.FC = () => { setTransferModal(null); transferForm.resetFields(); fetchData(); - } catch (e: any) { message.error(e?.message || '操作失败'); } + } catch (e: any) { + message.error(e?.message || '操作失败'); + } }; const handleBatchCheckOut = async () => { @@ -113,7 +150,9 @@ const OccupanciesPage: React.FC = () => { batchCheckOutForm.resetFields(); setSelectedRowKeys([]); fetchData(); - } catch (e: any) { message.error(e?.message || '批量退宿失败'); } + } catch (e: any) { + message.error(e?.message || '批量退宿失败'); + } }; const handleBatchDelete = async () => { @@ -122,7 +161,9 @@ const OccupanciesPage: React.FC = () => { message.success(res?.message || `已删除 ${selectedRowKeys.length} 条`); setSelectedRowKeys([]); fetchData(); - } catch (e: any) { message.error(e?.message || '批量删除失败'); } + } catch (e: any) { + message.error(e?.message || '批量删除失败'); + } }; const columns = [ @@ -130,26 +171,63 @@ const OccupanciesPage: React.FC = () => { { title: '宿舍', render: (_: any, r: any) => r.room?.roomNumber || '-' }, { title: '入住日期', dataIndex: 'checkInDate' }, { title: '计费起始', dataIndex: 'billingStartDate' }, - { title: '退宿日期', dataIndex: 'checkOutDate', render: (v: any) => v || 在住 }, + { + title: '退宿日期', + dataIndex: 'checkOutDate', + render: (v: any) => v || 在住, + }, { title: '计费截止', dataIndex: 'billingEndDate', render: (v: any) => v || '-' }, { title: '退宿原因', dataIndex: 'checkOutReason', render: (v: any) => v || '-' }, { - 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 || '删除失败'); } }}> - - - + + + - } onClick={() => { checkInForm.resetFields(); checkInForm.setFieldsValue({ checkInDate: dayjs() }); setCheckInModal(true); }}> + } + onClick={() => { + checkInForm.resetFields(); + checkInForm.setFieldsValue({ checkInDate: dayjs() }); + setCheckInModal(true); + }} + > 入住登记 @@ -193,57 +297,99 @@ const OccupanciesPage: React.FC = () => { params.set('depositAmount', String(depositAmount)); } try { - const res: any = await api.post(`/occupancies/import?${params.toString()}`, formData, { headers: { 'Content-Type': 'multipart/form-data' } }); + 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 }); + 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); } + } 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}` } }) - .then(res => res.blob()) - .then(blob => { - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = '入住名单导入模板.xlsx'; - a.click(); - URL.revokeObjectURL(url); + } + 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}` }, }) - .catch(() => message.error('下载失败')); - }}>下载模板 - } 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' : ''; - fetch(`${baseURL}/occupancies/export${params}`, { headers: { Authorization: `Bearer ${token}` } }) - .then(res => res.blob()) - .then(blob => { - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = showActive ? '在住记录.xlsx' : '全部入住记录.xlsx'; - a.click(); - URL.revokeObjectURL(url); + .then((res) => res.blob()) + .then((blob) => { + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = '入住名单导入模板.xlsx'; + a.click(); + URL.revokeObjectURL(url); + }) + .catch(() => message.error('下载失败')); + }} + > + 下载模板 + + } + 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' : ''; + fetch(`${baseURL}/occupancies/export${params}`, { + headers: { Authorization: `Bearer ${token}` }, }) - .catch(() => message.error('导出失败')); - }}>导出记录 + .then((res) => res.blob()) + .then((blob) => { + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = showActive ? '在住记录.xlsx' : '全部入住记录.xlsx'; + a.click(); + URL.revokeObjectURL(url); + }) + .catch(() => message.error('导出失败')); + }} + > + 导出记录 + 导入时自动收押金 - {autoDeposit && setDepositAmount(v || 500)} style={{ width: 80 }} addonAfter="元" />} + {autoDeposit && ( + setDepositAmount(v || 500)} + style={{ width: 80 }} + addonAfter="元" + /> + )}
@@ -253,15 +399,42 @@ const OccupanciesPage: React.FC = () => { 已选 {selectedRowKeys.length} 条记录 {showActive ? ( - } onClick={() => { batchCheckOutForm.resetFields(); batchCheckOutForm.setFieldsValue({ checkOutDate: dayjs() }); setBatchCheckOutModal(true); }} style={{ marginLeft: 12 }}>批量退宿 + } + onClick={() => { + batchCheckOutForm.resetFields(); + batchCheckOutForm.setFieldsValue({ checkOutDate: dayjs() }); + setBatchCheckOutModal(true); + }} + style={{ marginLeft: 12 }} + > + 批量退宿 + ) : ( - - + + )} - + } type="info" @@ -278,21 +451,61 @@ const OccupanciesPage: React.FC = () => { /> {/* 入住登记弹窗 */} - setCheckInModal(false)} okText="确认入住" width={500}> + setCheckInModal(false)} + okText="确认入住" + width={500} + > - - s.status === 'active') + .map((s: any) => ({ + value: s.id, + label: `${s.name} (${s.idNumber || s.phone || ''})`, + }))} + /> - - ({ + value: r.id, + label: `${r.roomNumber} (${r.building || ''}) [${r.currentCount}/${r.capacity}]`, + disabled: r.currentCount >= r.capacity, + }))} + /> - - + + @@ -301,67 +514,132 @@ const OccupanciesPage: React.FC = () => { {/* 退宿弹窗 */} - setCheckOutModal(null)} okText="确认退宿"> + setCheckOutModal(null)} + okText="确认退宿" + > - + - {/* 批量退宿弹窗 */} - setBatchCheckOutModal(false)} okText="确认批量退宿" width={500}> + setBatchCheckOutModal(false)} + okText="确认批量退宿" + width={500} + >
- + - -
+
即将退宿的学生:
- {data.filter((r: any) => selectedRowKeys.includes(r.id)).map((r: any) => ( - {r.student?.name} ({r.room?.roomNumber}) - ))} + {data + .filter((r: any) => selectedRowKeys.includes(r.id)) + .map((r: any) => ( + + {r.student?.name} ({r.room?.roomNumber}) + + ))}
{/* 换房弹窗 */} - setTransferModal(null)} okText="确认换房" width={500}> + setTransferModal(null)} + okText="确认换房" + width={500} + >
- r.id !== transferModal?.roomId) + .map((r: any) => ({ + value: r.id, + label: `${r.roomNumber} (${r.building || ''}) [${r.currentCount}/${r.capacity}]`, + disabled: r.currentCount >= r.capacity, + }))} + /> - + - + diff --git a/apps/admin/src/pages/OperationLogs/index.tsx b/apps/admin/src/pages/OperationLogs/index.tsx index fe02532..d61bcf6 100644 --- a/apps/admin/src/pages/OperationLogs/index.tsx +++ b/apps/admin/src/pages/OperationLogs/index.tsx @@ -6,7 +6,13 @@ import api from '../../api'; const { RangePicker } = DatePicker; const moduleColorMap: Record = { - '学生': 'blue', '宿舍': 'green', '入住': 'cyan', '费用': 'orange', '账单': 'red', '账号': 'purple', '认证': 'magenta', + 学生: 'blue', + 宿舍: 'green', + 入住: 'cyan', + 费用: 'orange', + 账单: 'red', + 账号: 'purple', + 认证: 'magenta', }; const statusMap: Record = { @@ -27,40 +33,93 @@ const OperationLogsPage: React.FC = () => { try { const params: any = { page, pageSize: 20 }; if (filterModule) params.module = filterModule; - if (dateRange) { params.startDate = dateRange[0]; params.endDate = dateRange[1]; } + if (dateRange) { + params.startDate = dateRange[0]; + params.endDate = dateRange[1]; + } const res: any = await api.get('/operation-logs', { params }); setData(res.data); setTotal(res.total); - } catch (e) { console.error(e); } + } catch (e) { + console.error(e); + } setLoading(false); }; - useEffect(() => { fetchData(); }, [page, filterModule, dateRange]); + useEffect(() => { + fetchData(); + }, [page, filterModule, dateRange]); const columns = [ - { title: '时间', dataIndex: 'createdAt', width: 170, render: (v: string) => dayjs(v).format('YYYY-MM-DD HH:mm:ss') }, + { + title: '时间', + dataIndex: 'createdAt', + width: 170, + render: (v: string) => dayjs(v).format('YYYY-MM-DD HH:mm:ss'), + }, { title: '操作人', dataIndex: 'username', width: 100 }, - { title: '模块', dataIndex: 'module', width: 80, render: (v: string) => {v} }, + { + title: '模块', + dataIndex: 'module', + width: 80, + render: (v: string) => {v}, + }, { title: '操作', dataIndex: 'action', width: 150 }, - { title: '状态', dataIndex: 'status', width: 70, render: (v: string) => { - const s = statusMap[v] || statusMap['success']; - return {s.text}; - }}, - { title: '详情', dataIndex: 'detail', ellipsis: true, render: (v: string) => v ? {v} : '-' }, + { + title: '状态', + dataIndex: 'status', + width: 70, + render: (v: string) => { + const s = statusMap[v] || statusMap['success']; + return {s.text}; + }, + }, + { + title: '详情', + dataIndex: 'detail', + ellipsis: true, + render: (v: string) => + v ? ( + + {v} + + ) : ( + '-' + ), + }, { title: 'IP地址', dataIndex: 'ipAddress', width: 130, render: (v: string) => v || '-' }, - { title: '终端', dataIndex: 'userAgent', width: 100, ellipsis: true, render: (v: string) => { - if (!v) return '-'; - if (v.includes('Mobile')) return 手机; - if (v.includes('Windows')) return Windows; - if (v.includes('Mac')) return Mac; - if (v.includes('Linux')) return Linux; - return 其他; - }}, + { + title: '终端', + dataIndex: 'userAgent', + width: 100, + ellipsis: true, + render: (v: string) => { + if (!v) return '-'; + if (v.includes('Mobile')) return 手机; + if (v.includes('Windows')) return Windows; + if (v.includes('Mac')) return Mac; + if (v.includes('Linux')) return Linux; + return ( + + 其他 + + ); + }, + }, ]; return (
-
+

操作日志

+
{ destroyOnClose > - + @@ -168,7 +251,7 @@ const RolesPage: React.FC = () => {
- {allPerms.map(group => ( + {allPerms.map((group) => ( { handleGroupCheckAll(group.group, e.target.checked)} + onChange={(e) => handleGroupCheckAll(group.group, e.target.checked)} > {groupNames[group.group] || group.group} @@ -185,11 +268,13 @@ const RolesPage: React.FC = () => { > setSelectedPermIds(vals as number[])} + onChange={(vals) => setSelectedPermIds(vals as number[])} > - {group.permissions.map(p => ( - {p.name} + {group.permissions.map((p) => ( + + {p.name} + ))} diff --git a/apps/admin/src/pages/RoomVisual/index.tsx b/apps/admin/src/pages/RoomVisual/index.tsx index 3b814af..02bc294 100644 --- a/apps/admin/src/pages/RoomVisual/index.tsx +++ b/apps/admin/src/pages/RoomVisual/index.tsx @@ -14,27 +14,40 @@ const RoomVisualPage: React.FC = () => { try { const res: any = await api.get('/rooms/visual'); setData(res); - } catch (e) { console.error(e); } + } catch (e) { + console.error(e); + } setLoading(false); }; - useEffect(() => { fetchData(); }, []); + useEffect(() => { + fetchData(); + }, []); - if (loading || !data) return ; + if (loading || !data) + return ; - const rooms = selectedBuilding === 'all' - ? data.rooms - : data.rooms.filter((r: any) => r.building === selectedBuilding); + const rooms = + selectedBuilding === 'all' + ? data.rooms + : data.rooms.filter((r: any) => r.building === selectedBuilding); const totalRooms = rooms.length; - const emptyRooms = rooms.filter((r: any) => r.currentCount === 0 && r.status !== 'maintenance').length; - const availableBeds = rooms.reduce((sum: number, r: any) => r.status !== 'maintenance' ? sum + (r.capacity - r.currentCount) : sum, 0); + const emptyRooms = rooms.filter( + (r: any) => r.currentCount === 0 && r.status !== 'maintenance', + ).length; + const availableBeds = rooms.reduce( + (sum: number, r: any) => + r.status !== 'maintenance' ? sum + (r.capacity - r.currentCount) : sum, + 0, + ); const fullRooms = rooms.filter((r: any) => r.currentCount >= r.capacity).length; const getCardStyle = (room: any): React.CSSProperties => { if (room.status === 'maintenance') return { background: '#f5f5f5', borderColor: '#d9d9d9' }; if (room.currentCount === 0) return { background: '#f6ffed', borderColor: '#b7eb8f' }; - if (room.currentCount >= room.capacity) return { background: '#fff2f0', borderColor: '#ffccc7' }; + if (room.currentCount >= room.capacity) + return { background: '#fff2f0', borderColor: '#ffccc7' }; return { background: '#e6f4ff', borderColor: '#91caff' }; }; @@ -47,7 +60,16 @@ const RoomVisualPage: React.FC = () => { return (
-
+

宿舍总览

{ rowKey="id" loading={loading} pagination={{ pageSize: 20, showTotal: (total) => `共 ${total} 间` }} - rowClassName={(record) => record.status === 'archived' ? 'archived-row' : ''} + rowClassName={(record) => (record.status === 'archived' ? 'archived-row' : '')} rowSelection={{ selectedRowKeys, onChange: (keys) => setSelectedRowKeys(keys as number[]), @@ -253,33 +386,62 @@ const RoomsPage: React.FC = () => { /> - { setModalOpen(false); setEditing(null); }} okText="保存"> + { + setModalOpen(false); + setEditing(null); + }} + okText="保存" + > - - - - + + + + + + + + + + + + - {editing && ( - )} - setDetailModal(null)} footer={null} width={600}> + setDetailModal(null)} + footer={null} + width={600} + > {detailModal?.currentOccupants?.length > 0 ? (
{ message.success(res?.message || `已批量归档 ${selectedRowKeys.length} 人`); setSelectedRowKeys([]); fetchData(); - } catch (e: any) { message.error(e?.message || '批量归档失败'); } + } catch (e: any) { + message.error(e?.message || '批量归档失败'); + } }; const fetchData = async () => { @@ -40,11 +62,15 @@ const StudentsPage: React.FC = () => { setArchivedCount(archived.length); const filtered = showArchived ? res : res.filter((r: any) => r.status !== 'archived'); setData(filtered); - } catch (e) { console.error(e); } + } catch (e) { + console.error(e); + } setLoading(false); }; - useEffect(() => { fetchData(); }, [searchName, showArchived]); + useEffect(() => { + fetchData(); + }, [searchName, showArchived]); const handleSave = async () => { const values = await form.validateFields(); @@ -60,7 +86,9 @@ const StudentsPage: React.FC = () => { form.resetFields(); setEditing(null); fetchData(); - } catch (e: any) { message.error(e?.message || '操作失败'); } + } catch (e: any) { + message.error(e?.message || '操作失败'); + } }; const handleArchive = async (id: number) => { @@ -68,7 +96,9 @@ const StudentsPage: React.FC = () => { await api.delete(`/students/${id}`); message.success('已归档'); fetchData(); - } catch (e: any) { message.error(e?.message || '归档失败'); } + } catch (e: any) { + message.error(e?.message || '归档失败'); + } }; const handleRestore = async (id: number) => { @@ -76,15 +106,19 @@ const StudentsPage: React.FC = () => { await api.put(`/students/${id}/restore`); message.success('已恢复'); fetchData(); - } catch (e: any) { message.error(e?.message || '恢复失败'); } + } catch (e: any) { + message.error(e?.message || '恢复失败'); + } }; const handleDownloadTemplate = () => { - const baseURL = import.meta.env.PROD ? '/api' : `http://localhost:${import.meta.env.VITE_API_PORT || 3002}/api`; + const baseURL = import.meta.env.PROD + ? '/api' + : `http://localhost:${import.meta.env.VITE_API_PORT || 3002}/api`; const token = localStorage.getItem('token'); fetch(`${baseURL}/students/template`, { headers: { Authorization: `Bearer ${token}` } }) - .then(res => res.blob()) - .then(blob => { + .then((res) => res.blob()) + .then((blob) => { const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; @@ -96,12 +130,14 @@ const StudentsPage: React.FC = () => { }; const handleExport = () => { - const baseURL = import.meta.env.PROD ? '/api' : `http://localhost:${import.meta.env.VITE_API_PORT || 3002}/api`; + const baseURL = import.meta.env.PROD + ? '/api' + : `http://localhost:${import.meta.env.VITE_API_PORT || 3002}/api`; const token = localStorage.getItem('token'); const params = showArchived ? '?includeArchived=true' : ''; fetch(`${baseURL}/students/export${params}`, { headers: { Authorization: `Bearer ${token}` } }) - .then(res => res.blob()) - .then(blob => { + .then((res) => res.blob()) + .then((blob) => { const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; @@ -121,28 +157,58 @@ const StudentsPage: React.FC = () => { { title: '民族', dataIndex: 'ethnicity', width: 80 }, { title: '紧急联系人', dataIndex: 'emergencyContact' }, { title: '紧急联系人电话', dataIndex: 'emergencyPhone' }, - { title: '所属机构', dataIndex: 'organization', render: (v: string) => v ? {v} : '-' }, + { + title: '所属机构', + dataIndex: 'organization', + render: (v: string) => (v ? {v} : '-'), + }, { title: '负责人', dataIndex: 'supervisor' }, { - title: '状态', dataIndex: 'status', + title: '状态', + dataIndex: 'status', render: (s: string) => {statusMap[s]?.text || s}, }, { - title: '操作', width: 180, + title: '操作', + width: 180, render: (_: any, record: any) => ( {record.status === 'archived' ? ( - handleRestore(record.id)} okText="恢复" cancelText="取消"> - + handleRestore(record.id)} + okText="恢复" + cancelText="取消" + > + ) : ( <> - { setEditing(record); form.setFieldsValue(record); setModalOpen(true); }}>编辑 + { + setEditing(record); + form.setFieldsValue(record); + setModalOpen(true); + }} + > + 编辑 + - handleArchive(record.id)} okText="归档" cancelText="取消"> - + handleArchive(record.id)} + okText="归档" + cancelText="取消" + > + @@ -156,18 +222,45 @@ const StudentsPage: React.FC = () => {
- - - - + + - } onClick={() => { setEditing(null); form.resetFields(); setModalOpen(true); }}> + } + onClick={() => { + setEditing(null); + form.resetFields(); + setModalOpen(true); + }} + > 添加学生 @@ -178,18 +271,35 @@ const StudentsPage: React.FC = () => { const formData = new FormData(); formData.append('file', file); try { - const res: any = await api.post('/students/import', formData, { headers: { 'Content-Type': 'multipart/form-data' } }); + 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); } + } catch (e: any) { + message.error(e?.message || '导入失败'); + onError?.(e); + } }} > - } onClick={handleDownloadTemplate}>下载模板 - } onClick={handleExport}>导出名单 + } + onClick={handleDownloadTemplate} + > + 下载模板 + + } + onClick={handleExport} + > + 导出名单 +
{ rowKey="id" loading={loading} pagination={{ pageSize: 15, showTotal: (total) => `共 ${total} 人` }} - rowClassName={(record: any) => record.status === 'archived' ? 'archived-row' : ''} + rowClassName={(record: any) => (record.status === 'archived' ? 'archived-row' : '')} rowSelection={{ selectedRowKeys, onChange: (keys) => setSelectedRowKeys(keys as number[]), @@ -210,7 +320,10 @@ const StudentsPage: React.FC = () => { title={editing ? '编辑学生' : '添加学生'} open={modalOpen} onOk={handleSave} - onCancel={() => { setModalOpen(false); setEditing(null); }} + onCancel={() => { + setModalOpen(false); + setEditing(null); + }} okText="保存" > @@ -218,7 +331,13 @@ const StudentsPage: React.FC = () => { - @@ -235,7 +354,11 @@ const StudentsPage: React.FC = () => { - + @@ -243,11 +366,13 @@ const StudentsPage: React.FC = () => { {editing && ( - )} diff --git a/apps/admin/src/pages/Tenants/index.tsx b/apps/admin/src/pages/Tenants/index.tsx index 788add8..6889ba4 100644 --- a/apps/admin/src/pages/Tenants/index.tsx +++ b/apps/admin/src/pages/Tenants/index.tsx @@ -5,8 +5,16 @@ import api from '../../api'; import PermissionButton from '../../components/PermissionButton'; const PRESET_COLORS = [ - '#ff7875', '#ffa940', '#ffc53d', '#73d13d', '#36cfc9', - '#40a9ff', '#597ef7', '#9254de', '#f759ab', '#8c8c8c', + '#ff7875', + '#ffa940', + '#ffc53d', + '#73d13d', + '#36cfc9', + '#40a9ff', + '#597ef7', + '#9254de', + '#f759ab', + '#8c8c8c', ]; const TenantsPage: React.FC = () => { @@ -20,7 +28,9 @@ const TenantsPage: React.FC = () => { const filteredData = useMemo(() => { if (!searchText) return data; const s = searchText.toLowerCase(); - return data.filter((d: any) => d.name?.toLowerCase().includes(s) || d.contact?.toLowerCase().includes(s)); + return data.filter( + (d: any) => d.name?.toLowerCase().includes(s) || d.contact?.toLowerCase().includes(s), + ); }, [data, searchText]); const fetchData = async () => { @@ -28,11 +38,15 @@ const TenantsPage: React.FC = () => { try { const res: any = await api.get('/tenants'); setData(res); - } catch (e) { console.error(e); } + } catch (e) { + console.error(e); + } setLoading(false); }; - useEffect(() => { fetchData(); }, []); + useEffect(() => { + fetchData(); + }, []); const handleSave = async () => { const values = await form.validateFields(); @@ -48,7 +62,9 @@ const TenantsPage: React.FC = () => { form.resetFields(); setEditing(null); fetchData(); - } catch (e: any) { message.error(e?.message || '操作失败'); } + } catch (e: any) { + message.error(e?.message || '操作失败'); + } }; const handleArchive = async (id: number) => { @@ -56,30 +72,74 @@ const TenantsPage: React.FC = () => { await api.delete(`/tenants/${id}`); message.success('已归档'); fetchData(); - } catch (e: any) { message.error(e?.message || '归档失败'); } + } catch (e: any) { + message.error(e?.message || '归档失败'); + } }; const columns = [ { - title: '租赁方名称', dataIndex: 'name', + title: '租赁方名称', + dataIndex: 'name', render: (v: string, r: any) => ( - {v} + + {v} + ), }, { title: '联系人', dataIndex: 'contact', render: (v: string) => v || '-' }, { title: '电话', dataIndex: 'phone', render: (v: string) => v || '-' }, - { title: '颜色', dataIndex: 'color', render: (v: string) => v ? : '-' }, + { + title: '颜色', + dataIndex: 'color', + render: (v: string) => + v ? ( + + ) : ( + '-' + ), + }, { title: '备注', dataIndex: 'notes', ellipsis: true, render: (v: string) => v || '-' }, { - title: '操作', width: 150, + title: '操作', + width: 150, render: (_: any, record: any) => ( - { setEditing(record); form.setFieldsValue(record); setModalOpen(true); }}>编辑 + { + setEditing(record); + form.setFieldsValue(record); + setModalOpen(true); + }} + > + 编辑 + - handleArchive(record.id)} okText="归档" cancelText="取消"> - + handleArchive(record.id)} + okText="归档" + cancelText="取消" + > + @@ -89,39 +149,92 @@ const TenantsPage: React.FC = () => { return (
-
+
setSearchText(v)} - onChange={e => { if (!e.target.value) setSearchText(''); }} + onSearch={(v) => setSearchText(v)} + onChange={(e) => { + if (!e.target.value) setSearchText(''); + }} /> - } onClick={() => { setEditing(null); form.resetFields(); setModalOpen(true); }}> + } + onClick={() => { + setEditing(null); + form.resetFields(); + setModalOpen(true); + }} + > 添加租赁方
-
`共 ${total} 条` }} /> +
`共 ${total} 条` }} + /> - { setModalOpen(false); setEditing(null); }} okText="保存"> + { + setModalOpen(false); + setEditing(null); + }} + okText="保存" + >
- - - - - - {PRESET_COLORS.map(c => ( - form.setFieldValue('color', c)} - style={{ display: 'inline-block', width: 16, height: 16, background: c, borderRadius: 3, cursor: 'pointer', border: '1px solid #d9d9d9' }} - /> - ))} - - } /> + + + + + + + + + + + + {PRESET_COLORS.map((c) => ( + form.setFieldValue('color', c)} + style={{ + display: 'inline-block', + width: 16, + height: 16, + background: c, + borderRadius: 3, + cursor: 'pointer', + border: '1px solid #d9d9d9', + }} + /> + ))} + + } + /> + + + -
diff --git a/apps/admin/src/pages/Users/index.tsx b/apps/admin/src/pages/Users/index.tsx index e6b974a..e891f1a 100644 --- a/apps/admin/src/pages/Users/index.tsx +++ b/apps/admin/src/pages/Users/index.tsx @@ -1,5 +1,17 @@ import React, { useEffect, useState } from 'react'; -import { Table, Button, Modal, Form, Input, Select, Switch, Space, Tag, Popconfirm, message } 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'; @@ -25,11 +37,15 @@ const UsersPage: React.FC = () => { ]); setData(users); setRoles(rolesRes); - } catch (e) { console.error(e); } + } catch (e) { + console.error(e); + } setLoading(false); }; - useEffect(() => { fetchData(); }, []); + useEffect(() => { + fetchData(); + }, []); const handleAdd = () => { setEditing(null); @@ -52,15 +68,27 @@ const UsersPage: React.FC = () => { const values = await form.validateFields(); try { if (editing) { - await api.put(`/rbac/users/${editing.id}`, { username: values.username, name: values.name, isActive: values.isActive, roleIds: values.roleIds || [] }); + 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('/rbac/users', { username: values.username, password: values.password, name: values.name, roleIds: values.roleIds || [] }); + await api.post('/rbac/users', { + username: values.username, + password: values.password, + name: values.name, + roleIds: values.roleIds || [], + }); message.success('创建成功'); } setModalOpen(false); fetchData(); - } catch (e: any) { message.error(e.message || '操作失败'); } + } catch (e: any) { + message.error(e.message || '操作失败'); + } }; const handleDelete = async (id: number) => { @@ -68,7 +96,9 @@ const UsersPage: React.FC = () => { await api.delete(`/rbac/users/${id}`); message.success('已删除'); fetchData(); - } catch (e: any) { message.error(e.message || '删除失败'); } + } catch (e: any) { + message.error(e.message || '删除失败'); + } }; const handleResetPwd = (record: any) => { @@ -83,7 +113,9 @@ const UsersPage: React.FC = () => { await api.put(`/rbac/users/${resetTarget.id}/password`, { password: values.password }); message.success('密码已重置'); setPwdModalOpen(false); - } catch (e: any) { message.error(e.message || '操作失败'); } + } catch (e: any) { + message.error(e.message || '操作失败'); + } }; const columns = [ @@ -91,33 +123,68 @@ const UsersPage: React.FC = () => { { title: '用户名', dataIndex: 'username', width: 120 }, { title: '姓名', dataIndex: 'name', width: 120 }, { - title: '角色', dataIndex: 'roles', width: 200, - render: (v: any[]) => v && v.length > 0 - ? v.map((r: any) => {r.name}) - : 无角色, + title: '角色', + dataIndex: 'roles', + width: 200, + render: (v: any[]) => + v && v.length > 0 ? ( + v.map((r: any) => ( + + {r.name} + + )) + ) : ( + 无角色 + ), }, { - title: '状态', dataIndex: 'isActive', width: 80, + title: '状态', + dataIndex: 'isActive', + width: 80, render: (v: boolean) => {v ? '启用' : '禁用'}, }, { - title: '最后登录', dataIndex: 'lastLoginAt', width: 170, - render: (v: string) => v ? dayjs(v).format('YYYY-MM-DD HH:mm:ss') : '-', + title: '最后登录', + dataIndex: 'lastLoginAt', + width: 170, + render: (v: string) => (v ? dayjs(v).format('YYYY-MM-DD HH:mm:ss') : '-'), }, { - title: '创建时间', dataIndex: 'createdAt', width: 170, + title: '创建时间', + dataIndex: 'createdAt', + width: 170, render: (v: string) => dayjs(v).format('YYYY-MM-DD HH:mm:ss'), }, { - title: '操作', width: 220, fixed: 'right' as const, + title: '操作', + width: 220, + fixed: 'right' as const, render: (_: any, record: any) => ( - } onClick={() => handleEdit(record)}>编辑 - } onClick={() => handleResetPwd(record)}>重置密码 + } + onClick={() => handleEdit(record)} + > + 编辑 + + } + onClick={() => handleResetPwd(record)} + > + 重置密码 + {record.username !== 'admin' && ( handleDelete(record.id)}> - + )} @@ -128,19 +195,54 @@ const UsersPage: React.FC = () => { return (
-
+

账号管理

- } onClick={handleAdd}>新增账号 + } + onClick={handleAdd} + > + 新增账号 +
-
+
- setModalOpen(false)} destroyOnClose> + setModalOpen(false)} + destroyOnClose + >
- + {!editing && ( - + )} @@ -152,19 +254,38 @@ const UsersPage: React.FC = () => { )} - +