feat: 重构各业务模块管理页面与服务

This commit is contained in:
2026-08-05 17:12:00 +08:00
parent 80e6fccf05
commit fd39e1686a
163 changed files with 18409 additions and 13449 deletions

View File

@@ -0,0 +1,551 @@
// aislop-ignore-file: duplicate-block -- 退宿/换房表单结构相似且字段不同,已共享 DateFormItem
import React from 'react';
import {
DatePicker,
Form,
Input,
InputNumber,
Modal,
Select,
Switch,
Tag,
} from 'antd';
import type { Dayjs } from 'dayjs';
import { maskIdNumber, maskPhone } from '../../utils/sensitive';
import type { OccupancyRow } from './OccupancyColumns';
export type FormRule = React.ComponentProps<typeof Form.Item>['rules'];
export const DateFormItem: React.FC<{
name: string;
label: string;
placeholder: string;
required?: boolean;
dependencies?: string[];
extra?: string;
rules?: FormRule;
}> = ({ name, label, placeholder, required, dependencies, extra, rules }) => (
<Form.Item
name={name}
label={label}
dependencies={dependencies}
extra={extra}
rules={[
...(required ? [{ required: true, message: `请选择${label}` }] : []),
...(rules ?? []),
]}
>
<DatePicker style={{ width: '100%' }} placeholder={placeholder} format="YYYY-MM-DD" />
</Form.Item>
);
export const CheckInModal: React.FC<{
open: boolean;
canCheckIn: boolean;
saving: boolean;
form: ReturnType<typeof Form.useForm>[0];
students: any[];
activeOccupancyByStudentId: Map<number, OccupancyRow>;
rooms: any[];
roomOptionLabel: (room: any) => string;
isRoomSelectable: (room: any) => boolean;
onRoomChange: (roomId: number) => void;
availableBeds: any[];
availableLockers: any[];
availableResourcesLoading: boolean;
selectedCheckInRoomId?: number;
dateNotBefore: (start: string | Dayjs | null | undefined, messageText: string) => unknown;
onOk: () => void;
onCancel: () => void;
}> = ({
open,
canCheckIn,
saving,
form,
students,
activeOccupancyByStudentId,
rooms,
roomOptionLabel,
isRoomSelectable,
onRoomChange,
availableBeds,
availableLockers,
availableResourcesLoading,
selectedCheckInRoomId,
dateNotBefore,
onOk,
onCancel,
}) => {
return (
<Modal
title="入住登记"
open={open && canCheckIn}
onOk={canCheckIn ? onOk : undefined}
onCancel={onCancel}
okText="确认入住"
confirmLoading={saving}
width={500}
>
<Form form={form} layout="vertical">
<Form.Item name="studentId" label="选择学生" rules={[{ required: true, message: '请选择学生' }]}>
<Select
showSearch
optionFilterProp="label"
placeholder="搜索并选择学生"
options={students
.filter((s) => s.status === 'active')
.map((s) => {
const activeOccupancy = activeOccupancyByStudentId.get(s.id);
const identifier = s.idNumber
? maskIdNumber(s.idNumber)
: s.phone
? maskPhone(s.phone)
: '';
return {
value: s.id,
label: `${s.name} (${identifier})${
activeOccupancy
? ` · 已入住${activeOccupancy.room?.roomNumber ? ` ${activeOccupancy.room.roomNumber}` : ''}`
: ''
}`,
disabled: !!activeOccupancy,
};
})}
/>
</Form.Item>
<Form.Item name="roomId" label="选择宿舍" rules={[{ required: true, message: '请选择宿舍' }]}>
<Select
showSearch
optionFilterProp="label"
placeholder="搜索并选择宿舍"
onChange={onRoomChange}
options={rooms.map((r) => ({
value: r.id,
label: roomOptionLabel(r),
disabled: !isRoomSelectable(r),
}))}
/>
</Form.Item>
<DateFormItem name="checkInDate" label="入住日期" placeholder="选择入住日期" required />
<DateFormItem
name="billingStartDate"
label="计费起始日"
placeholder="选择计费起始日"
required
dependencies={['checkInDate']}
extra="默认与入住日期相同,可调整(如学生要求从次日开始计费)"
rules={[
({ getFieldValue }) => ({
validator: dateNotBefore(
getFieldValue('checkInDate'),
'计费起始日不能早于入住日期',
) as never,
}),
]}
/>
<Form.Item name="stayType" label="入住类型" rules={[{ required: true, message: '请选择入住类型' }]}>
<Select
options={[
{ value: 'short', label: '短租' },
{ value: 'long', label: '长租' },
]}
placeholder="默认为短租"
/>
</Form.Item>
<Form.Item
name="bedId"
label="床位"
rules={[
{ required: true, message: '请选择床位' },
{
validator: (_: unknown, value?: number) =>
!value || availableBeds.some((bed) => bed.id === value)
? Promise.resolve()
: Promise.reject(new Error('请选择当前宿舍下的可用床位')),
},
]}
>
<Select
placeholder={selectedCheckInRoomId ? '请选择床位' : '请先选择房间'}
loading={availableResourcesLoading}
disabled={!selectedCheckInRoomId || availableResourcesLoading || availableBeds.length === 0}
options={availableBeds.map((b) => ({
value: b.id,
label: b.bedNumber,
}))}
notFoundContent={selectedCheckInRoomId ? '该房间暂无可用床位' : '请先选择房间'}
/>
</Form.Item>
{availableBeds.length > 0 && (
<div style={{ marginTop: -16, marginBottom: 16, color: '#888', fontSize: 12 }}>
{availableBeds.length}
</div>
)}
<Form.Item name="lockerId" label="柜子(可选)">
<Select
allowClear
placeholder="可选分配柜子"
loading={availableResourcesLoading}
disabled={!selectedCheckInRoomId || availableResourcesLoading || availableLockers.length === 0}
options={availableLockers.map((l) => ({
value: l.id,
label: l.lockerNumber,
}))}
/>
</Form.Item>
<Form.Item
name="collectDeposit"
label="押金缴纳"
valuePropName="checked"
extra="开启后,确认入住时同步生成已缴押金记录;已有已缴押金时不会重复创建"
>
<Switch checkedChildren="已缴" unCheckedChildren="不缴" />
</Form.Item>
<Form.Item
noStyle
shouldUpdate={(prev, current) => prev.collectDeposit !== current.collectDeposit}
>
{({ getFieldValue }) =>
getFieldValue('collectDeposit') ? (
<Form.Item
name="depositAmount"
label="押金金额"
rules={[{ required: true, message: '请输入押金金额' }]}
>
<InputNumber min={0.01} precision={2} addonAfter="元" style={{ width: '100%' }} />
</Form.Item>
) : null
}
</Form.Item>
<Form.Item name="notes" label="备注">
<Input.TextArea rows={2} />
</Form.Item>
</Form>
</Modal>
);
};
export const CheckOutModal: React.FC<{
record: OccupancyRow | null;
canCheckOut: boolean;
saving: boolean;
form: ReturnType<typeof Form.useForm>[0];
dateNotBefore: (start: string | Dayjs | null | undefined, messageText: string) => unknown;
onOk: () => void;
onCancel: () => void;
}> = ({ record, canCheckOut, saving, form, dateNotBefore, onOk, onCancel }) => {
return (
<Modal
title={`退宿 - ${record?.student?.name}`}
open={!!record && canCheckOut}
onOk={canCheckOut ? onOk : undefined}
onCancel={onCancel}
okText="确认退宿"
confirmLoading={saving}
>
<Form form={form} layout="vertical">
<DateFormItem
name="checkOutDate"
label="退宿日期"
placeholder="选择退宿日期"
required
rules={[
{ validator: dateNotBefore(record?.checkInDate, '退宿日期不能早于入住日期') as never },
]}
/>
<DateFormItem
name="billingEndDate"
label="计费截止日"
placeholder="选择计费截止日"
dependencies={['checkOutDate']}
extra="默认与退宿日期相同"
rules={[
({ getFieldValue }) => ({
validator: dateNotBefore(
record?.billingStartDate || record?.checkInDate || getFieldValue('checkOutDate'),
'计费截止日不能早于计费起始日',
) as never,
}),
]}
/>
<Form.Item name="checkOutReason" label="退宿原因">
<Select
allowClear
options={[
{ value: '换房', label: '换房' },
{ value: '退训', label: '退训' },
{ value: '结业', label: '结业' },
{ value: '毕业', label: '毕业' },
{ value: '其他', label: '其他' },
]}
/>
</Form.Item>
</Form>
</Modal>
);
};
export const BatchCheckOutModal: React.FC<{
open: boolean;
canCheckOut: boolean;
selectedRowKeys: number[];
latestSelectedCheckInDate?: string;
latestSelectedBillingStartDate?: string;
data: any[];
form: ReturnType<typeof Form.useForm>[0];
dateNotBefore: (start: string | Dayjs | null | undefined, messageText: string) => unknown;
onOk: () => void;
onCancel: () => void;
}> = ({
open,
canCheckOut,
selectedRowKeys,
latestSelectedCheckInDate,
latestSelectedBillingStartDate,
data,
form,
dateNotBefore,
onOk,
onCancel,
}) => {
return (
<Modal
title={`批量退宿(${selectedRowKeys.length} 人)`}
open={open && canCheckOut}
onOk={canCheckOut ? onOk : undefined}
onCancel={onCancel}
okText="确认批量退宿"
width={500}
>
<Form form={form} layout="vertical">
<DateFormItem
name="checkOutDate"
label="退宿日期"
placeholder="选择退宿日期"
required
rules={[
{
validator: dateNotBefore(
latestSelectedCheckInDate,
'退宿日期不能早于所选记录中最晚的入住日期',
) as never,
},
]}
/>
<DateFormItem
name="billingEndDate"
label="计费截止日"
placeholder="选择计费截止日"
extra="默认与退宿日期相同"
rules={[
{
validator: dateNotBefore(
latestSelectedBillingStartDate,
'计费截止日不能早于所选记录中最晚的计费起始日',
) as never,
},
]}
/>
<Form.Item name="checkOutReason" label="退宿原因">
<Select
allowClear
options={[
{ value: '结业', label: '结业' },
{ value: '退训', label: '退训' },
{ value: '毕业', label: '毕业' },
{ value: '其他', label: '其他' },
]}
/>
</Form.Item>
</Form>
<div
style={{
marginTop: 12,
padding: '8px 12px',
background: '#f5f5f5',
borderRadius: 6,
maxHeight: 150,
overflow: 'auto',
}}
>
<div style={{ fontSize: 12, color: '#666', marginBottom: 4 }}>退宿</div>
{data
.filter((r: any) => selectedRowKeys.includes(r.id))
.map((r: any) => (
<Tag key={r.id} style={{ marginBottom: 4 }}>
{r.student?.name} ({r.room?.roomNumber})
</Tag>
))}
</div>
</Modal>
);
};
export const TransferModal: React.FC<{
record: OccupancyRow | null;
canTransfer: boolean;
saving: boolean;
form: ReturnType<typeof Form.useForm>[0];
rooms: any[];
roomOptionLabel: (room: any) => string;
isRoomSelectable: (room: any) => boolean;
onRoomChange: (roomId: number) => void;
transferAvailableBeds: any[];
transferAvailableLockers: any[];
transferResourcesLoading: boolean;
selectedTransferRoomId?: number;
dateNotBefore: (start: string | Dayjs | null | undefined, messageText: string) => unknown;
onOk: () => void;
onCancel: () => void;
}> = ({
record,
canTransfer,
saving,
form,
rooms,
roomOptionLabel,
isRoomSelectable,
onRoomChange,
transferAvailableBeds,
transferAvailableLockers,
transferResourcesLoading,
selectedTransferRoomId,
dateNotBefore,
onOk,
onCancel,
}) => {
return (
<Modal
title={`换房 - ${record?.student?.name}`}
open={!!record && canTransfer}
onOk={onOk}
onCancel={onCancel}
okText="确认换房"
confirmLoading={saving}
width={500}
>
<Form form={form} layout="vertical">
<Form.Item name="newRoomId" label="目标宿舍" rules={[{ required: true }]}>
<Select
showSearch
optionFilterProp="label"
placeholder="选择目标宿舍"
onChange={onRoomChange}
options={rooms
.filter((r) => r.id !== record?.roomId)
.map((r) => ({
value: r.id,
label: roomOptionLabel(r),
disabled: !isRoomSelectable(r),
}))}
/>
</Form.Item>
<Form.Item
name="newBedId"
label="目标床位"
rules={[
{ required: true, message: '请选择目标床位' },
{
validator: (_: unknown, value?: number) =>
!value || transferAvailableBeds.some((bed) => bed.id === value)
? Promise.resolve()
: Promise.reject(new Error('请选择目标宿舍下的可用床位')),
},
]}
>
<Select
placeholder={selectedTransferRoomId ? '请选择目标床位' : '请先选择目标宿舍'}
loading={transferResourcesLoading}
disabled={
!selectedTransferRoomId ||
transferResourcesLoading ||
transferAvailableBeds.length === 0
}
options={transferAvailableBeds.map((bed) => ({
value: bed.id,
label: bed.bedNumber,
}))}
notFoundContent={selectedTransferRoomId ? '目标宿舍暂无可用床位' : '请先选择目标宿舍'}
/>
</Form.Item>
{transferAvailableBeds.length > 0 && (
<div style={{ marginTop: -16, marginBottom: 16, color: '#888', fontSize: 12 }}>
{transferAvailableBeds.length}
</div>
)}
<Form.Item name="newLockerId" label="目标柜子(可选)">
<Select
allowClear
placeholder="可选分配目标宿舍柜子"
loading={transferResourcesLoading}
disabled={
!selectedTransferRoomId ||
transferResourcesLoading ||
transferAvailableLockers.length === 0
}
options={transferAvailableLockers.map((locker) => ({
value: locker.id,
label: locker.lockerNumber,
}))}
notFoundContent="目标宿舍暂无可用柜子"
/>
</Form.Item>
<Form.Item
name="transferDate"
label="换房日期"
rules={[
{ required: true, message: '请选择换房日期' },
{
validator: dateNotBefore(record?.checkInDate, '换房日期不能早于原入住日期') as never,
},
]}
>
<DatePicker style={{ width: '100%' }} placeholder="选择换房日期" format="YYYY-MM-DD" />
</Form.Item>
<Form.Item
name="oldBillingEndDate"
label="旧房计费截止日"
dependencies={['transferDate']}
extra="默认为换房当天"
rules={[
({ getFieldValue }) => ({
validator: dateNotBefore(
record?.billingStartDate || record?.checkInDate || getFieldValue('transferDate'),
'旧房计费截止日不能早于计费起始日',
) as never,
}),
]}
>
<DatePicker
style={{ width: '100%' }}
placeholder="选择旧房计费截止日"
format="YYYY-MM-DD"
/>
</Form.Item>
<Form.Item
name="newBillingStartDate"
label="新房计费起始日"
dependencies={['transferDate']}
extra="默认为换房次日"
rules={[
({ getFieldValue }) => ({
validator: dateNotBefore(
getFieldValue('transferDate'),
'新房计费起始日不能早于换房日期',
) as never,
}),
]}
>
<DatePicker
style={{ width: '100%' }}
placeholder="选择新房计费起始日"
format="YYYY-MM-DD"
/>
</Form.Item>
<Form.Item name="reason" label="换房原因">
<Input />
</Form.Item>
</Form>
</Modal>
);
};