first version

This commit is contained in:
liu
2026-07-23 20:41:25 +08:00
parent 00a0938a1b
commit 10cff754a4
211 changed files with 11577 additions and 842 deletions
+236
View File
@@ -0,0 +1,236 @@
import React, { useEffect, useRef, useState } from 'react';
import {
Button,
Descriptions,
Drawer,
Dropdown,
Tag,
Typography,
} from 'antd';
import { DownloadOutlined } from '@ant-design/icons';
import XinTable from '@/components/XinTable';
import type {
XinTableColumn,
XinTableInstance,
XinTableProps,
} from '@/components/XinTable/typings.ts';
import type ISettlement from '@/domain/iSettlement.ts';
import { SETTLEMENT_STATUS_MAP } from '@/domain/iSettlement.ts';
import { getStoreOptions } from '@/api/customer/store.ts';
import type IStore from '@/domain/iStore.ts';
import { downloadSettlement } from '@/api/recon/settlement.ts';
import { Get } from '@/api/common/table.ts';
import AuthButton from '@/components/AuthButton';
const { Title, Text } = Typography;
/**
* 结算表(D9 生成于对账结算,D10 导出存档)
*/
const SettlementPage: React.FC = () => {
const tableRef = useRef<XinTableInstance<ISettlement>>(null);
const [stores, setStores] = useState<IStore[]>([]);
const [detailOpen, setDetailOpen] = useState(false);
const [detail, setDetail] = useState<ISettlement | null>(null);
const [detailLoading, setDetailLoading] = useState(false);
useEffect(() => {
getStoreOptions().then((res) => setStores(res.data.data ?? []));
}, []);
const openDetail = async (id: number) => {
setDetailOpen(true);
setDetailLoading(true);
try {
const res = await Get<ISettlement>('/recon/settlement', id);
setDetail(res.data.data ?? null);
} finally {
setDetailLoading(false);
}
};
const columns: XinTableColumn<ISettlement>[] = [
{
title: '结算单号',
dataIndex: 'settlement_no',
valueType: 'text',
hideInForm: true,
render: (_, record) => <Text copyable={{ text: record.settlement_no }}>{record.settlement_no}</Text>,
},
{
title: '门店',
dataIndex: 'store_id',
valueType: 'select',
hideInForm: true,
fieldProps: {
options: stores.map((s) => ({ label: s.name, value: s.id })),
showSearch: true,
optionFilterProp: 'label',
},
render: (_, record) => record.store?.name ?? '-',
},
{
title: '来源对账单',
dataIndex: 'recon',
hideInForm: true,
hideInSearch: true,
render: (_, record) =>
record.recon ? (
<span>
{record.recon.recon_no}
<Text type="secondary">{record.recon.title}</Text>
</span>
) : (
'-'
),
},
{
title: '结算周期',
dataIndex: 'period',
hideInForm: true,
hideInSearch: true,
render: (_, record) => `${record.period_start} ~ ${record.period_end}`,
},
{
title: '公布金额',
dataIndex: 'total_amount',
hideInForm: true,
hideInSearch: true,
align: 'right',
render: (_, record) => `¥${record.total_amount}`,
},
{
title: '实际金额',
dataIndex: 'actual_amount',
hideInForm: true,
hideInSearch: true,
align: 'right',
render: (_, record) => <Text strong>¥{record.actual_amount}</Text>,
},
{
title: '差额',
dataIndex: 'diff_amount',
hideInForm: true,
hideInSearch: true,
align: 'right',
render: (_, record) => {
const num = Number(record.diff_amount ?? 0);
return (
<Text type={num === 0 ? 'secondary' : 'danger'} strong={num !== 0}>
¥{record.diff_amount}
</Text>
);
},
},
{
title: '状态',
dataIndex: 'status',
valueType: 'select',
hideInForm: true,
fieldProps: {
options: Object.entries(SETTLEMENT_STATUS_MAP).map(([value, item]) => ({
value: Number(value),
label: item.text,
})),
},
render: (_, record) => {
const item = SETTLEMENT_STATUS_MAP[record.status ?? 0];
return <Tag color={item?.color}>{item?.text}</Tag>;
},
align: 'center',
},
{
title: '结算时间',
dataIndex: 'settled_at',
hideInForm: true,
hideInSearch: true,
align: 'center',
render: (_, record) => record.settled_at ?? '-',
},
];
const operateRender: XinTableProps<ISettlement>['operateRender'] = (record) => [
<Button key="detail" size="small" onClick={() => openDetail(record.id!)}>
</Button>,
<AuthButton key="download" auth="recon.settlement.download">
<Dropdown
menu={{
items: [
{ key: 'xlsx', label: '下载 Excel', onClick: () => downloadSettlement(record.id!, 'xlsx') },
{ key: 'pdf', label: '下载 PDF', onClick: () => downloadSettlement(record.id!, 'pdf') },
],
}}
>
<Button size="small" type="primary" ghost icon={<DownloadOutlined />}>
</Button>
</Dropdown>
</AuthButton>,
];
const tableProps: XinTableProps<ISettlement> = {
api: '/recon/settlement',
columns,
rowKey: 'id',
accessName: 'recon.settlement',
tableRef,
operateRender,
formProps: false,
scroll: { x: 1200 },
};
return (
<>
<div className="mb-5">
<Title level={3}></Title>
<Text type="secondary">
Excel / PDF
</Text>
</div>
<XinTable<ISettlement> {...tableProps} />
<Drawer
title={detail ? `结算表 ${detail.settlement_no}` : '结算表详情'}
open={detailOpen}
onClose={() => setDetailOpen(false)}
width={640}
loading={detailLoading}
>
{detail ? (
<Descriptions column={2} size="small" bordered>
<Descriptions.Item label="门店">{detail.store?.name}</Descriptions.Item>
<Descriptions.Item label="状态">
<Tag color={SETTLEMENT_STATUS_MAP[detail.status ?? 0]?.color}>
{SETTLEMENT_STATUS_MAP[detail.status ?? 0]?.text}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="来源对账单">
{detail.recon?.recon_no ?? '-'}
</Descriptions.Item>
<Descriptions.Item label="结算周期">
{detail.period_start} ~ {detail.period_end}
</Descriptions.Item>
<Descriptions.Item label="公布金额">¥{detail.total_amount}</Descriptions.Item>
<Descriptions.Item label="实际金额">¥{detail.actual_amount}</Descriptions.Item>
<Descriptions.Item label="差额">¥{detail.diff_amount}</Descriptions.Item>
<Descriptions.Item label="结算时间">
{detail.settled_at ?? '-'}
</Descriptions.Item>
<Descriptions.Item label="存档文件" span={2}>
{detail.file_path ?? <Text type="secondary"></Text>}
</Descriptions.Item>
{detail.remark ? (
<Descriptions.Item label="备注" span={2}>
{detail.remark}
</Descriptions.Item>
) : null}
</Descriptions>
) : null}
</Drawer>
</>
);
};
export default SettlementPage;