订单支付记录

This commit is contained in:
liu
2026-08-14 01:58:57 +08:00
parent bf8ac68391
commit 889f987f17
15 changed files with 1044 additions and 16 deletions
+398
View File
@@ -0,0 +1,398 @@
import React, { useEffect, useRef, useState } from 'react';
import {
Button,
Descriptions,
Drawer,
Form,
Image,
Input,
message,
Modal,
Radio,
Space,
Table,
Tag,
Typography,
} from 'antd';
import type { TableProps } from 'antd';
import { UnorderedListOutlined } from '@ant-design/icons';
import XinTable from '@/components/XinTable';
import type {
XinTableColumn,
XinTableInstance,
XinTableProps,
} from '@/components/XinTable/typings.ts';
import type IPayment from '@/domain/iPayment.ts';
import type { IPaymentBill, IPaymentDetail } from '@/domain/iPayment.ts';
import { PAY_METHOD_MAP, PAYMENT_STATUS_MAP } from '@/domain/iPayment.ts';
import { BILL_STATUS_MAP } from '@/domain/iPurchaseOrder.ts';
import { getPaymentDetail, auditPayment } from '@/api/recon/payment.ts';
import { getStoreOptions } from '@/api/customer/store.ts';
import type IStore from '@/domain/iStore.ts';
import AuthButton from '@/components/AuthButton';
const { Title, Text } = Typography;
/** 审核表单 */
interface AuditFormValues {
result: 'pass' | 'reject';
audit_remark?: string;
}
/**
* 支付记录(小程序合并付款提交汇款凭证;审核通过后关联账单全部置已支付,拒绝则释放账单)
*/
const PaymentPage: React.FC = () => {
const tableRef = useRef<XinTableInstance<IPayment>>(null);
const [stores, setStores] = useState<IStore[]>([]);
const [detailOpen, setDetailOpen] = useState(false);
const [detail, setDetail] = useState<IPaymentDetail | null>(null);
const [detailLoading, setDetailLoading] = useState(false);
// 审核弹窗
const [auditTarget, setAuditTarget] = useState<IPayment | null>(null);
const [auditSaving, setAuditSaving] = useState(false);
const [auditForm] = Form.useForm<AuditFormValues>();
const watchAuditResult = Form.useWatch('result', auditForm);
useEffect(() => {
getStoreOptions().then((res) => setStores(res.data.data ?? []));
}, []);
const openDetail = async (id: number) => {
setDetailOpen(true);
setDetailLoading(true);
try {
const res = await getPaymentDetail(id);
setDetail(res.data.data ?? null);
} finally {
setDetailLoading(false);
}
};
/** 打开审核弹窗 */
const openAudit = (record: IPayment) => {
setAuditTarget(record);
auditForm.setFieldsValue({ result: 'pass', audit_remark: '' });
};
/** 提交审核:通过 → 账单批量置已支付;拒绝 → 释放账单 */
const handleAuditSave = async (values: AuditFormValues) => {
if (!auditTarget?.id) {
return;
}
setAuditSaving(true);
try {
const res = await auditPayment(auditTarget.id, values);
message.success(res.data.msg ?? '审核完成');
setAuditTarget(null);
await tableRef.current?.reload();
if (detail && detail.payment.id === auditTarget.id) {
await openDetail(auditTarget.id);
}
} finally {
setAuditSaving(false);
}
};
/** 合并账单列 */
const billColumns: TableProps<IPaymentBill>['columns'] = [
{
title: '账单号',
dataIndex: 'bill_no',
align: 'center',
render: (v) => <Text copyable={{ text: v }}>{v}</Text>,
},
{ title: '账单日期', dataIndex: 'bill_date', align: 'center' },
{
title: '商品金额',
dataIndex: 'product_amount',
align: 'center',
render: (v) => `¥${Number(v).toFixed(2)}`,
},
{
title: '配送费',
dataIndex: 'delivery_fee',
align: 'center',
render: (v) => `¥${Number(v).toFixed(2)}`,
},
{
title: '附加金额',
dataIndex: 'added_amount',
align: 'center',
render: (v) => `¥${Number(v).toFixed(2)}`,
},
{
title: '账单总金额',
dataIndex: 'total_amount',
align: 'center',
render: (v) => <Text strong>¥{Number(v).toFixed(2)}</Text>,
},
{
title: '支付状态',
dataIndex: 'status',
align: 'center',
render: (v) => {
const item = BILL_STATUS_MAP[Number(v ?? 0)];
return <Tag color={item?.color}>{item?.text}</Tag>;
},
},
];
const columns: XinTableColumn<IPayment>[] = [
{
title: '支付单号',
dataIndex: 'payment_no',
valueType: 'text',
hideInForm: true,
width: 210,
render: (_, record) => <Text copyable={{ text: record.payment_no }}>{record.payment_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 ?? `门店#${record.store_id}`,
},
{
title: '支付金额',
dataIndex: 'amount',
hideInForm: true,
hideInSearch: true,
align: 'center',
render: (_, record) => <Text strong type="danger">¥{record.amount}</Text>,
},
{
title: '支付方式',
dataIndex: 'pay_method',
valueType: 'select',
hideInForm: true,
align: 'center',
fieldProps: {
options: Object.entries(PAY_METHOD_MAP).map(([value, item]) => ({
value: Number(value),
label: item.text,
})),
},
render: (_, record) => {
const item = PAY_METHOD_MAP[record.pay_method ?? 0];
return <Tag color={item?.color}>{item?.text ?? '-'}</Tag>;
},
},
{
title: '合并账单',
dataIndex: 'bills_count',
hideInForm: true,
hideInSearch: true,
align: 'center',
render: (_, record) => `${record.bills_count ?? 0}`,
},
{
title: '状态',
dataIndex: 'status',
valueType: 'select',
hideInForm: true,
align: 'center',
fieldProps: {
options: Object.entries(PAYMENT_STATUS_MAP).map(([value, item]) => ({
value: Number(value),
label: item.text,
})),
},
render: (_, record) => {
const item = PAYMENT_STATUS_MAP[record.status ?? 0];
return <Tag color={item?.color}>{item?.text}</Tag>;
},
},
{
title: '提交人',
dataIndex: 'user',
hideInForm: true,
hideInSearch: true,
align: 'center',
render: (_, record) => record.user?.nickname ?? '-',
},
{
title: '提交时间',
dataIndex: 'created_at',
hideInForm: true,
hideInSearch: true,
align: 'center',
},
{
title: '审核人',
dataIndex: 'auditor',
hideInForm: true,
hideInSearch: true,
align: 'center',
render: (_, record) => record.auditor?.nickname ?? '-',
},
];
const operateRender: XinTableProps<IPayment>['operateRender'] = (record) => [
<Button
key="detail"
size="small"
type="primary"
icon={<UnorderedListOutlined />}
onClick={() => openDetail(record.id!)}
/>,
record.status === 0 ? (
<AuthButton key="audit" auth="recon.payment.audit">
<Button size="small" variant="solid" color="orange" onClick={() => openAudit(record)}>
</Button>
</AuthButton>
) : null,
];
const tableProps: XinTableProps<IPayment> = {
api: '/recon/payment',
columns,
rowKey: 'id',
accessName: 'recon.payment',
tableRef,
operateRender,
formProps: false,
actionBarRender: (dom) => [dom.search, dom.keywordSearch],
};
return (
<>
<div className="mb-5">
<Title level={3}></Title>
<Text type="secondary">
</Text>
</div>
<XinTable<IPayment> {...tableProps} />
{/* 支付详情:支付信息 + 凭证 + 合并账单 */}
<Drawer
title={detail ? `支付单 ${detail.payment.payment_no}` : '支付记录详情'}
open={detailOpen}
onClose={() => setDetailOpen(false)}
size={1000}
loading={detailLoading}
footer={
detail && detail.payment.status === 0 ? (
<Space className="flex justify-end">
<AuthButton auth="recon.payment.audit">
<Button type="primary" onClick={() => openAudit(detail.payment)}>
</Button>
</AuthButton>
</Space>
) : null
}
>
{detail ? (
<>
<Descriptions column={3} size="small" bordered>
<Descriptions.Item label="门店">{detail.payment.store?.name ?? `门店#${detail.payment.store_id}`}</Descriptions.Item>
<Descriptions.Item label="支付金额">
<Text strong type="danger">¥{detail.payment.amount}</Text>
</Descriptions.Item>
<Descriptions.Item label="支付方式">
<Tag color={PAY_METHOD_MAP[detail.payment.pay_method ?? 0]?.color}>
{PAY_METHOD_MAP[detail.payment.pay_method ?? 0]?.text ?? '-'}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="状态">
<Tag color={PAYMENT_STATUS_MAP[detail.payment.status ?? 0]?.color}>
{PAYMENT_STATUS_MAP[detail.payment.status ?? 0]?.text}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="提交人">{detail.payment.user?.nickname ?? '-'}</Descriptions.Item>
<Descriptions.Item label="提交时间">{detail.payment.created_at}</Descriptions.Item>
<Descriptions.Item label="审核人">{detail.payment.auditor?.nickname ?? '-'}</Descriptions.Item>
<Descriptions.Item label="审核时间">{detail.payment.audited_at ?? '-'}</Descriptions.Item>
<Descriptions.Item label="审核备注">{detail.payment.audit_remark || '-'}</Descriptions.Item>
{detail.payment.remark ? (
<Descriptions.Item label="门店备注" span={3}>{detail.payment.remark}</Descriptions.Item>
) : null}
</Descriptions>
<Title level={5} className="mt-6! mb-3!">
</Title>
{(detail.payment.voucher_urls ?? []).length > 0 ? (
<Image.PreviewGroup>
<Space wrap size={12}>
{(detail.payment.voucher_urls ?? []).map((url, index) => (
<Image
key={index}
src={url}
width={120}
height={120}
style={{ objectFit: 'cover', borderRadius: 4 }}
/>
))}
</Space>
</Image.PreviewGroup>
) : (
<Text type="secondary"></Text>
)}
<Title level={5} className="mt-6! mb-3!">
{detail.bills.length}
</Title>
<Table<IPaymentBill>
rowKey="id"
size="small"
bordered
columns={billColumns}
dataSource={detail.bills}
pagination={false}
/>
</>
) : null}
</Drawer>
{/* 审核弹窗 */}
<Modal
title={auditTarget ? `审核支付单 ${auditTarget.payment_no}` : '审核'}
open={auditTarget !== null}
onCancel={() => setAuditTarget(null)}
onOk={() => auditForm.submit()}
confirmLoading={auditSaving}
okText="提交审核"
destroyOnHidden
>
<div className="py-2 text-gray-500">
<Text strong type="danger">¥{auditTarget?.amount ?? '0.00'}</Text>{auditTarget?.bills_count ?? 0}
</div>
<Form form={auditForm} layout="vertical" onFinish={handleAuditSave}>
<Form.Item label="审核结果" name="result" rules={[{ required: true, message: '请选择审核结果' }]}>
<Radio.Group
options={[
{ value: 'pass', label: '通过(账单置为已支付)' },
{ value: 'reject', label: '拒绝(释放账单)' },
]}
/>
</Form.Item>
<Form.Item
label={watchAuditResult === 'reject' ? '拒绝原因' : '审核备注'}
name="audit_remark"
rules={[
{ required: watchAuditResult === 'reject', message: '拒绝时请填写原因' },
{ max: 255 },
]}
>
<Input.TextArea rows={2} maxLength={255} placeholder="审核备注(拒绝时必填)" />
</Form.Item>
</Form>
</Modal>
</>
);
};
export default PaymentPage;