Files
gongxue-base/apps/admin/src/pages/Deposits/index.tsx
wangziqi 46a817503e feat(task1): restructure directories for turborepo monorepo
- Move backend/ to apps/server/ via git mv
- Move frontend/ to apps/admin/ via git mv
- Create packages/typescript-config/ with base, nestjs, and react-vite presets
2026-07-02 15:05:12 +08:00

192 lines
8.3 KiB
TypeScript

import React, { useEffect, useState, useMemo } from 'react';
import { Table, Button, Modal, Form, Select, DatePicker, InputNumber, Input, Space, message, Tag, Popconfirm } from 'antd';
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons';
import dayjs from 'dayjs';
import api from '../../api';
import PermissionButton from '../../components/PermissionButton';
const statusMap: Record<string, { text: string; color: string }> = {
paid: { text: '已缴', color: 'green' },
refunded: { text: '已全退', color: 'blue' },
partial_refund: { text: '部分退还', color: 'orange' },
deducted: { text: '已全扣', color: 'red' },
};
const DepositsPage: React.FC = () => {
const [data, setData] = useState<any[]>([]);
const [students, setStudents] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const [createModal, setCreateModal] = useState(false);
const [refundModal, setRefundModal] = useState<any>(null);
const [createForm] = Form.useForm();
const [refundForm] = Form.useForm();
const [searchText, setSearchText] = useState('');
const [filterStatus, setFilterStatus] = useState<string | undefined>(undefined);
const fetchData = async () => {
setLoading(true);
try {
const [d, s]: any[] = await Promise.all([
api.get('/deposits'),
api.get('/students'),
]);
setData(d);
setStudents(s);
} catch (e) { console.error(e); }
setLoading(false);
};
useEffect(() => { fetchData(); }, []);
const filteredData = useMemo(() => {
return data.filter((d: any) => {
if (searchText) {
const s = searchText.toLowerCase();
if (!d.student?.name?.toLowerCase().includes(s)) return false;
}
if (filterStatus && d.status !== filterStatus) return false;
return true;
});
}, [data, searchText, filterStatus]);
const handleCreate = async () => {
const values = await createForm.validateFields();
try {
await api.post('/deposits', {
studentId: values.studentId,
amount: values.amount,
paidDate: values.paidDate.format('YYYY-MM-DD'),
notes: values.notes,
});
message.success('押金记录已创建');
setCreateModal(false);
createForm.resetFields();
fetchData();
} catch (e: any) { message.error(e?.message || '操作失败'); }
};
const handleRefund = async () => {
const values = await refundForm.validateFields();
try {
await api.put(`/deposits/${refundModal.id}/refund`, {
refundDate: values.refundDate.format('YYYY-MM-DD'),
deductionAmount: values.deductionAmount || 0,
deductionReason: values.deductionReason,
notes: values.notes,
});
message.success('退还操作完成');
setRefundModal(null);
refundForm.resetFields();
fetchData();
} catch (e: any) { message.error(e?.message || '操作失败'); }
};
const columns = [
{ title: '学生', render: (_: any, r: any) => r.student?.name || '-' },
{ title: '押金金额', dataIndex: 'amount', render: (v: number) => `¥${Number(v).toFixed(2)}` },
{ title: '缴纳日期', dataIndex: 'paidDate' },
{
title: '状态', dataIndex: 'status',
render: (s: string) => <Tag color={statusMap[s]?.color}>{statusMap[s]?.text || s}</Tag>,
},
{ title: '退还金额', dataIndex: 'refundAmount', render: (v: any) => v != null ? `¥${Number(v).toFixed(2)}` : '-' },
{ title: '扣除金额', dataIndex: 'deductionAmount', render: (v: any) => v > 0 ? `¥${Number(v).toFixed(2)}` : '-' },
{ title: '扣除原因', dataIndex: 'deductionReason', render: (v: any) => v || '-' },
{ title: '退还日期', dataIndex: 'refundDate', render: (v: any) => v || '-' },
{ title: '备注', dataIndex: 'notes', render: (v: any) => v || '-' },
{
title: '操作', width: 160,
render: (_: any, record: any) => (
<Space>
{record.status === 'paid' && (
<PermissionButton permission="deposit:edit" size="small" type="primary" onClick={() => {
setRefundModal(record);
refundForm.setFieldsValue({ refundDate: dayjs(), deductionAmount: 0 });
}}>退</PermissionButton>
)}
<PermissionButton permission="deposit:delete">
<Popconfirm title="确定删除?" onConfirm={async () => {
try { await api.delete(`/deposits/${record.id}`); message.success('删除成功'); fetchData(); } catch (e: any) { message.error(e?.message || '删除失败'); }
}}>
<Button size="small" danger icon={<DeleteOutlined />} />
</Popconfirm>
</PermissionButton>
</Space>
),
},
];
return (
<div>
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', flexWrap: 'wrap', gap: 8 }}>
<Space wrap>
<Input.Search
placeholder="搜索学生姓名"
allowClear
style={{ width: 180 }}
onSearch={v => setSearchText(v)}
onChange={e => { if (!e.target.value) setSearchText(''); }}
/>
<Select
placeholder="状态筛选"
allowClear
style={{ width: 120 }}
value={filterStatus}
onChange={v => setFilterStatus(v)}
options={[
{ value: 'paid', label: '已缴' },
{ value: 'refunded', label: '已全退' },
{ value: 'partial_refund', label: '部分退还' },
{ value: 'deducted', label: '已全扣' },
]}
/>
</Space>
<PermissionButton permission="deposit:create" type="primary" icon={<PlusOutlined />} onClick={() => { createForm.resetFields(); createForm.setFieldsValue({ amount: 500, paidDate: dayjs() }); setCreateModal(true); }}>
</PermissionButton>
</div>
<Table columns={columns} dataSource={filteredData} rowKey="id" loading={loading} pagination={{ pageSize: 15, showTotal: (total) => `${total}` }} />
<Modal title="收取押金" open={createModal} onOk={handleCreate} onCancel={() => setCreateModal(false)} okText="确认">
<Form form={createForm} layout="vertical">
<Form.Item name="studentId" label="学生" rules={[{ required: true, message: '请选择学生' }]}>
<Select showSearch optionFilterProp="label" placeholder="搜索并选择学生"
options={students.filter((s: any) => s.status === 'active').map((s: any) => ({ value: s.id, label: `${s.name} (${s.idNumber || s.phone || ''})` }))} />
</Form.Item>
<Form.Item name="amount" label="押金金额(元)" rules={[{ required: true }]}>
<InputNumber min={0} 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={handleRefund} onCancel={() => setRefundModal(null)} okText="确认退还">
<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="deductionAmount" label="扣除金额(元)" extra="如无扣除填0">
<InputNumber min={0} max={Number(refundModal?.amount || 500)} precision={2} style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="deductionReason" label="扣除原因">
<Input placeholder="如:房间损坏赔偿" />
</Form.Item>
<Form.Item name="notes" label="备注">
<Input.TextArea rows={2} />
</Form.Item>
</Form>
</Modal>
</div>
);
};
export default DepositsPage;