订单支付记录
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
import createAxios from '@/utils/request';
|
||||
import type { IPaymentDetail } from '@/domain/iPayment.ts';
|
||||
|
||||
/** 支付记录详情(支付信息 + 凭证图片 + 合并账单) */
|
||||
export async function getPaymentDetail(id: number) {
|
||||
return createAxios<IPaymentDetail>({
|
||||
url: `/recon/payment/${id}`,
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
/** 审核支付记录:pass 通过(账单批量置已支付)/ reject 拒绝(释放账单,需填原因) */
|
||||
export async function auditPayment(id: number, data: { result: 'pass' | 'reject'; audit_remark?: string }) {
|
||||
return createAxios({
|
||||
url: `/recon/payment/${id}/audit`,
|
||||
method: 'put',
|
||||
data,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/** 支付记录(小程序合并付款提交汇款凭证,后台审核) */
|
||||
export default interface IPayment {
|
||||
id?: number;
|
||||
payment_no?: string;
|
||||
store_id?: number;
|
||||
user_id?: number;
|
||||
/** 支付金额(= 关联账单总金额合计) */
|
||||
amount?: string;
|
||||
/** 支付方式:1微信 2支付宝 3对公汇款 */
|
||||
pay_method?: number;
|
||||
/** 汇款凭证图片ID列表 */
|
||||
voucher_ids?: number[];
|
||||
/** 凭证图片URL列表(详情接口解析) */
|
||||
voucher_urls?: string[];
|
||||
/** 状态:0待审核 1已通过 2已拒绝 */
|
||||
status?: number;
|
||||
/** 门店备注 */
|
||||
remark?: string;
|
||||
audited_at?: string | null;
|
||||
auditor_id?: number;
|
||||
audit_remark?: string;
|
||||
created_at?: string;
|
||||
/** 列表/详情接口附带 */
|
||||
store?: { id: number; name: string; contact?: string; phone?: string } | null;
|
||||
user?: { id: number; nickname: string } | null;
|
||||
auditor?: { id: number; nickname: string } | null;
|
||||
bills_count?: number;
|
||||
}
|
||||
|
||||
/** 支付记录关联账单(合并付款) */
|
||||
export interface IPaymentBill {
|
||||
id: number;
|
||||
bill_no: string;
|
||||
bill_date: string;
|
||||
product_amount: string;
|
||||
delivery_fee: string;
|
||||
added_amount: string;
|
||||
total_amount: string;
|
||||
/** 0未支付 1已支付 */
|
||||
status: number;
|
||||
}
|
||||
|
||||
/** 支付记录详情 */
|
||||
export interface IPaymentDetail {
|
||||
payment: IPayment;
|
||||
bills: IPaymentBill[];
|
||||
}
|
||||
|
||||
/** 支付方式映射 */
|
||||
export const PAY_METHOD_MAP: Record<number, { text: string; color: string }> = {
|
||||
1: { text: '微信支付', color: 'green' },
|
||||
2: { text: '支付宝', color: 'blue' },
|
||||
3: { text: '对公汇款', color: 'purple' },
|
||||
};
|
||||
|
||||
/** 支付记录状态映射 */
|
||||
export const PAYMENT_STATUS_MAP: Record<number, { text: string; color: string }> = {
|
||||
0: { text: '待审核', color: 'warning' },
|
||||
1: { text: '已通过', color: 'success' },
|
||||
2: { text: '已拒绝', color: 'error' },
|
||||
};
|
||||
@@ -76,6 +76,8 @@ export interface IBill {
|
||||
total_amount: string;
|
||||
/** 支付状态:0未支付 1已支付 */
|
||||
status?: number;
|
||||
/** 关联支付记录ID(0=未发起支付) */
|
||||
payment_id?: number;
|
||||
/** 付款时间(线下收款手动登记) */
|
||||
paid_at?: string | null;
|
||||
/** 付款备注(线下收款信息) */
|
||||
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user