main #5

Merged
wangziqi merged 5 commits from xiongyuxing/gongxue-base:main into main 2026-07-14 02:56:04 +00:00
19 changed files with 213 additions and 52 deletions

View File

@@ -321,7 +321,11 @@ const EnrollmentsTab: React.FC<TabProps & { data: EnrollmentRecord[] }> = ({
columns={columns} columns={columns}
dataSource={data} dataSource={data}
rowKey="id" rowKey="id"
pagination={{ pageSize: 15 }} pagination={{
defaultPageSize: 15,
showSizeChanger: true,
pageSizeOptions: [15, 30, 50],
}}
/> />
<Modal <Modal
title="添加报读记录" title="添加报读记录"
@@ -428,7 +432,11 @@ const ExamScoresTab: React.FC<TabProps & { data: ExamScoreRecord[]; enrollments:
columns={columns} columns={columns}
dataSource={data} dataSource={data}
rowKey="id" rowKey="id"
pagination={{ pageSize: 15 }} pagination={{
defaultPageSize: 15,
showSizeChanger: true,
pageSizeOptions: [15, 30, 50],
}}
/> />
<Modal <Modal
title="添加考试成绩" title="添加考试成绩"
@@ -529,7 +537,11 @@ const LearningTab: React.FC<TabProps & { data: LearningRecord[] }> = ({ data, st
columns={columns} columns={columns}
dataSource={data} dataSource={data}
rowKey="id" rowKey="id"
pagination={{ pageSize: 15 }} pagination={{
defaultPageSize: 15,
showSizeChanger: true,
pageSizeOptions: [15, 30, 50],
}}
/> />
<Modal <Modal
title="添加学情记录" title="添加学情记录"
@@ -712,7 +724,11 @@ const AttachmentsTab: React.FC<TabProps & { data: AttachmentRecord[] }> = ({ dat
columns={columns} columns={columns}
dataSource={data} dataSource={data}
rowKey="id" rowKey="id"
pagination={{ pageSize: 15 }} pagination={{
defaultPageSize: 15,
showSizeChanger: true,
pageSizeOptions: [15, 30, 50],
}}
style={{ marginTop: 16 }} style={{ marginTop: 16 }}
/> />
</div> </div>

View File

@@ -399,7 +399,12 @@ const BillsPage: React.FC = () => {
dataSource={filteredBills} dataSource={filteredBills}
rowKey="id" rowKey="id"
loading={loading} loading={loading}
pagination={{ pageSize: 15, showTotal: (total) => `${total}` }} pagination={{
defaultPageSize: 15,
showSizeChanger: true,
pageSizeOptions: [15, 30, 50, 100],
showTotal: (total) => `${total}`,
}}
locale={{ emptyText: <Empty description="暂无数据" /> }} locale={{ emptyText: <Empty description="暂无数据" /> }}
rowSelection={{ rowSelection={{
selectedRowKeys: selectedRows, selectedRowKeys: selectedRows,

View File

@@ -530,7 +530,11 @@ const ClassDetailPage: React.FC = () => {
columns={studentColumns} columns={studentColumns}
dataSource={students} dataSource={students}
rowKey="id" rowKey="id"
pagination={{ pageSize: 20 }} pagination={{
defaultPageSize: 20,
showSizeChanger: true,
pageSizeOptions: [20, 50, 100],
}}
/> />
<Modal <Modal
title="添加学员" title="添加学员"
@@ -574,7 +578,11 @@ const ClassDetailPage: React.FC = () => {
columns={teacherColumns} columns={teacherColumns}
dataSource={teachers} dataSource={teachers}
rowKey="id" rowKey="id"
pagination={{ pageSize: 20 }} pagination={{
defaultPageSize: 20,
showSizeChanger: true,
pageSizeOptions: [20, 50, 100],
}}
/> />
<Modal <Modal
title="添加教师" title="添加教师"
@@ -633,7 +641,11 @@ const ClassDetailPage: React.FC = () => {
columns={scheduleColumns} columns={scheduleColumns}
dataSource={schedules} dataSource={schedules}
rowKey="id" rowKey="id"
pagination={{ pageSize: 20 }} pagination={{
defaultPageSize: 20,
showSizeChanger: true,
pageSizeOptions: [20, 50, 100],
}}
/> />
</div> </div>
), ),

View File

@@ -264,7 +264,11 @@ const ClassesPage: React.FC = () => {
rowKey="id" rowKey="id"
loading={loading} loading={loading}
locale={{ emptyText: <Empty description="暂无数据" /> }} locale={{ emptyText: <Empty description="暂无数据" /> }}
pagination={{ pageSize: 20 }} pagination={{
defaultPageSize: 20,
showSizeChanger: true,
pageSizeOptions: [20, 50, 100],
}}
scroll={{ x: 1100 }} scroll={{ x: 1100 }}
/> />

View File

@@ -483,7 +483,12 @@ const ClassroomRentalsPage: React.FC = () => {
rowKey="id" rowKey="id"
loading={loading} loading={loading}
locale={{ emptyText: <Empty description="暂无数据" /> }} locale={{ emptyText: <Empty description="暂无数据" /> }}
pagination={{ pageSize: 15, showTotal: (total) => `${total}` }} pagination={{
defaultPageSize: 15,
showSizeChanger: true,
pageSizeOptions: [15, 30, 50, 100],
showTotal: (total) => `${total}`,
}}
scroll={{ x: 1200 }} scroll={{ x: 1200 }}
/> />
<Modal <Modal

View File

@@ -287,7 +287,12 @@ const ClassroomsPage: React.FC = () => {
rowKey="id" rowKey="id"
loading={loading} loading={loading}
locale={{ emptyText: <Empty description="暂无数据" /> }} locale={{ emptyText: <Empty description="暂无数据" /> }}
pagination={{ pageSize: 20, showTotal: (total) => `${total}` }} pagination={{
defaultPageSize: 20,
showSizeChanger: true,
pageSizeOptions: [20, 50, 100],
showTotal: (total) => `${total}`,
}}
/> />
<Modal <Modal
title={editing ? '编辑教室' : '添加教室'} title={editing ? '编辑教室' : '添加教室'}

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import { buildDepositStudentOption } from './deposit-student-option'; import { buildDepositStudentOption, buildDepositStudentOptions } from './deposit-student-option';
describe('deposit student option', () => { describe('deposit student option', () => {
it('uses the student number as the non-sensitive identifier', () => { it('uses the student number as the non-sensitive identifier', () => {
@@ -17,4 +17,13 @@ describe('deposit student option', () => {
label: '张三 (#23)', label: '张三 (#23)',
}); });
}); });
it('uses lookup rows without requiring a status field', () => {
expect(buildDepositStudentOptions([{ id: 23, name: '张三', studentNo: 'S2026001' }])).toEqual([
{
value: 23,
label: '张三 (S2026001)',
},
]);
});
}); });

View File

@@ -8,3 +8,6 @@ export const buildDepositStudentOption = (student: DepositStudentLookup) => ({
value: student.id, value: student.id,
label: `${student.name} (${student.studentNo || `#${student.id}`})`, label: `${student.name} (${student.studentNo || `#${student.id}`})`,
}); });
export const buildDepositStudentOptions = (students: DepositStudentLookup[]) =>
students.map(buildDepositStudentOption);

View File

@@ -19,7 +19,7 @@ import dayjs from 'dayjs';
import api from '../../api'; import api from '../../api';
import PermissionButton from '../../components/PermissionButton'; import PermissionButton from '../../components/PermissionButton';
import { message } from '../../ui/app-message'; import { message } from '../../ui/app-message';
import { buildDepositStudentOption } from './deposit-student-option'; import { buildDepositStudentOptions } from './deposit-student-option';
const statusMap: Record<string, { text: string; color: string }> = { const statusMap: Record<string, { text: string; color: string }> = {
paid: { text: '已缴', color: 'green' }, paid: { text: '已缴', color: 'green' },
@@ -33,6 +33,10 @@ const installmentStatusMap: Record<string, { text: string; color: string }> = {
paid: { text: '已缴', color: 'green' }, paid: { text: '已缴', color: 'green' },
}; };
const isFormValidationError = (error: unknown) =>
typeof error === 'object'
&& error !== null
&& Array.isArray((error as { errorFields?: unknown }).errorFields);
const DepositsPage: React.FC = () => { const DepositsPage: React.FC = () => {
const [data, setData] = useState<any[]>([]); const [data, setData] = useState<any[]>([]);
@@ -82,17 +86,14 @@ const DepositsPage: React.FC = () => {
}, [data, searchText, filterStatus]); }, [data, searchText, filterStatus]);
const studentOptions = useMemo( const studentOptions = useMemo(
() => () => buildDepositStudentOptions(students),
students
.filter((s: any) => s.status === 'active')
.map(buildDepositStudentOption),
[students], [students],
); );
const handleCreate = async () => { const handleCreate = async () => {
setSaving(true); setSaving(true);
const values = await createForm.validateFields();
try { try {
const values = await createForm.validateFields();
await api.post('/deposits', { await api.post('/deposits', {
studentId: values.studentId, studentId: values.studentId,
amount: values.amount, amount: values.amount,
@@ -104,7 +105,9 @@ const DepositsPage: React.FC = () => {
createForm.resetFields(); createForm.resetFields();
fetchData(); fetchData();
} catch (e: any) { } catch (e: any) {
if (!isFormValidationError(e)) {
message.error(e?.message || '操作失败'); message.error(e?.message || '操作失败');
}
} finally { } finally {
setSaving(false); setSaving(false);
} }
@@ -112,8 +115,8 @@ const DepositsPage: React.FC = () => {
const handleRefund = async () => { const handleRefund = async () => {
setSaving(true); setSaving(true);
const values = await refundForm.validateFields();
try { try {
const values = await refundForm.validateFields();
await api.put(`/deposits/${refundModal.id}/refund`, { await api.put(`/deposits/${refundModal.id}/refund`, {
refundDate: values.refundDate.format('YYYY-MM-DD'), refundDate: values.refundDate.format('YYYY-MM-DD'),
deductionAmount: values.deductionAmount || 0, deductionAmount: values.deductionAmount || 0,
@@ -125,7 +128,9 @@ const DepositsPage: React.FC = () => {
refundForm.resetFields(); refundForm.resetFields();
fetchData(); fetchData();
} catch (e: any) { } catch (e: any) {
if (!isFormValidationError(e)) {
message.error(e?.message || '操作失败'); message.error(e?.message || '操作失败');
}
} finally { } finally {
setSaving(false); setSaving(false);
} }
@@ -133,8 +138,8 @@ const DepositsPage: React.FC = () => {
const handleAddInstallment = async () => { const handleAddInstallment = async () => {
if (installmentModal == null) return; if (installmentModal == null) return;
const values = await installmentForm.validateFields();
try { try {
const values = await installmentForm.validateFields();
await api.post(`/deposits/${installmentModal}/installments`, { await api.post(`/deposits/${installmentModal}/installments`, {
amount: values.amount, amount: values.amount,
dueDate: values.dueDate.format('YYYY-MM-DD'), dueDate: values.dueDate.format('YYYY-MM-DD'),
@@ -144,8 +149,10 @@ const DepositsPage: React.FC = () => {
installmentForm.resetFields(); installmentForm.resetFields();
fetchData(); fetchData();
} catch (e: any) { } catch (e: any) {
if (!isFormValidationError(e)) {
message.error(e?.message || '操作失败'); message.error(e?.message || '操作失败');
} }
}
}; };
const handlePayInstallment = async (installmentId: number) => { const handlePayInstallment = async (installmentId: number) => {
@@ -304,7 +311,12 @@ const DepositsPage: React.FC = () => {
rowKey="id" rowKey="id"
loading={loading} loading={loading}
scroll={{ x: 1200 }} scroll={{ x: 1200 }}
pagination={{ pageSize: 15, showTotal: (total) => `${total}` }} pagination={{
defaultPageSize: 15,
showSizeChanger: true,
pageSizeOptions: [15, 30, 50, 100],
showTotal: (total) => `${total}`,
}}
locale={{ emptyText: <Empty description="暂无数据" /> }} locale={{ emptyText: <Empty description="暂无数据" /> }}
/> />

View File

@@ -452,7 +452,12 @@ const ExpensesPage: React.FC = () => {
rowKey="id" rowKey="id"
loading={loading} loading={loading}
scroll={{ x: 1200 }} scroll={{ x: 1200 }}
pagination={{ pageSize: 15, showTotal: (total) => `${total}` }} pagination={{
defaultPageSize: 15,
showSizeChanger: true,
pageSizeOptions: [15, 30, 50, 100],
showTotal: (total) => `${total}`,
}}
locale={{ emptyText: <Empty description="暂无数据" /> }} locale={{ emptyText: <Empty description="暂无数据" /> }}
rowSelection={{ rowSelection={{
selectedRowKeys: selectedRoomKeys, selectedRowKeys: selectedRoomKeys,
@@ -575,7 +580,12 @@ const ExpensesPage: React.FC = () => {
rowKey="id" rowKey="id"
loading={loading} loading={loading}
scroll={{ x: 1200 }} scroll={{ x: 1200 }}
pagination={{ pageSize: 15, showTotal: (total) => `${total}` }} pagination={{
defaultPageSize: 15,
showSizeChanger: true,
pageSizeOptions: [15, 30, 50, 100],
showTotal: (total) => `${total}`,
}}
locale={{ emptyText: <Empty description="暂无数据" /> }} locale={{ emptyText: <Empty description="暂无数据" /> }}
rowSelection={{ rowSelection={{
selectedRowKeys: selectedPersonalKeys, selectedRowKeys: selectedPersonalKeys,

View File

@@ -522,7 +522,12 @@ const OccupanciesPage: React.FC = () => {
loading={loading} loading={loading}
locale={{ emptyText: <Empty description="暂无数据" /> }} locale={{ emptyText: <Empty description="暂无数据" /> }}
scroll={{ x: 1300 }} scroll={{ x: 1300 }}
pagination={{ pageSize: 15, showTotal: (total) => `${total}` }} pagination={{
defaultPageSize: 15,
showSizeChanger: true,
pageSizeOptions: [15, 30, 50, 100],
showTotal: (total) => `${total}`,
}}
rowSelection={rowSelection} rowSelection={rowSelection}
/> />
<Modal <Modal

View File

@@ -26,13 +26,14 @@ const OperationLogsPage: React.FC = () => {
const [total, setTotal] = useState(0); const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(20);
const [filterModule, setFilterModule] = useState<string | undefined>(); const [filterModule, setFilterModule] = useState<string | undefined>();
const [dateRange, setDateRange] = useState<[string, string] | null>(null); const [dateRange, setDateRange] = useState<[string, string] | null>(null);
const fetchData = useCallback(async () => { const fetchData = useCallback(async () => {
setLoading(true); setLoading(true);
try { try {
const params: any = { page, pageSize: 20 }; const params: any = { page, pageSize };
if (filterModule) params.module = filterModule; if (filterModule) params.module = filterModule;
if (dateRange) { if (dateRange) {
params.startDate = dateRange[0]; params.startDate = dateRange[0];
@@ -46,7 +47,7 @@ const OperationLogsPage: React.FC = () => {
message.error(err?.message || '加载失败,请稍后重试'); message.error(err?.message || '加载失败,请稍后重试');
} }
setLoading(false); setLoading(false);
}, [page, filterModule, dateRange]); }, [page, pageSize, filterModule, dateRange]);
useEffect(() => { useEffect(() => {
fetchData(); fetchData();
@@ -159,8 +160,13 @@ const OperationLogsPage: React.FC = () => {
pagination={{ pagination={{
current: page, current: page,
total, total,
pageSize: 20, pageSize,
onChange: setPage, showSizeChanger: true,
pageSizeOptions: [20, 50, 100],
onChange: (nextPage, nextPageSize) => {
setPage(nextPage);
setPageSize(nextPageSize);
},
showTotal: (t) => `${t}`, showTotal: (t) => `${t}`,
}} }}
/> />

View File

@@ -235,7 +235,12 @@ const OrganizationsPage: React.FC = () => {
loading={loading} loading={loading}
locale={{ emptyText: <Empty description="暂无机构" /> }} locale={{ emptyText: <Empty description="暂无机构" /> }}
scroll={{ x: 1100 }} scroll={{ x: 1100 }}
pagination={{ pageSize: 20, showTotal: (total) => `${total} 个机构` }} pagination={{
defaultPageSize: 20,
showSizeChanger: true,
pageSizeOptions: [20, 50, 100],
showTotal: (total) => `${total} 个机构`,
}}
/> />
<Modal <Modal
title={editing ? `编辑机构 · ${editing.name}` : '添加外部机构'} title={editing ? `编辑机构 · ${editing.name}` : '添加外部机构'}

View File

@@ -156,6 +156,11 @@ const RoomsPage: React.FC = () => {
if (filterStatus) result = result.filter((r: Record<string, unknown>) => r.status === filterStatus); if (filterStatus) result = result.filter((r: Record<string, unknown>) => r.status === filterStatus);
return result; return result;
}, [data, searchText, filterBuilding, filterStatus]); }, [data, searchText, filterBuilding, filterStatus]);
const remainingBedSlots = useMemo(() => {
const capacity = Number(drawerRoom?.capacity) || 0;
return Math.max(capacity - beds.length, 0);
}, [drawerRoom?.capacity, beds.length]);
const defaultBatchBedCount = Math.min(4, Math.max(remainingBedSlots, 1));
const handleSave = async () => { const handleSave = async () => {
const values = await form.validateFields(); const values = await form.validateFields();
@@ -167,7 +172,7 @@ const RoomsPage: React.FC = () => {
message.success('更新成功'); message.success('更新成功');
} else { } else {
await api.post('/rooms', payload); await api.post('/rooms', payload);
message.success('创建成功'); message.success(`创建成功,已自动生成 ${values.capacity} 张床位`);
} }
setModalOpen(false); setModalOpen(false);
form.resetFields(); form.resetFields();
@@ -514,7 +519,12 @@ const RoomsPage: React.FC = () => {
scroll={{ x: 1200 }} scroll={{ x: 1200 }}
loading={loading} loading={loading}
locale={{ emptyText: <Empty description="暂无数据" /> }} locale={{ emptyText: <Empty description="暂无数据" /> }}
pagination={{ pageSize: 20, showTotal: (total) => `${total}` }} pagination={{
defaultPageSize: 20,
showSizeChanger: true,
pageSizeOptions: [20, 50, 100],
showTotal: (total) => `${total}`,
}}
rowClassName={(record) => (record.status === 'archived' ? 'archived-row' : '')} rowClassName={(record) => (record.status === 'archived' ? 'archived-row' : '')}
rowSelection={{ rowSelection={{
selectedRowKeys, selectedRowKeys,
@@ -629,24 +639,26 @@ const RoomsPage: React.FC = () => {
type="primary" type="primary"
size="small" size="small"
icon={<PlusOutlined />} icon={<PlusOutlined />}
disabled={drawerRoom?.status === 'archived'} disabled={drawerRoom?.status === 'archived' || remainingBedSlots === 0}
onClick={() => { setBedEditing(null); bedForm.resetFields(); setBedModalOpen(true); }} onClick={() => { setBedEditing(null); bedForm.resetFields(); setBedModalOpen(true); }}
> >
</Button> </Button>
<Popconfirm <Popconfirm
title="批量生成床位" title={remainingBedSlots > 0 ? '批量生成床位' : '床位已达到额定人数'}
description={ description={
<InputNumber min={1} max={20} defaultValue={4} id="batch-bed-count" style={{ width: 80 }} /> remainingBedSlots > 0
? <InputNumber min={1} max={remainingBedSlots} defaultValue={defaultBatchBedCount} id="batch-bed-count" style={{ width: 80 }} />
: '如需增加床位,请先调整宿舍额定人数'
} }
onConfirm={() => { onConfirm={() => {
const input = document.getElementById('batch-bed-count') as HTMLInputElement; const input = document.getElementById('batch-bed-count') as HTMLInputElement;
handleBatchBeds(input ? parseInt(input.value) || 4 : 4); handleBatchBeds(input ? parseInt(input.value) || defaultBatchBedCount : defaultBatchBedCount);
}} }}
okText="生成" okText="生成"
disabled={drawerRoom?.status === 'archived'} disabled={drawerRoom?.status === 'archived' || remainingBedSlots === 0}
> >
<Button size="small" disabled={drawerRoom?.status === 'archived'}></Button> <Button size="small" disabled={drawerRoom?.status === 'archived' || remainingBedSlots === 0}></Button>
</Popconfirm> </Popconfirm>
</div> </div>
<Table <Table

View File

@@ -577,7 +577,12 @@ const StudentsPage: React.FC = () => {
loading={loading} loading={loading}
locale={{ emptyText: <Empty description="暂无数据" /> }} locale={{ emptyText: <Empty description="暂无数据" /> }}
scroll={{ x: 1410 }} scroll={{ x: 1410 }}
pagination={{ pageSize: 15, showTotal: (total) => `${total}` }} pagination={{
defaultPageSize: 15,
showSizeChanger: true,
pageSizeOptions: [15, 30, 50, 100],
showTotal: (total) => `${total}`,
}}
rowClassName={(record: any) => (record.status === 'archived' ? 'archived-row' : '')} rowClassName={(record: any) => (record.status === 'archived' ? 'archived-row' : '')}
rowSelection={{ rowSelection={{
selectedRowKeys, selectedRowKeys,

View File

@@ -146,7 +146,12 @@ const TeacherWorkspacePage: React.FC = () => {
columns={classColumns} columns={classColumns}
dataSource={data.assignedClasses} dataSource={data.assignedClasses}
rowKey="classId" rowKey="classId"
pagination={{ pageSize: 20, showTotal: (total) => `${total} 个班级` }} pagination={{
defaultPageSize: 20,
showSizeChanger: true,
pageSizeOptions: [20, 50, 100],
showTotal: (total) => `${total} 个班级`,
}}
/> />
) : ( ) : (
<Empty description="暂无分配的班级" /> <Empty description="暂无分配的班级" />
@@ -160,7 +165,12 @@ const TeacherWorkspacePage: React.FC = () => {
columns={scheduleColumns} columns={scheduleColumns}
dataSource={data.todaySchedules} dataSource={data.todaySchedules}
rowKey="id" rowKey="id"
pagination={{ pageSize: 20, showTotal: (total) => `${total}` }} pagination={{
defaultPageSize: 20,
showSizeChanger: true,
pageSizeOptions: [20, 50, 100],
showTotal: (total) => `${total}`,
}}
/> />
) : ( ) : (
<Empty description="今日无排课" /> <Empty description="今日无排课" />
@@ -174,7 +184,12 @@ const TeacherWorkspacePage: React.FC = () => {
columns={studentColumns} columns={studentColumns}
dataSource={data.myStudents} dataSource={data.myStudents}
rowKey="studentId" rowKey="studentId"
pagination={{ pageSize: 20, showTotal: (total) => `${total}` }} pagination={{
defaultPageSize: 20,
showSizeChanger: true,
pageSizeOptions: [20, 50, 100],
showTotal: (total) => `${total}`,
}}
/> />
) : ( ) : (
<Empty description="暂无学生" /> <Empty description="暂无学生" />

View File

@@ -46,13 +46,14 @@ const ROLE_TYPE_LABELS: Record<string, string> = {
academic_teacher: '教务老师', academic_teacher: '教务老师',
}; };
const PAGE_SIZE = 20; const DEFAULT_PAGE_SIZE = 20;
const TeachersPage: React.FC = () => { const TeachersPage: React.FC = () => {
const [data, setData] = useState<TeacherRow[]>([]); const [data, setData] = useState<TeacherRow[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [total, setTotal] = useState(0); const [total, setTotal] = useState(0);
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE);
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [profileModal, setProfileModal] = useState<TeacherRow | null>(null); const [profileModal, setProfileModal] = useState<TeacherRow | null>(null);
const [form] = Form.useForm<ProfileFormValues>(); const [form] = Form.useForm<ProfileFormValues>();
@@ -62,7 +63,7 @@ const TeachersPage: React.FC = () => {
setLoading(true); setLoading(true);
try { try {
const res = await api.get<TeacherListResponse>('/rbac/teachers', { const res = await api.get<TeacherListResponse>('/rbac/teachers', {
params: { search: search || undefined, page, pageSize: PAGE_SIZE }, params: { search: search || undefined, page, pageSize },
}); });
setData(res.list); setData(res.list);
setTotal(res.total); setTotal(res.total);
@@ -70,7 +71,7 @@ const TeachersPage: React.FC = () => {
// silent // silent
} }
setLoading(false); setLoading(false);
}, [page, search]); }, [page, pageSize, search]);
useEffect(() => { useEffect(() => {
fetchData(); fetchData();
@@ -199,9 +200,14 @@ const TeachersPage: React.FC = () => {
scroll={{ x: 1300 }} scroll={{ x: 1300 }}
pagination={{ pagination={{
current: page, current: page,
pageSize: PAGE_SIZE, pageSize,
total, total,
onChange: setPage, showSizeChanger: true,
pageSizeOptions: [20, 50, 100],
onChange: (nextPage, nextPageSize) => {
setPage(nextPage);
setPageSize(nextPageSize);
},
showTotal: (t) => `${t}`, showTotal: (t) => `${t}`,
}} }}
expandable={{ expandable={{

View File

@@ -5,7 +5,7 @@ import { Bill } from '../entities/bill.entity';
import { BillItem } from '../entities/bill-item.entity'; import { BillItem } from '../entities/bill-item.entity';
import { Deposit } from '../entities/deposit.entity'; import { Deposit } from '../entities/deposit.entity';
import * as ExcelJS from 'exceljs'; import * as ExcelJS from 'exceljs';
import * as PDFDocument from 'pdfkit'; import PDFDocument from 'pdfkit';
import { Response } from 'express'; import { Response } from 'express';
@Injectable() @Injectable()

View File

@@ -110,7 +110,9 @@ export class RoomsService {
roomType: dto.roomType ?? parsed.roomType, roomType: dto.roomType ?? parsed.roomType,
capacity: dto.capacity ?? parsed.capacity, capacity: dto.capacity ?? parsed.capacity,
}); });
return this.repo.save(entity); const room = await this.repo.save(entity);
await this.createDefaultBeds(room.id, room.capacity);
return room;
} }
async update(id: number, dto: UpdateRoomDto) { async update(id: number, dto: UpdateRoomDto) {
@@ -312,7 +314,7 @@ export class RoomsService {
} }
// 智能解析房间号 // 智能解析房间号
const parsed = RoomsService.parseRoomNumber(row.roomNumber.trim()); const parsed = RoomsService.parseRoomNumber(row.roomNumber.trim());
await this.repo.save( const room = await this.repo.save(
this.repo.create({ this.repo.create({
roomNumber: row.roomNumber.trim(), roomNumber: row.roomNumber.trim(),
building: row.building?.trim() || parsed.building || undefined, building: row.building?.trim() || parsed.building || undefined,
@@ -323,6 +325,7 @@ export class RoomsService {
monthlyRate: row.monthlyRate ?? undefined, monthlyRate: row.monthlyRate ?? undefined,
}), }),
); );
await this.createDefaultBeds(room.id, room.capacity);
imported++; imported++;
} }
return { return {
@@ -353,6 +356,7 @@ export class RoomsService {
const room = await this.repo.findOne({ where: { id: roomId } }); const room = await this.repo.findOne({ where: { id: roomId } });
if (!room) throw new NotFoundException('宿舍不存在'); if (!room) throw new NotFoundException('宿舍不存在');
if (room.status === 'archived') throw new BadRequestException('已归档宿舍不能添加床位'); if (room.status === 'archived') throw new BadRequestException('已归档宿舍不能添加床位');
await this.assertCanAddBeds(room, 1);
const existing = await this.bedRepo.findOne({ where: { roomId, bedNumber: dto.bedNumber } }); const existing = await this.bedRepo.findOne({ where: { roomId, bedNumber: dto.bedNumber } });
if (existing) throw new BadRequestException('该床位编号已存在'); if (existing) throw new BadRequestException('该床位编号已存在');
const bed = this.bedRepo.create({ ...dto, roomId }); const bed = this.bedRepo.create({ ...dto, roomId });
@@ -387,6 +391,7 @@ export class RoomsService {
if (!room) throw new NotFoundException('宿舍不存在'); if (!room) throw new NotFoundException('宿舍不存在');
if (room.status === 'archived') throw new BadRequestException('已归档宿舍不能添加床位'); if (room.status === 'archived') throw new BadRequestException('已归档宿舍不能添加床位');
const existing = await this.bedRepo.find({ where: { roomId }, order: { bedNumber: 'ASC' } }); const existing = await this.bedRepo.find({ where: { roomId }, order: { bedNumber: 'ASC' } });
this.assertCanAddBedsFromCount(room, existing.length, dto.count);
const numbers = existing.map((b) => { const numbers = existing.map((b) => {
const match = b.bedNumber.match(/^\d+/); const match = b.bedNumber.match(/^\d+/);
return match ? parseInt(match[0]) : 0; return match ? parseInt(match[0]) : 0;
@@ -399,6 +404,27 @@ export class RoomsService {
return this.bedRepo.save(beds); return this.bedRepo.save(beds);
} }
private async createDefaultBeds(roomId: number, capacity: number): Promise<void> {
const count = Math.max(capacity ?? 0, 0);
if (count === 0) return;
const beds = Array.from({ length: count }, (_, index) =>
this.bedRepo.create({ roomId, bedNumber: `${index + 1}号床` }),
);
await this.bedRepo.save(beds);
}
private async assertCanAddBeds(room: Room, count: number): Promise<void> {
const existingCount = await this.bedRepo.count({ where: { roomId: room.id } });
this.assertCanAddBedsFromCount(room, existingCount, count);
}
private assertCanAddBedsFromCount(room: Room, existingCount: number, count: number): void {
const remaining = Math.max((room.capacity ?? 0) - existingCount, 0);
if (count > remaining) {
throw new BadRequestException(`床位不能超过额定人数,当前已有 ${existingCount} 张,额定 ${room.capacity} 张,最多还能添加 ${remaining}`);
}
}
// ── 柜子管理 ── // ── 柜子管理 ──
async getRoomLockers(roomId: number): Promise<Locker[]> { async getRoomLockers(roomId: number): Promise<Locker[]> {