Files
gongxue-base/apps/admin/src/pages/Occupancies/OccupancyModals.tsx
wangziqi 026a9f35d8 fix(admin): 迁移 antd v6 弃用 API 并清理控制台警告
- Card bordered -> variant;Modal maskClosable -> mask.closable
- InputNumber addonBefore/After -> prefix/suffix;Statistic valueStyle -> styles.content
- Alert message -> title;Steps items.description -> items.content
- 移除考勤页未挂载 Form 上的 setFieldsValue,消除 useForm 警告
2026-08-06 11:58:58 +08:00

552 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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} suffix="元" 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>
);
};