Files
xin-procurement/web/pages/order/store.tsx
T
2026-08-11 19:09:10 +08:00

448 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useEffect, useRef, useState } from 'react';
import {
Button,
Descriptions,
Drawer,
Form,
InputNumber,
message,
Modal,
Popconfirm,
Space,
Table,
Tag,
Typography,
Image,
} from 'antd';
import type { TableProps } from 'antd';
import XinTable from '@/components/XinTable';
import type {
XinTableColumn,
XinTableInstance,
XinTableProps,
} from '@/components/XinTable/typings.ts';
import type IStoreOrder from '@/domain/iStoreOrder.ts';
import type { IStoreOrderItem } from '@/domain/iStoreOrder.ts';
import { STORE_ORDER_STATUS_MAP } from '@/domain/iStoreOrder.ts';
import { getStoreOrder, updateOrderContainer, updateOrderStatus } from '@/api/order/store.ts';
import { getStoreOptions } from '@/api/customer/store.ts';
import type IStore from '@/domain/iStore.ts';
import AuthButton from '@/components/AuthButton';
import {UnorderedListOutlined} from "@ant-design/icons";
const { Title, Text } = Typography;
/** 允许修改周转框/托盘数量的订单状态:已接单、采购中、配送中 */
const CONTAINER_EDITABLE_STATUS = [1, 2, 3];
/**
* 状态流转合法路径:待汇总→配送中/取消;已汇总→配送中;配送中→已完成
*/
const NEXT_STATUS: Record<number, { status: number; label: string; danger?: boolean }[]> = {
0: [
{ status: 2, label: '开始配送' },
{ status: 9, label: '取消订单', danger: true },
],
1: [{ status: 2, label: '开始配送' }],
2: [{ status: 3, label: '完成订单' }],
};
/**
* 门店订单管理(只读 + 状态流转,订单由小程序端创建)
*/
const StoreOrderPage: React.FC = () => {
const tableRef = useRef<XinTableInstance<IStoreOrder>>(null);
const [stores, setStores] = useState<IStore[]>([]);
const [detailOpen, setDetailOpen] = useState(false);
const [detail, setDetail] = useState<IStoreOrder | null>(null);
const [detailLoading, setDetailLoading] = useState(false);
const [containerOpen, setContainerOpen] = useState(false);
const [containerOrder, setContainerOrder] = useState<IStoreOrder | null>(null);
const [containerSaving, setContainerSaving] = useState(false);
const [containerForm] = Form.useForm<{ box_num: number; tray_num: number }>();
useEffect(() => {
getStoreOptions().then((res) => setStores(res.data.data ?? []));
}, []);
const openDetail = async (id: number) => {
setDetailOpen(true);
setDetailLoading(true);
try {
const res = await getStoreOrder(id);
setDetail(res.data.data ?? null);
} finally {
setDetailLoading(false);
}
};
const handleStatusChange = async (id: number, status: number) => {
await updateOrderStatus(id, status);
message.success('状态已更新');
setDetailOpen(false);
await tableRef.current?.reload();
};
const openContainer = (record: IStoreOrder) => {
setContainerOrder(record);
containerForm.setFieldsValue({ box_num: record.box_num, tray_num: record.tray_num });
setContainerOpen(true);
};
const handleContainerSave = async (values: { box_num: number; tray_num: number }) => {
if (!containerOrder?.id) return;
setContainerSaving(true);
try {
await updateOrderContainer(containerOrder.id, values);
message.success('周转框/托盘数量已更新');
setContainerOpen(false);
await tableRef.current?.reload();
} finally {
setContainerSaving(false);
}
};
// 弹窗内实时预览:附加金额 = 框×单价 + 托盘×单价;订单总金额 = 商品金额 + 附加金额
const watchBoxNum = Number(Form.useWatch('box_num', containerForm) ?? 0);
const watchTrayNum = Number(Form.useWatch('tray_num', containerForm) ?? 0);
const previewAdded = watchBoxNum * Number(containerOrder?.box_price ?? 0)
+ watchTrayNum * Number(containerOrder?.tray_price ?? 0);
const previewTotal = Number(containerOrder?.product_amount ?? 0) + previewAdded;
const itemColumns: TableProps<IStoreOrderItem>['columns'] = [
{ title: '品名', dataIndex: 'product_name' },
{ title: '规格', dataIndex: 'product_spec', render: (v) => v || '-' },
{ title: '单价', dataIndex: 'price', align: 'right', render: (v) => ${v}` },
{ title: '订货量', dataIndex: 'quantity', align: 'right' },
{ title: '金额', dataIndex: 'amount', align: 'right', render: (v) => ${v}` },
{ title: '备注', dataIndex: 'remark', render: (v) => v || '-' },
];
const columns: XinTableColumn<IStoreOrder>[] = [
{
title: '订单号',
hideInTable: true,
dataIndex: 'order_no',
valueType: 'text',
hideInForm: true
},
{
title: '商品信息',
dataIndex: 'items',
hideInForm: true,
hideInSearch: true,
width: 300,
render: (_, record) => {
return (
<Space orientation={'vertical'} className={'max-h-30 w-60 overflow-y-auto pr-6'}>
{ record.items && record.items.length > 0 && record.items.map(item => (
<div className={'flex'}>
<Image src={item.image} width={40} height={40} ></Image>
<div className={'ml-2.5'}>
<div>{item.product_name}</div>
<div>{ item.price } × { item.quantity }</div>
</div>
</div>
))}
</Space>
)
}
},
{
title: '基本信息',
dataIndex: 'order_no',
hideInForm: true,
hideInSearch: true,
width: 300,
render: (_, record) => {
return (
<Space orientation={'vertical'}>
<div>
<Text type={'secondary'}>订货门店:</Text>
<Tag color="blue">{record.store?.name ?? `门店#${record.store_id}`}</Tag>
</div>
<div>
<Text type={'secondary'}>订单号:</Text>
<Text copyable={{ text: record.order_no }}>{record.order_no}</Text>
</div>
<div><Text type={'secondary'}>订货时间:</Text>{record.created_at}</div>
</Space>
)
}
},
{
title: '门店',
dataIndex: 'store_id',
valueType: 'select',
hideInForm: true,
hideInTable: true,
fieldProps: {
options: stores.map((s) => ({ label: s.name, value: s.id })),
showSearch: true,
optionFilterProp: 'label',
},
render: (_, record) => record.store?.name ?? '-',
},
{
title: '门店信息',
dataIndex: 'store_id',
hideInForm: true,
hideInSearch: true,
width: 300,
render: (_, record) => (
<Space orientation={'vertical'}>
<div><Text type={'secondary'}>联系人:</Text>{ record.store?.contact ?? '-' }</div>
<div><Text type={'secondary'}>联系电话:</Text>{ record.store?.phone ?? '-' }</div>
<div><Text type={'secondary'}>门店地址:</Text>{ record.store?.address ?? '-' }</div>
</Space>
),
},
{
title: '订货日期',
dataIndex: 'order_date',
valueType: 'dateRange',
hideInForm: true,
hideInTable: true,
align: 'center',
},
{
title: '订单信息',
hideInForm: true,
dataIndex: 'status',
hideInSearch: true,
width: 200,
render: (_, record) => (
<Space orientation={'vertical'}>
<div>
<Text type={'secondary'}>订货数量:</Text>
{record.total_quantity}
</div>
<div>
<Text type={'secondary'}>周转框数量:</Text>
{record.box_num}
</div>
<div>
<Text type={'secondary'}>周转托盘数量:</Text>
{record.tray_num}
</div>
</Space>
)
},
{
title: '附加信息',
hideInForm: true,
dataIndex: 'box_num',
hideInSearch: true,
width: 200,
render: (_, record) => (
<Space orientation={'vertical'}>
<div>
<Text type={'secondary'}>附加总金额:</Text>
{record.added_amount}
</div>
<div>
<Text type={'secondary'}>商品总金额:</Text>
{record.product_amount}
</div>
<div>
<Text type={'secondary'}>订单总金额:</Text>
{record.total_amount}
</div>
</Space>
)
},
{
title: '订单状态',
dataIndex: 'status',
valueType: 'select',
hideInForm: true,
align: 'center',
fieldProps: {
options: Object.entries(STORE_ORDER_STATUS_MAP).map(([value, item]) => ({
value: Number(value),
label: item.text,
})),
},
render: (_, record) => (
<Tag color={STORE_ORDER_STATUS_MAP[record.status ?? 0]?.color}>
{STORE_ORDER_STATUS_MAP[record.status ?? 0]?.text}
</Tag>
)
},
{
title: '采购单ID',
dataIndex: 'purchase_id',
valueType: 'digit',
hideInForm: true,
},
{
title: '账单ID',
dataIndex: 'statement_id',
valueType: 'digit',
hideInForm: true,
},
{
title: '操作栏',
dataIndex: 'operate',
align: 'center',
hideInForm: true,
hideInSearch: true,
render: (_, record) => (
<Space orientation={'vertical'}>
<Button key="detail" type={'primary'} icon={<UnorderedListOutlined />} onClick={() => openDetail(record.id!)}>
详情
</Button>
{
CONTAINER_EDITABLE_STATUS.includes(record.status ?? -1) ? (
<AuthButton key="container" auth="order.store.update">
<Button type={'primary'} onClick={() => openContainer(record)}>
设置附加信息
</Button>
</AuthButton>
) : null
}
</Space>
)
}
];
const tableProps: XinTableProps<IStoreOrder> = {
api: '/order/store',
columns,
rowKey: 'id',
accessName: 'order.store',
tableRef,
operateShow: false,
formProps: false,
};
return (
<>
<div className="mb-5">
<Title level={3}>门店订单</Title>
<Text type="secondary">
小程序下单汇总;待汇总订单将进入采购单生成,订单创建与取消在小程序端完成。
</Text>
</div>
<XinTable<IStoreOrder> {...tableProps} />
<Drawer
title="订单详情"
open={detailOpen}
onClose={() => setDetailOpen(false)}
width={720}
loading={detailLoading}
footer={
detail && NEXT_STATUS[detail.status ?? -1] ? (
<Space className="flex justify-end">
{NEXT_STATUS[detail.status!].map((action) => (
<AuthButton key={action.status} auth="order.store.update">
<Popconfirm
title={`确认将订单状态更新为「${action.label}」?`}
onConfirm={() => handleStatusChange(detail.id!, action.status)}
>
<Button type={action.danger ? undefined : 'primary'} danger={action.danger}>
{action.label}
</Button>
</Popconfirm>
</AuthButton>
))}
</Space>
) : null
}
>
{detail ? (
<>
<Descriptions column={2} size="small" bordered>
<Descriptions.Item label="订单号">{detail.order_no}</Descriptions.Item>
<Descriptions.Item label="门店">{detail.store?.name}</Descriptions.Item>
<Descriptions.Item label="订货日期">{detail.order_date}</Descriptions.Item>
<Descriptions.Item label="状态">
<Tag color={STORE_ORDER_STATUS_MAP[detail.status ?? 0]?.color}>
{STORE_ORDER_STATUS_MAP[detail.status ?? 0]?.text}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="订货总量">{detail.total_quantity}</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">
商品明细
</Title>
<Table<IStoreOrderItem>
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} />
</Table.Summary.Row>
)}
/>
</>
) : null}
</Drawer>
{/* 修改周转框/托盘数量 */}
<Modal
title="修改周转框/托盘"
open={containerOpen}
onCancel={() => setContainerOpen(false)}
onOk={() => containerForm.submit()}
confirmLoading={containerSaving}
okText="保存"
destroyOnHidden
>
<div className="py-2 text-gray-500">
订单 {containerOrder?.order_no},保存后将按单价自动重算附加金额与订单总金额。
</div>
<Form form={containerForm} layout="vertical" onFinish={handleContainerSave}>
<Form.Item
label={`周转框数量(单价 ¥${containerOrder?.box_price ?? '0.00'}`}
name="box_num"
rules={[{ required: true, message: '请输入周转框数量' }]}
>
<InputNumber className="w-full" min={0} precision={0} placeholder="请输入周转框数量" />
</Form.Item>
<Form.Item
label={`周转托盘数量(单价 ¥${containerOrder?.tray_price ?? '0.00'}`}
name="tray_num"
rules={[{ required: true, message: '请输入周转托盘数量' }]}
>
<InputNumber className="w-full" min={0} precision={0} placeholder="请输入周转托盘数量" />
</Form.Item>
</Form>
<Space orientation="vertical" className="w-full rounded bg-gray-50 p-3">
<div>
<Text type="secondary">商品总金额:</Text>¥{containerOrder?.product_amount ?? '0.00'}
</div>
<div>
<Text type="secondary">附加总金额:</Text>
<Text strong>¥{previewAdded.toFixed(2)}</Text>
</div>
<div>
<Text type="secondary">订单总金额:</Text>
<Text strong type="danger">¥{previewTotal.toFixed(2)}</Text>
</div>
</Space>
</Modal>
</>
);
};
export default StoreOrderPage;