448 lines
15 KiB
TypeScript
448 lines
15 KiB
TypeScript
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;
|