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,423 @@
import React from 'react';
import {
Card,
DatePicker,
Empty,
Form,
Input,
InputNumber,
Modal,
Popconfirm,
Select,
Space,
Table,
Tag,
} from 'antd';
import { DollarOutlined, InboxOutlined, PlusOutlined } from '@ant-design/icons';
import PermissionButton from '../../components/PermissionButton';
import EditableCell from '../../components/EditableCell';
import type { DepositStudentLookup } from './deposit-student-option';
export interface DepositRecord {
id: number;
studentId: number;
amount: number;
status: string;
paidDate: string;
refundDate?: string | null;
notes?: string | null;
installments?: Array<{
id: number;
amount: number;
dueDate: string;
paidDate?: string | null;
status: string;
}>;
student?: DepositStudentLookup;
}
export interface EligibleStudent {
studentId: number;
studentName: string;
studentNo?: string | null;
roomId: number;
roomNumber: string;
building?: string | null;
roomType?: string | null;
capacity: number;
depositAmount: number;
}
export const statusMap: Record<string, { text: string; color: string }> = {
paid: { text: '有余额', color: 'green' },
refunded: { text: '已全退', color: 'blue' },
depleted: { text: '已扣完', color: 'red' },
};
export const installmentStatusMap: Record<string, { text: string; color: string }> = {
pending: { text: '待缴', color: 'orange' },
paid: { text: '已缴', color: 'green' },
};
export const roomTypeOptions = [
{ value: '单人间', label: '单人间' },
{ value: '四人间', label: '四人间' },
];
export const suggestedDepositByRoomType: Record<string, number> = {
单人间: 200,
四人间: 100,
};
export interface DepositModalsProps {
batchModal: boolean;
createModal: boolean;
refundModal: DepositRecord | null;
detailModal: DepositRecord | null;
installmentModal: number | null;
batchForm: ReturnType<typeof Form.useForm>[0];
createForm: ReturnType<typeof Form.useForm>[0];
refundForm: ReturnType<typeof Form.useForm>[0];
installmentForm: ReturnType<typeof Form.useForm>[0];
saving: boolean;
batchRoomType: string;
effectiveSelectedEligibleIds: number[];
eligibleStudents: EligibleStudent[];
eligibleLoading: boolean;
eligibleColumns: Array<{ title: string; render?: unknown; dataIndex?: string }>;
studentOptions: Array<{ value: number; label: string }>;
onBatchRoomTypeChange: (roomType: string) => void;
onBatchCreate: () => void;
onCreate: () => void;
onRefund: () => void;
onAddInstallment: () => void;
onPayInstallment: (installmentId: number) => void;
onSaveInstallmentCell: (
installmentId: number,
field: 'status' | 'paidDate',
value: unknown,
) => void;
onDeleteInstallment: (installmentId: number) => void;
onCloseBatch: () => void;
onCloseCreate: () => void;
onCloseRefund: () => void;
onCloseDetail: () => void;
onCloseInstallment: () => void;
onOpenInstallment: (id: number) => void;
onSelectEligible: (ids: number[]) => void;
}
export const DepositModals: React.FC<DepositModalsProps> = ({
batchModal,
createModal,
refundModal,
detailModal,
installmentModal,
batchForm,
createForm,
refundForm,
installmentForm,
saving,
batchRoomType,
effectiveSelectedEligibleIds,
eligibleStudents,
eligibleLoading,
eligibleColumns,
studentOptions,
onBatchRoomTypeChange,
onBatchCreate,
onCreate,
onRefund,
onAddInstallment,
onPayInstallment,
onSaveInstallmentCell,
onDeleteInstallment,
onCloseBatch,
onCloseCreate,
onCloseRefund,
onCloseDetail,
onCloseInstallment,
onOpenInstallment,
onSelectEligible,
}) => {
return (
<>
<Modal
title="按房型批量收取押金"
open={batchModal}
onOk={onBatchCreate}
onCancel={onCloseBatch}
okText="确认批量收取"
confirmLoading={saving}
okButtonProps={{ disabled: effectiveSelectedEligibleIds.length === 0 }}
width={760}
>
<Form form={batchForm} layout="vertical">
<Space style={{ width: '100%' }} align="start" wrap>
<Form.Item
name="roomType"
label="房型"
rules={[{ required: true, message: '请选择房型' }]}
>
<Select
style={{ width: 140 }}
options={roomTypeOptions}
onChange={onBatchRoomTypeChange}
/>
</Form.Item>
<Form.Item
name="amount"
label="每人收取金额(元)"
rules={[{ required: true, message: '请输入金额' }]}
>
<InputNumber min={0.01} precision={2} style={{ width: 180 }} />
</Form.Item>
<Form.Item
name="paidDate"
label="收取日期"
rules={[{ required: true, message: '请选择日期' }]}
>
<DatePicker style={{ width: 180 }} placeholder="选择收取日期" format="YYYY-MM-DD" />
</Form.Item>
</Space>
<Form.Item name="notes" label="备注">
<Input.TextArea rows={2} placeholder={`${batchRoomType}押金`} />
</Form.Item>
</Form>
<div style={{ marginBottom: 8 }}>
<strong>{effectiveSelectedEligibleIds.length}</strong> / {eligibleStudents.length}{' '}
{suggestedDepositByRoomType[batchRoomType] && (
<span style={{ color: '#999', marginLeft: 8 }}>
¥{suggestedDepositByRoomType[batchRoomType]}
</span>
)}
</div>
<Table
size="small"
columns={eligibleColumns as never}
dataSource={eligibleStudents}
rowKey="studentId"
loading={eligibleLoading}
locale={{ emptyText: <Empty description="暂无符合条件的在住人员" /> }}
pagination={{ pageSize: 6, showSizeChanger: false }}
rowSelection={{
selectedRowKeys: effectiveSelectedEligibleIds,
onChange: (keys) => onSelectEligible(keys as number[]),
}}
/>
</Modal>
<Modal
title="收取押金"
open={createModal}
onOk={onCreate}
onCancel={onCloseCreate}
okText="确认"
confirmLoading={saving}
>
<Form form={createForm} layout="vertical">
<Form.Item
name="studentId"
label="学生"
rules={[{ required: true, message: '请选择学生' }]}
>
<Select
showSearch
optionFilterProp="label"
placeholder="搜索并选择学生"
options={studentOptions}
/>
</Form.Item>
<Form.Item name="amount" label="本次收取金额(元)" rules={[{ required: true }]}>
<InputNumber min={0.01} precision={2} style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="paidDate" label="收取日期" rules={[{ required: true }]}>
<DatePicker style={{ width: '100%' }} placeholder="选择收取日期" format="YYYY-MM-DD" />
</Form.Item>
<Form.Item name="notes" label="备注">
<Input.TextArea rows={2} />
</Form.Item>
</Form>
</Modal>
<Modal
title={`退还押金 - ${refundModal?.student?.name}`}
open={!!refundModal}
onOk={onRefund}
onCancel={onCloseRefund}
okText="确认退还"
confirmLoading={saving}
>
<Form form={refundForm} layout="vertical">
<div style={{ marginBottom: 16, padding: 12, background: '#f5f5f5', borderRadius: 8 }}>
: <strong>¥{Number(refundModal?.amount || 0).toFixed(2)}</strong>
</div>
<Form.Item name="refundDate" label="退还日期" rules={[{ required: true }]}>
<DatePicker style={{ width: '100%' }} placeholder="选择退还日期" format="YYYY-MM-DD" />
</Form.Item>
<Form.Item name="notes" label="备注">
<Input.TextArea rows={2} />
</Form.Item>
</Form>
</Modal>
<Modal
title={`押金详情 - ${detailModal?.student?.name}`}
open={!!detailModal}
onCancel={onCloseDetail}
footer={null}
width={640}
>
{detailModal && (
<div>
<Card size="small" style={{ marginBottom: 16 }}>
<p>
<strong>:</strong> ¥{Number(detailModal.amount).toFixed(2)}
</p>
<p>
<strong>:</strong> {detailModal.paidDate}
</p>
<p>
<strong>:</strong>{' '}
<Tag color={statusMap[detailModal.status]?.color}>
{statusMap[detailModal.status]?.text || detailModal.status}
</Tag>
</p>
{detailModal.notes && (
<p>
<strong>:</strong> {detailModal.notes}
</p>
)}
</Card>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 8,
}}
>
<h4 style={{ margin: 0 }}></h4>
<PermissionButton
permission="deposit:edit"
size="small"
type="primary"
icon={<PlusOutlined />}
onClick={() => onOpenInstallment(detailModal.id)}
>
</PermissionButton>
</div>
{detailModal.installments && detailModal.installments.length > 0 ? (
<Table
size="small"
pagination={false}
rowKey="id"
dataSource={detailModal.installments}
columns={[
{
title: '金额',
dataIndex: 'amount',
render: (value: number) => `¥${value.toFixed(2)}`,
},
{ title: '到期日', dataIndex: 'dueDate' },
{
title: '实付日',
dataIndex: 'paidDate',
render: (value: string, item: any) => (
<EditableCell
value={value}
editor="date"
permission="deposit:edit"
onSave={async (next) =>
onSaveInstallmentCell(item.id, 'paidDate', next)
}
>
{value || '-'}
</EditableCell>
),
},
{
title: '状态',
dataIndex: 'status',
render: (value: string, item: any) => (
<EditableCell
value={value}
editor="select"
options={[
{ value: 'pending', label: '待缴' },
{ value: 'paid', label: '已缴' },
{ value: 'overdue', label: '逾期' },
]}
permission="deposit:edit"
onSave={async (next) =>
onSaveInstallmentCell(item.id, 'status', next)
}
>
<Tag color={installmentStatusMap[value]?.color}>
{installmentStatusMap[value]?.text || value}
</Tag>
</EditableCell>
),
},
{
title: '操作',
render: (_: unknown, item: any) => (
<Space>
{item.status === 'pending' && (
<PermissionButton
permission="deposit:edit"
size="small"
type="primary"
icon={<DollarOutlined />}
onClick={() => onPayInstallment(item.id)}
>
</PermissionButton>
)}
<Popconfirm
title="确定归档?"
onConfirm={() => onDeleteInstallment(item.id)}
>
<PermissionButton
permission="deposit:delete"
size="small"
danger
icon={<InboxOutlined />}
>
</PermissionButton>
</Popconfirm>
</Space>
),
},
]}
/>
) : (
<p style={{ color: '#999' }}></p>
)}
</div>
)}
</Modal>
<Modal
title="添加分期"
open={installmentModal != null}
onOk={onAddInstallment}
onCancel={onCloseInstallment}
okText="确认"
>
<Form form={installmentForm} layout="vertical">
<Form.Item name="amount" label="分期金额(元)" rules={[{ required: true }]}>
<InputNumber min={0.01} precision={2} style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="dueDate" label="到期日期" rules={[{ required: true }]}>
<DatePicker style={{ width: '100%' }} placeholder="选择到期日期" format="YYYY-MM-DD" />
</Form.Item>
</Form>
</Modal>
</>
);
};