feat: 重构各业务模块管理页面与服务
This commit is contained in:
423
apps/admin/src/pages/Deposits/DepositModals.tsx
Normal file
423
apps/admin/src/pages/Deposits/DepositModals.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user