回筐、账单完成

This commit is contained in:
liu
2026-08-14 01:19:47 +08:00
parent e35c951a59
commit bf8ac68391
39 changed files with 1392 additions and 963 deletions
+19
View File
@@ -0,0 +1,19 @@
import createAxios from '@/utils/request';
import type { IBillDetail } from '@/domain/iBill.ts';
/** 账单详情(账单信息 + 合并商品明细 + 关联订单) */
export async function getBillDetail(id: number) {
return createAxios<IBillDetail>({
url: `/recon/bill/${id}`,
method: 'get',
});
}
/** 确认收款:线下收款后手动登记付款信息,支付状态置为已支付 */
export async function payBill(id: number, data: { paid_at: string; pay_remark?: string }) {
return createAxios({
url: `/recon/bill/${id}/pay`,
method: 'put',
data,
});
}
+41
View File
@@ -0,0 +1,41 @@
import type { IBill } from '@/domain/iPurchaseOrder.ts';
/** 门店账单(采购单完成后按门店生成),类型定义在 iPurchaseOrder.ts */
export type { IBill };
export { BILL_STATUS_MAP } from '@/domain/iPurchaseOrder.ts';
/** 账单合并商品明细行(按商品聚合账单关联的全部订单明细) */
export interface IBillGoodsItem {
product_id: number;
product_name: string;
/** 规格/包规 */
product_spec: string;
unit: string;
/** 单价(加权平均:Σ金额÷Σ数量) */
price: string;
/** 数量合计 */
quantity: number;
/** 重量合计 */
weight: string;
/** 金额合计 = Σ 明细金额 */
amount: string;
}
/** 账单关联的门店订单 */
export interface IBillOrder {
id: number;
order_no: string;
order_date: string;
total_quantity: number;
total_weight: string;
total_amount: string;
/** 0待接单 1已接单 2采购中 3配送中 4已完成 9已取消 */
status: number;
}
/** 账单详情(账单 + 合并商品明细 + 关联订单) */
export interface IBillDetail {
bill: IBill;
items: IBillGoodsItem[];
orders: IBillOrder[];
}
+26
View File
@@ -0,0 +1,26 @@
/** 门店回筐记录(压筐=生成账单自动写入只读;回筐=门店退回手动登记) */
export default interface IContainerReturn {
id?: number;
store_id?: number;
/** 关联账单ID(0=手动回筐登记) */
bill_id?: number;
/** 类型:1压筐 2回筐 */
type?: number;
box_num?: number;
tray_num?: number;
/** 记录日期(压筐=账单日期,回筐=退回日期) */
return_date?: string;
operator_id?: number;
remark?: string;
created_at?: string;
/** 列表接口附带 */
store?: { id: number; name: string; pending_box_num?: number; pending_tray_num?: number } | null;
bill?: { id: number; bill_no: string } | null;
operator?: { id: number; nickname: string } | null;
}
/** 回筐记录类型映射 */
export const CONTAINER_TYPE_MAP: Record<number, { text: string; color: string }> = {
1: { text: '压筐', color: 'processing' },
2: { text: '回筐', color: 'success' },
};
+19
View File
@@ -74,11 +74,30 @@ export interface IBill {
added_amount: string;
/** 账单总金额 = 商品金额 + 配送费 + 附加金额 */
total_amount: string;
/** 支付状态:0未支付 1已支付 */
status?: number;
/** 付款时间(线下收款手动登记) */
paid_at?: string | null;
/** 付款备注(线下收款信息) */
pay_remark?: string;
paid_operator_id?: number;
order_count?: number;
remark?: string;
created_at?: string;
/** 门店账单列表/详情接口附带 */
store?: { id: number; name: string; address?: string; contact?: string; phone?: string } | null;
purchase?: { id: number; purchase_no: string; purchase_date: string; status?: number } | null;
operator?: { id: number; nickname: string } | null;
paid_operator?: { id: number; nickname: string } | null;
orders_count?: number;
}
/** 账单支付状态映射 */
export const BILL_STATUS_MAP: Record<number, { text: string; color: string }> = {
0: { text: '未支付', color: 'warning' },
1: { text: '已支付', color: 'success' },
};
/** 账单生成预览行(按门店汇总,金额只读) */
export interface IBillPrepareStore {
store_id: number;
-43
View File
@@ -1,43 +0,0 @@
/** 门店对账单明细 */
export interface IStatementItem {
id?: number;
statement_id?: number;
order_id?: number;
order_item_id?: number;
product_id?: number;
product_name?: string;
price?: string;
quantity?: string;
weight?: string;
amount?: string;
is_reconciled?: number;
store_remark?: string;
}
/** 门店对账单 */
export default interface IStatement {
id?: number;
statement_no?: string;
store_id?: number;
store?: { id: number; name: string };
period_start?: string;
period_end?: string;
total_amount?: string;
/** 回款周期快照(天) */
payment_cycle_days?: number;
/** 应结算日期 = period_end + 回款周期 */
settlement_date?: string;
/** 0待对账 1已对账 2已结算 */
status?: number;
reconciled_at?: string;
settled_at?: string;
remark?: string;
items?: IStatementItem[];
created_at?: string;
}
export const STATEMENT_STATUS_MAP: Record<number, { text: string; color: string }> = {
0: { text: '待对账', color: 'default' },
1: { text: '已对账', color: 'processing' },
2: { text: '已结算', color: 'success' },
};
+4
View File
@@ -14,6 +14,10 @@ export default interface IStore {
address?: string;
/** 回款周期(天) */
payment_cycle_days?: number;
/** 待回筐数量(生成账单压筐累加,回筐登记扣减) */
pending_box_num?: number;
/** 待回托盘数量 */
pending_tray_num?: number;
status?: number;
remark?: string;
created_at?: string;
-1
View File
@@ -44,7 +44,6 @@ export default interface IStoreOrder {
order_no?: string;
store_id?: number;
purchase_id?: number;
statement_id?: number;
/** 关联账单ID(采购单完成后按门店生成账单时回写) */
bill_id?: number;
store?: {
+24
View File
@@ -86,6 +86,30 @@ const StorePage: React.FC = () => {
fieldProps: { min: 0, precision: 0 },
align: 'center',
},
{
title: '待回筐数量',
dataIndex: 'pending_box_num',
hideInForm: true,
hideInSearch: true,
align: 'center',
render: (_, record) => (
<Text strong type={Number(record.pending_box_num) > 0 ? 'warning' : undefined}>
{record.pending_box_num ?? 0}
</Text>
),
},
{
title: '待回托盘数量',
dataIndex: 'pending_tray_num',
hideInForm: true,
hideInSearch: true,
align: 'center',
render: (_, record) => (
<Text strong type={Number(record.pending_tray_num) > 0 ? 'warning' : undefined}>
{record.pending_tray_num ?? 0}
</Text>
),
},
{
title: '状态',
dataIndex: 'status',
+14 -1
View File
@@ -39,7 +39,7 @@ import type {
IPurchaseStoreItem,
IPurchaseStoreSummary,
} from '@/domain/iPurchaseOrder.ts';
import { PURCHASE_STATUS_MAP } from '@/domain/iPurchaseOrder.ts';
import { PURCHASE_STATUS_MAP, BILL_STATUS_MAP } from '@/domain/iPurchaseOrder.ts';
import { STORE_ORDER_STATUS_MAP } from '@/domain/iStoreOrder.ts';
import {
exportPurchase,
@@ -544,6 +544,16 @@ const PurchaseOrderPage: React.FC = () => {
align: 'center',
render: (v) => <Text strong type="danger">¥{Number(v).toFixed(2)}</Text>,
},
{
title: '支付状态',
dataIndex: 'status',
width: 100,
align: 'center',
render: (v) => {
const item = BILL_STATUS_MAP[Number(v ?? 0)];
return <Tag color={item?.color}>{item?.text}</Tag>;
},
},
];
/** 门店账单合计行 */
@@ -573,6 +583,9 @@ const PurchaseOrderPage: React.FC = () => {
<Table.Summary.Cell index={8} align="center">
<Text strong type="danger">¥{totalAmount.toFixed(2)}</Text>
</Table.Summary.Cell>
<Table.Summary.Cell index={9} align="center">
<Text strong>-</Text>
</Table.Summary.Cell>
</Table.Summary.Row>
);
};
+422
View File
@@ -0,0 +1,422 @@
import React, { useEffect, useRef, useState } from 'react';
import {
Button,
DatePicker,
Descriptions,
Drawer,
Form,
Input,
message,
Modal,
Table,
Tag,
Typography,
} from 'antd';
import type { TableProps } from 'antd';
import { UnorderedListOutlined } from '@ant-design/icons';
import dayjs from 'dayjs';
import XinTable from '@/components/XinTable';
import type {
XinTableColumn,
XinTableInstance,
XinTableProps,
} from '@/components/XinTable/typings.ts';
import type { IBill, IBillDetail, IBillGoodsItem, IBillOrder } from '@/domain/iBill.ts';
import { BILL_STATUS_MAP } from '@/domain/iBill.ts';
import { STORE_ORDER_STATUS_MAP } from '@/domain/iStoreOrder.ts';
import { getBillDetail, payBill } from '@/api/recon/bill.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 PayFormValues {
paid_at: dayjs.Dayjs;
pay_remark?: string;
}
/**
* 门店账单(采购单完成后按门店生成;详情含合并后的商品明细与关联订单;线下收款手动登记)
*/
const BillPage: React.FC = () => {
const tableRef = useRef<XinTableInstance<IBill>>(null);
const [stores, setStores] = useState<IStore[]>([]);
const [detailOpen, setDetailOpen] = useState(false);
const [detail, setDetail] = useState<IBillDetail | null>(null);
const [detailLoading, setDetailLoading] = useState(false);
// 确认收款(线下收款手动登记)
const [payTarget, setPayTarget] = useState<IBill | null>(null);
const [paySaving, setPaySaving] = useState(false);
const [payForm] = Form.useForm<PayFormValues>();
useEffect(() => {
getStoreOptions().then((res) => setStores(res.data.data ?? []));
}, []);
const openDetail = async (id: number) => {
setDetailOpen(true);
setDetailLoading(true);
try {
const res = await getBillDetail(id);
setDetail(res.data.data ?? null);
} finally {
setDetailLoading(false);
}
};
/** 打开确认收款弹窗(默认付款时间为当前) */
const openPay = (record: IBill) => {
setPayTarget(record);
payForm.setFieldsValue({ paid_at: dayjs(), pay_remark: '' });
};
/** 提交确认收款:登记付款信息并置为已支付 */
const handlePaySave = async (values: PayFormValues) => {
if (!payTarget?.id) {
return;
}
setPaySaving(true);
try {
await payBill(payTarget.id, {
paid_at: values.paid_at.format('YYYY-MM-DD HH:mm:ss'),
pay_remark: values.pay_remark ?? '',
});
message.success('收款已登记,账单已置为已支付');
setPayTarget(null);
await tableRef.current?.reload();
} finally {
setPaySaving(false);
}
};
/** 合并商品明细列:品名/包规/单位/单价(加权平均)/数量/重量/金额 */
const itemColumns: TableProps<IBillGoodsItem>['columns'] = [
{ title: '品名', dataIndex: 'product_name', width: 160, align: 'center' },
{ title: '包规', dataIndex: 'product_spec', width: 100, align: 'center', render: (v) => v || '-' },
{ title: '单位', dataIndex: 'unit', width: 80, align: 'center', render: (v) => v || '-' },
{
title: '单价',
dataIndex: 'price',
width: 100,
align: 'center',
render: (v) => `¥${Number(v).toFixed(2)}`,
},
{
title: '数量',
dataIndex: 'quantity',
width: 90,
align: 'center',
render: (v) => <Text strong>{v}</Text>,
},
{ title: '重量', dataIndex: 'weight', width: 100, align: 'center', render: (v) => `${v}` },
{
title: '金额',
dataIndex: 'amount',
width: 110,
align: 'center',
render: (v) => <Text strong>¥{Number(v).toFixed(2)}</Text>,
},
];
/** 合并商品明细合计行 */
const renderItemSummary = () => {
const items = detail?.items ?? [];
const totalQuantity = items.reduce((sum, row) => sum + Number(row.quantity), 0);
const totalWeight = items.reduce((sum, row) => sum + Number(row.weight), 0);
const totalAmount = items.reduce((sum, row) => sum + Number(row.amount), 0);
return (
<Table.Summary.Row>
<Table.Summary.Cell index={0} colSpan={4} align="center">
<Text strong></Text>
</Table.Summary.Cell>
<Table.Summary.Cell index={4} align="center">
<Text strong>{totalQuantity}</Text>
</Table.Summary.Cell>
<Table.Summary.Cell index={5} align="center">
<Text strong>{totalWeight.toFixed(3)}</Text>
</Table.Summary.Cell>
<Table.Summary.Cell index={6} align="center">
<Text strong type="danger">¥{totalAmount.toFixed(2)}</Text>
</Table.Summary.Cell>
</Table.Summary.Row>
);
};
/** 关联订单列 */
const orderColumns: TableProps<IBillOrder>['columns'] = [
{
title: '订单号',
dataIndex: 'order_no',
align: 'center',
render: (v) => <Text copyable={{ text: v }}>{v}</Text>,
},
{ title: '订货日期', dataIndex: 'order_date', align: 'center' },
{ title: '订货数量', dataIndex: 'total_quantity', align: 'center' },
{ title: '总重量', dataIndex: 'total_weight', align: 'center', render: (v) => `${v}` },
{
title: '订单金额',
dataIndex: 'total_amount',
align: 'center',
render: (v) => <Text strong>¥{v}</Text>,
},
{
title: '状态',
dataIndex: 'status',
align: 'center',
render: (v) => {
const item = STORE_ORDER_STATUS_MAP[Number(v ?? 0)];
return <Tag color={item?.color}>{item?.text}</Tag>;
},
},
];
const columns: XinTableColumn<IBill>[] = [
{
title: '账单号',
dataIndex: 'bill_no',
valueType: 'text',
hideInForm: true,
width: 210,
render: (_, record) => <Text copyable={{ text: record.bill_no }}>{record.bill_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: 'purchase_no',
valueType: 'text',
hideInForm: true,
render: (_, record) => record.purchase?.purchase_no ?? '-',
},
{
title: '账单日期',
dataIndex: 'bill_date',
valueType: 'dateRange',
hideInForm: true,
align: 'center',
render: (_, record) => record.bill_date,
},
{
title: '商品金额',
dataIndex: 'product_amount',
hideInForm: true,
hideInSearch: true,
align: 'center',
render: (_, record) => `¥${record.product_amount}`,
},
{
title: '配送费',
dataIndex: 'delivery_fee',
hideInForm: true,
hideInSearch: true,
align: 'center',
render: (_, record) => `¥${record.delivery_fee}`,
},
{
title: '附加金额',
dataIndex: 'added_amount',
hideInForm: true,
hideInSearch: true,
align: 'center',
render: (_, record) => (
<span title={`周转筐 ${record.box_num} 个 / 周转托盘 ${record.tray_num}`}>
¥{record.added_amount}
</span>
),
},
{
title: '账单总金额',
dataIndex: 'total_amount',
hideInForm: true,
hideInSearch: true,
align: 'center',
render: (_, record) => <Text strong type="danger">¥{record.total_amount}</Text>,
},
{
title: '支付状态',
dataIndex: 'status',
valueType: 'select',
hideInForm: true,
align: 'center',
fieldProps: {
options: Object.entries(BILL_STATUS_MAP).map(([value, item]) => ({
value: Number(value),
label: item.text,
})),
},
render: (_, record) => {
const item = BILL_STATUS_MAP[record.status ?? 0];
return <Tag color={item?.color}>{item?.text}</Tag>;
},
},
{
title: '关联订单',
dataIndex: 'orders_count',
hideInForm: true,
hideInSearch: true,
align: 'center',
render: (_, record) => `${record.orders_count ?? 0}`,
},
{
title: '生成时间',
dataIndex: 'created_at',
hideInForm: true,
hideInSearch: true,
align: 'center',
},
];
const operateRender: XinTableProps<IBill>['operateRender'] = (record) => [
<Button
key="detail"
size="small"
type="primary"
icon={<UnorderedListOutlined />}
onClick={() => openDetail(record.id!)}
/>,
record.status === 0 ? (
<AuthButton key="pay" auth="recon.bill.pay">
<Button size="small" variant="solid" color="green" onClick={() => openPay(record)}>
</Button>
</AuthButton>
) : null,
];
const tableProps: XinTableProps<IBill> = {
api: '/recon/bill',
columns,
rowKey: 'id',
accessName: 'recon.bill',
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<IBill> {...tableProps} />
<Drawer
title={detail ? `账单 ${detail.bill.bill_no}` : '账单详情'}
open={detailOpen}
onClose={() => setDetailOpen(false)}
size={1000}
loading={detailLoading}
>
{detail ? (
<>
<Descriptions column={3} size="small" bordered>
<Descriptions.Item label="门店">{detail.bill.store?.name ?? `门店#${detail.bill.store_id}`}</Descriptions.Item>
<Descriptions.Item label="采购单号">{detail.bill.purchase?.purchase_no ?? '-'}</Descriptions.Item>
<Descriptions.Item label="账单日期">{detail.bill.bill_date}</Descriptions.Item>
<Descriptions.Item label="商品金额">¥{detail.bill.product_amount}</Descriptions.Item>
<Descriptions.Item label="配送费">¥{detail.bill.delivery_fee}</Descriptions.Item>
<Descriptions.Item label="附加金额">
¥{detail.bill.added_amount}
<Text type="secondary" className="ml-2!">
{detail.bill.box_num}×¥{detail.bill.box_price} {detail.bill.tray_num}×¥{detail.bill.tray_price}
</Text>
</Descriptions.Item>
<Descriptions.Item label="账单总金额">
<Text strong type="danger">¥{detail.bill.total_amount}</Text>
</Descriptions.Item>
<Descriptions.Item label="支付状态">
<Tag color={BILL_STATUS_MAP[detail.bill.status ?? 0]?.color}>
{BILL_STATUS_MAP[detail.bill.status ?? 0]?.text}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="付款时间">{detail.bill.paid_at ?? '-'}</Descriptions.Item>
<Descriptions.Item label="收款人">{detail.bill.paid_operator?.nickname ?? '-'}</Descriptions.Item>
<Descriptions.Item label="生成人">{detail.bill.operator?.nickname ?? '-'}</Descriptions.Item>
<Descriptions.Item label="生成时间">{detail.bill.created_at}</Descriptions.Item>
{detail.bill.pay_remark ? (
<Descriptions.Item label="付款备注" span={3}>{detail.bill.pay_remark}</Descriptions.Item>
) : null}
{detail.bill.remark ? (
<Descriptions.Item label="备注" span={3}>{detail.bill.remark}</Descriptions.Item>
) : null}
</Descriptions>
<Title level={5} className="mt-6! mb-3!">
</Title>
<Table<IBillGoodsItem>
rowKey="product_id"
size="small"
bordered
columns={itemColumns}
dataSource={detail.items}
pagination={false}
summary={renderItemSummary}
/>
<Title level={5} className="mt-6! mb-3!">
{detail.orders.length}
</Title>
<Table<IBillOrder>
rowKey="id"
size="small"
bordered
columns={orderColumns}
dataSource={detail.orders}
pagination={false}
/>
</>
) : null}
</Drawer>
{/* 确认收款:线下收款后手动登记付款信息 */}
<Modal
title={payTarget ? `确认收款 · ${payTarget.bill_no}` : '确认收款'}
open={payTarget !== null}
onCancel={() => setPayTarget(null)}
onOk={() => payForm.submit()}
confirmLoading={paySaving}
okText="确认收款"
destroyOnHidden
>
<div className="py-2 text-gray-500">
<Text strong type="danger">¥{payTarget?.total_amount ?? '0.00'}</Text>
¥{payTarget?.product_amount ?? '0.00'} + ¥{payTarget?.delivery_fee ?? '0.00'} + ¥{payTarget?.added_amount ?? '0.00'}
线
</div>
<Form form={payForm} layout="vertical" onFinish={handlePaySave}>
<Form.Item
label="付款时间"
name="paid_at"
rules={[{ required: true, message: '请选择付款时间' }]}
>
<DatePicker className="w-full" showTime allowClear={false} />
</Form.Item>
<Form.Item label="付款备注" name="pay_remark" rules={[{ max: 255 }]}>
<Input.TextArea rows={2} maxLength={255} placeholder="如:现金/转账单号等线下收款信息(选填)" />
</Form.Item>
</Form>
</Modal>
</>
);
};
export default BillPage;
+177
View File
@@ -0,0 +1,177 @@
import React, { useEffect, useState } from 'react';
import { message, Tag, Typography } from 'antd';
import dayjs from 'dayjs';
import XinTable from '@/components/XinTable';
import type { XinTableColumn, XinTableProps } from '@/components/XinTable/typings.ts';
import type IContainerReturn from '@/domain/iContainerReturn.ts';
import { CONTAINER_TYPE_MAP } from '@/domain/iContainerReturn.ts';
import { getStoreOptions } from '@/api/customer/store.ts';
import { Create } from '@/api/common/table.ts';
import type IStore from '@/domain/iStore.ts';
const { Title, Text } = Typography;
/**
* 回筐记录(压筐=生成账单时自动写入,只读;回筐=门店退回手动登记,删除后恢复门店待回数量)
*/
const ContainerReturnPage: React.FC = () => {
const [stores, setStores] = useState<IStore[]>([]);
useEffect(() => {
getStoreOptions().then((res) => setStores(res.data.data ?? []));
}, []);
const columns: XinTableColumn<IContainerReturn>[] = [
{
title: '门店',
dataIndex: 'store_id',
valueType: 'select',
rules: [{ required: true, message: '请选择门店' }],
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: 'type',
valueType: 'select',
hideInForm: true,
align: 'center',
fieldProps: {
options: Object.entries(CONTAINER_TYPE_MAP).map(([value, item]) => ({
value: Number(value),
label: item.text,
})),
},
render: (_, record) => {
const item = CONTAINER_TYPE_MAP[record.type ?? 0];
return <Tag color={item?.color}>{item?.text ?? '-'}</Tag>;
},
},
{
title: '关联账单',
dataIndex: 'bill_id',
hideInForm: true,
hideInSearch: true,
align: 'center',
render: (_, record) =>
record.bill ? <Text copyable={{ text: record.bill.bill_no }}>{record.bill.bill_no}</Text> : '-',
},
{
title: '周转筐数量',
dataIndex: 'box_num',
valueType: 'digit',
hideInSearch: true,
initialValue: 0,
rules: [{ required: true, message: '请输入周转筐数量' }],
fieldProps: { min: 0, precision: 0 },
align: 'center',
render: (_, record) => <Text strong>{record.box_num}</Text>,
},
{
title: '周转托盘数量',
dataIndex: 'tray_num',
valueType: 'digit',
hideInSearch: true,
initialValue: 0,
rules: [{ required: true, message: '请输入周转托盘数量' }],
fieldProps: { min: 0, precision: 0 },
align: 'center',
render: (_, record) => <Text strong>{record.tray_num}</Text>,
},
{
title: '当前待回(筐/托盘)',
key: 'pending',
hideInForm: true,
hideInSearch: true,
align: 'center',
render: (_, record) =>
record.store
? `${record.store.pending_box_num ?? 0} / ${record.store.pending_tray_num ?? 0}`
: '-',
},
{
title: '退回日期',
dataIndex: 'return_date',
valueType: 'dateRange',
hideInTable: true,
hideInForm: true,
align: 'center',
},
{
title: '退回日期',
dataIndex: 'return_date',
valueType: 'date',
hideInSearch: true,
initialValue: dayjs(),
rules: [{ required: true, message: '请选择退回日期' }],
align: 'center',
},
{
title: '操作人',
dataIndex: 'operator',
hideInForm: true,
hideInSearch: true,
align: 'center',
render: (_, record) => record.operator?.nickname ?? '-',
},
{
title: '登记时间',
dataIndex: 'created_at',
hideInForm: true,
hideInSearch: true,
align: 'center',
},
{
title: '备注',
dataIndex: 'remark',
valueType: 'textarea',
hideInSearch: true,
fieldProps: { rows: 2 },
render: (v) => v || '-',
},
];
const tableProps: XinTableProps<IContainerReturn> = {
api: '/recon/container-return',
columns,
rowKey: 'id',
accessName: 'recon.containerReturn',
// 压筐记录由账单生成,仅回筐记录可删除;均不允许编辑
editShow: false,
deleteShow: (record) => record.type === 2,
// 自定义提交:DatePicker 值为 dayjs 对象,格式化为 Y-m-d 后再提交
handleFinish: async (values) => {
await Create('/recon/container-return', {
...values,
return_date: dayjs(values.return_date).format('YYYY-MM-DD'),
});
message.success('回筐已登记');
return true;
},
formProps: {
grid: true,
colProps: { span: 12 },
rowProps: { gutter: 20 },
layout: 'vertical',
},
modalProps: { width: 640, title: '回筐登记' },
};
return (
<>
<div className="mb-5">
<Title level={3}></Title>
<Text type="secondary">
退/
</Text>
</div>
<XinTable<IContainerReturn> {...tableProps} />
</>
);
};
export default ContainerReturnPage;
-239
View File
@@ -1,239 +0,0 @@
import React, { useEffect, useRef, useState } from 'react';
import {
Button,
Descriptions,
Drawer,
Table,
Tag,
Typography,
} from 'antd';
import type { TableProps } from 'antd';
import XinTable from '@/components/XinTable';
import type {
XinTableColumn,
XinTableInstance,
XinTableProps,
} from '@/components/XinTable/typings.ts';
import type IStatement from '@/domain/iStatement.ts';
import type { IStatementItem } from '@/domain/iStatement.ts';
import { STATEMENT_STATUS_MAP } from '@/domain/iStatement.ts';
import { RECONCILED_MAP } from '@/domain/iReconciliation.ts';
import { getStoreOptions } from '@/api/customer/store.ts';
import type IStore from '@/domain/iStore.ts';
import { Get } from '@/api/common/table.ts';
const { Title, Text } = Typography;
/**
* 门店对账单(后台只读视角;生成/导出在小程序端)
*/
const StatementPage: React.FC = () => {
const tableRef = useRef<XinTableInstance<IStatement>>(null);
const [stores, setStores] = useState<IStore[]>([]);
const [detailOpen, setDetailOpen] = useState(false);
const [detail, setDetail] = useState<IStatement | 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<IStatement>('/recon/statement', id);
setDetail(res.data.data ?? null);
} finally {
setDetailLoading(false);
}
};
const itemColumns: TableProps<IStatementItem>['columns'] = [
{ title: '品名', dataIndex: 'product_name' },
{ title: '单价', dataIndex: 'price', align: 'right', render: (v) => `¥${v}` },
{ title: '数量', dataIndex: 'quantity', align: 'right' },
{ title: '重量', dataIndex: 'weight', align: 'right' },
{ title: '金额', dataIndex: 'amount', align: 'right', render: (v) => `¥${v}` },
{
title: '对账状态',
dataIndex: 'is_reconciled',
align: 'center',
render: (v) => {
const item = RECONCILED_MAP[Number(v ?? 0)];
return <Tag color={item?.color}>{item?.text}</Tag>;
},
},
{ title: '备注', dataIndex: 'store_remark', render: (v) => v || '-' },
];
const columns: XinTableColumn<IStatement>[] = [
{
title: '对账单号',
dataIndex: 'statement_no',
valueType: 'text',
hideInForm: true,
render: (_, record) => <Text copyable={{ text: record.statement_no }}>{record.statement_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: 'period_start',
valueType: 'dateRange',
hideInForm: true,
render: (_, record) => `${record.period_start} ~ ${record.period_end}`,
},
{
title: '总金额',
dataIndex: 'total_amount',
hideInForm: true,
hideInSearch: true,
align: 'right',
render: (_, record) => <Text strong>¥{record.total_amount}</Text>,
},
{
title: '回款周期',
dataIndex: 'payment_cycle_days',
hideInForm: true,
hideInSearch: true,
align: 'center',
render: (_, record) => `${record.payment_cycle_days}`,
},
{
title: '应结算日期',
dataIndex: 'settlement_date',
hideInForm: true,
hideInSearch: true,
align: 'center',
render: (_, record) => {
const overdue =
record.status !== 2 && record.settlement_date
? new Date(record.settlement_date).getTime() < Date.now()
: false;
return (
<Text type={overdue ? 'danger' : undefined} strong={overdue}>
{record.settlement_date}
{overdue ? '(逾期)' : ''}
</Text>
);
},
},
{
title: '状态',
dataIndex: 'status',
valueType: 'select',
hideInForm: true,
fieldProps: {
options: Object.entries(STATEMENT_STATUS_MAP).map(([value, item]) => ({
value: Number(value),
label: item.text,
})),
},
render: (_, record) => {
const item = STATEMENT_STATUS_MAP[record.status ?? 0];
return <Tag color={item?.color}>{item?.text}</Tag>;
},
align: 'center',
},
];
const operateRender: XinTableProps<IStatement>['operateRender'] = (record) => [
<Button key="detail" size="small" onClick={() => openDetail(record.id!)}>
</Button>,
];
const tableProps: XinTableProps<IStatement> = {
api: '/recon/statement',
columns,
rowKey: 'id',
accessName: 'recon.statement',
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<IStatement> {...tableProps} />
<Drawer
title={detail ? `对账单 ${detail.statement_no}` : '对账单详情'}
open={detailOpen}
onClose={() => setDetailOpen(false)}
width={860}
loading={detailLoading}
>
{detail ? (
<>
<Descriptions column={2} size="small" bordered>
<Descriptions.Item label="门店">{detail.store?.name}</Descriptions.Item>
<Descriptions.Item label="状态">
<Tag color={STATEMENT_STATUS_MAP[detail.status ?? 0]?.color}>
{STATEMENT_STATUS_MAP[detail.status ?? 0]?.text}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="对账周期">
{detail.period_start} ~ {detail.period_end}
</Descriptions.Item>
<Descriptions.Item label="回款周期(快照)">
{detail.payment_cycle_days}
</Descriptions.Item>
<Descriptions.Item label="应结算日期">
{detail.settlement_date}
</Descriptions.Item>
<Descriptions.Item label="总金额">¥{detail.total_amount}</Descriptions.Item>
{detail.remark ? (
<Descriptions.Item label="备注" span={2}>
{detail.remark}
</Descriptions.Item>
) : null}
</Descriptions>
<Title level={5} className="!mt-6 !mb-3">
{detail.items?.length ?? 0}
</Title>
<Table<IStatementItem>
rowKey="id"
size="small"
columns={itemColumns}
dataSource={detail.items ?? []}
pagination={false}
summary={() => (
<Table.Summary.Row>
<Table.Summary.Cell index={0} colSpan={4} align="right">
</Table.Summary.Cell>
<Table.Summary.Cell index={1} align="right">
<Text strong>¥{detail.total_amount}</Text>
</Table.Summary.Cell>
<Table.Summary.Cell index={2} colSpan={2} />
</Table.Summary.Row>
)}
/>
</>
) : null}
</Drawer>
</>
);
};
export default StatementPage;