订单详情

This commit is contained in:
liu
2026-08-11 23:26:34 +08:00
parent 5d26f04ba2
commit ce958d690f
10 changed files with 996 additions and 70 deletions
+292 -40
View File
@@ -4,20 +4,21 @@ import {
DatePicker,
Descriptions,
Drawer,
Empty,
Form,
Input,
InputNumber,
message,
Modal,
Popconfirm,
Radio,
Select,
Space,
Table,
Tag,
Typography,
Image,
} from 'antd';
import dayjs from 'dayjs';
import type { TableProps } from 'antd';
import XinTable from '@/components/XinTable';
import type {
XinTableColumn,
@@ -25,20 +26,33 @@ import type {
XinTableProps,
} from '@/components/XinTable/typings.ts';
import type IStoreOrder from '@/domain/iStoreOrder.ts';
import type { IStoreOrderItem } from '@/domain/iStoreOrder.ts';
import type { IStoreOrderItem, IStoreOrderItemUpdate } from '@/domain/iStoreOrder.ts';
import { STORE_ORDER_STATUS_MAP } from '@/domain/iStoreOrder.ts';
import { getStoreOrder, updateOrderContainer, updateOrderStatus, batchUpdateOrderStatus } from '@/api/order/store.ts';
import {
getStoreOrder,
updateOrderContainer,
updateOrderStatus,
batchUpdateOrderStatus,
updateOrderItem,
syncOrderItem,
} from '@/api/order/store.ts';
import { generatePurchase } from '@/api/purchase/order.ts';
import { getStoreOptions } from '@/api/customer/store.ts';
import { getSupplierOptions } from '@/api/customer/supplier.ts';
import type IStore from '@/domain/iStore.ts';
import type ISupplier from '@/domain/iSupplier.ts';
import AuthButton from '@/components/AuthButton';
import {UnorderedListOutlined} from "@ant-design/icons";
import { EditOutlined, SyncOutlined, UnorderedListOutlined } from '@ant-design/icons';
import TextArea from "antd/es/input/TextArea";
const { Title, Text } = Typography;
/** 允许修改周转框/托盘数量的订单状态:已接单、采购中、配送中 */
const CONTAINER_EDITABLE_STATUS = [1, 2, 3];
/** 允许修改/同步商品明细的订单状态:待接单、已接单、采购中、配送中(已完成/已取消锁定) */
const ITEM_EDITABLE_STATUS = [0, 1, 2, 3];
/**
* 状态流转合法路径:待接单→已接单/已取消;采购中→配送中/已完成;配送中→已完成
* (已接单→采购中 通过「生成采购单」完成,不在流转按钮内)
@@ -90,8 +104,18 @@ const StoreOrderPage: React.FC = () => {
const [generateLoading, setGenerateLoading] = useState(false);
const [generateForm] = Form.useForm<{ purchase_date: dayjs.Dayjs }>();
/** 供应商选项(明细编辑用) */
const [suppliers, setSuppliers] = useState<ISupplier[]>([]);
/** 商品明细编辑(弹窗表单,保存后后端重算订单总价) */
const [itemEditOpen, setItemEditOpen] = useState(false);
const [itemEditTarget, setItemEditTarget] = useState<IStoreOrderItem | null>(null);
const [itemSaving, setItemSaving] = useState(false);
const [itemForm] = Form.useForm<IStoreOrderItemUpdate>();
useEffect(() => {
getStoreOptions().then((res) => setStores(res.data.data ?? []));
getSupplierOptions().then((res) => setSuppliers(res.data.data ?? []));
}, []);
const openDetail = async (id: number) => {
@@ -165,6 +189,46 @@ const StoreOrderPage: React.FC = () => {
}
};
/** 打开明细编辑弹窗(商品快照 + 订货量/重量) */
const openItemEdit = (item: IStoreOrderItem) => {
setItemEditTarget(item);
itemForm.setFieldsValue({
supplier_id: item.supplier_id ?? 0,
product_name: item.product_name ?? '',
product_spec: item.product_spec ?? '',
unit: item.unit ?? '',
price: Number(item.price ?? 0),
cost_price: Number(item.cost_price ?? 0),
quantity: Number(item.quantity ?? 0),
weight: Number(item.weight ?? 0),
});
setItemEditOpen(true);
};
/** 保存明细修改:后端重算单品金额与订单总价 */
const handleItemSave = async (values: IStoreOrderItemUpdate) => {
if (!itemEditTarget?.id || !detail?.id) return;
setItemSaving(true);
try {
await updateOrderItem(itemEditTarget.id, values);
message.success('明细已更新,订单总价已重算');
setItemEditOpen(false);
await openDetail(detail.id);
await tableRef.current?.reload();
} finally {
setItemSaving(false);
}
};
/** 一键同步:按商品ID拉取最新商品档案(供应商/品名/规格/单位/单价/成本价) */
const handleItemSync = async (item: IStoreOrderItem) => {
if (!item.id || !detail?.id) return;
await syncOrderItem(item.id);
message.success('已同步最新商品信息,订单总价已重算');
await openDetail(detail.id);
await tableRef.current?.reload();
};
// 弹窗内实时预览:附加金额 = 框×单价 + 托盘×单价;订单总金额 = 商品金额 + 附加金额
const watchBoxNum = Number(Form.useWatch('box_num', containerForm) ?? 0);
const watchTrayNum = Number(Form.useWatch('tray_num', containerForm) ?? 0);
@@ -172,14 +236,8 @@ const StoreOrderPage: React.FC = () => {
+ 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 itemEditable = ITEM_EDITABLE_STATUS.includes(detail?.status ?? -1);
const columns: XinTableColumn<IStoreOrder>[] = [
{
@@ -189,6 +247,13 @@ const StoreOrderPage: React.FC = () => {
valueType: 'text',
hideInForm: true
},
{
title: '商品名称',
hideInTable: true,
dataIndex: 'product_name',
valueType: 'text',
hideInForm: true
},
{
title: '商品信息',
dataIndex: 'items',
@@ -352,7 +417,7 @@ const StoreOrderPage: React.FC = () => {
hideInSearch: true,
render: (_, record) => (
<Space orientation={'vertical'}>
<Button key="detail" type={'primary'} icon={<UnorderedListOutlined />} onClick={() => openDetail(record.id!)}>
<Button key="detail" type={'link'} icon={<UnorderedListOutlined />} onClick={() => openDetail(record.id!)}>
</Button>
{
@@ -362,7 +427,7 @@ const StoreOrderPage: React.FC = () => {
title={`确认将订单状态更新为「${action.label}」?`}
onConfirm={() => handleStatusChange(record.id!, action.status)}
>
<Button type={action.danger ? undefined : 'primary'} danger={action.danger}>
<Button type={action.danger ? undefined : 'link'} danger={action.danger}>
{action.label}
</Button>
</Popconfirm>
@@ -372,7 +437,7 @@ const StoreOrderPage: React.FC = () => {
{
CONTAINER_EDITABLE_STATUS.includes(record.status ?? -1) ? (
<AuthButton key="container" auth="order.store.update">
<Button type={'primary'} onClick={() => openContainer(record)}>
<Button type={'link'} onClick={() => openContainer(record)}>
</Button>
</AuthButton>
@@ -429,7 +494,7 @@ const StoreOrderPage: React.FC = () => {
title="订单详情"
open={detailOpen}
onClose={() => setDetailOpen(false)}
width={720}
size={1000}
loading={detailLoading}
footer={
detail && NEXT_STATUS[detail.status ?? -1] ? (
@@ -452,9 +517,21 @@ const StoreOrderPage: React.FC = () => {
>
{detail ? (
<>
<Descriptions column={2} size="small" bordered>
{/* 门店信息 */}
<Descriptions title="门店信息" column={3} size="small" bordered>
<Descriptions.Item label="门店名称">
{detail.store?.name ?? `门店#${detail.store_id}`}
</Descriptions.Item>
<Descriptions.Item label="联系人">{detail.store?.contact ?? '-'}</Descriptions.Item>
<Descriptions.Item label="联系电话">{detail.store?.phone ?? '-'}</Descriptions.Item>
<Descriptions.Item label="门店地址" span={3}>
{detail.store?.address ?? '-'}
</Descriptions.Item>
</Descriptions>
{/* 订单信息 */}
<Descriptions title="订单信息" column={3} size="small" bordered className="mt-4!">
<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}>
@@ -462,34 +539,128 @@ const StoreOrderPage: React.FC = () => {
</Tag>
</Descriptions.Item>
<Descriptions.Item label="订货总量">{detail.total_quantity}</Descriptions.Item>
<Descriptions.Item label="订单金额">¥{detail.total_amount}</Descriptions.Item>
<Descriptions.Item label="周转框数量">{detail.box_num}</Descriptions.Item>
<Descriptions.Item label="周转托盘数量">{detail.tray_num}</Descriptions.Item>
{detail.remark ? (
<Descriptions.Item label="备注" span={2}>
<Descriptions.Item label="备注" span={3}>
{detail.remark}
</Descriptions.Item>
) : null}
</Descriptions>
<Title level={5} className="!mt-6 !mb-3">
{/* 商品明细(商城模式:首图 + 单价 × 订货量 + 金额) */}
<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>
)}
/>
<div className="overflow-hidden rounded border border-gray-200">
<div className="flex bg-gray-50 px-4 py-2 text-sm text-gray-500">
<div className="flex-1"></div>
<div className="w-30 shrink-0 text-center"></div>
<div className="w-30 shrink-0 text-center"></div>
<div className="w-26 shrink-0 text-center"></div>
<div className="w-33 shrink-0 text-center"></div>
{itemEditable ? <div className="w-40 shrink-0 text-center"></div> : null}
</div>
{(detail.items ?? []).map((item) => (
<div key={item.id} className="flex items-center border-t border-gray-100 px-4 py-3">
<div className="flex min-w-0 flex-1 items-center">
<Image.PreviewGroup>
{item.image ? (
<Image
src={item.image}
width={64}
height={64}
style={{ objectFit: 'cover', borderRadius: 4 }}
/>
) : (
<div className="flex h-16 w-16 shrink-0 items-center justify-center rounded bg-gray-100 text-xs text-gray-400">
</div>
)}
</Image.PreviewGroup>
<div className="ml-3 min-w-0">
<div className="text-sm font-medium">{item.product_name}</div>
<div className="mt-0.5 text-xs text-gray-500">
{item.product_spec || '-'} / {item.unit || '-'} · ¥
{item.cost_price ?? '0.00'}
{Number(item.weight ?? 0) > 0 ? ` · 重量:${item.weight}` : ''}
</div>
{item.remark ? (
<div className="mt-0.5 truncate text-xs text-gray-500">{item.remark}</div>
) : null}
</div>
</div>
<div className="w-30 shrink-0 text-center">{item.supplier?.name ?? '-'}</div>
<div className="w-30 shrink-0 text-center">¥{item.price}</div>
<div className="w-26 shrink-0 text-center">{item.quantity}</div>
<div className="w-33 shrink-0 text-center">
<Text strong>¥{item.amount}</Text>
</div>
{itemEditable ? (
<div className="w-40 shrink-0 text-center">
<Space size={0}>
<AuthButton auth="order.store.update">
<Button
type="link"
size="small"
icon={<EditOutlined />}
onClick={() => openItemEdit(item)}
>
</Button>
</AuthButton>
<AuthButton auth="order.store.update">
<Popconfirm
title="确认同步最新商品信息?"
description="将按商品ID同步供应商、品名、规格、单位、成本价,并按门店等级价重算单价"
onConfirm={() => handleItemSync(item)}
>
<Button type="link" size="small" icon={<SyncOutlined />}>
</Button>
</Popconfirm>
</AuthButton>
</Space>
</div>
) : null}
</div>
))}
{(detail.items ?? []).length === 0 ? (
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description="暂无商品明细"
className="py-8!"
/>
) : null}
</div>
{/* 附加信息 */}
<div className="mt-3! flex justify-end">
<div className="w-full rounded bg-gray-50 p-4 text-sm">
<div className="flex justify-between py-1">
<Text type="secondary">
{detail.box_num} × ¥{detail.box_price ?? '0.00'}
</Text>
<span>¥{detail.box_amount ?? '0.00'}</span>
</div>
<div className="flex justify-between py-1">
<Text type="secondary">
{detail.tray_num} × ¥{detail.tray_price ?? '0.00'}
</Text>
<span>¥{detail.tray_amount ?? '0.00'}</span>
</div>
<div className="flex justify-between py-1">
<Text type="secondary"></Text>
<span>¥{detail.product_amount}</span>
</div>
<div className="mt-1! flex justify-between border-t border-gray-200 pt-2">
<Text strong></Text>
<Text strong type="danger">
¥{detail.total_amount}
</Text>
</div>
</div>
</div>
</>
) : null}
</Drawer>
@@ -593,6 +764,87 @@ const StoreOrderPage: React.FC = () => {
</div>
</Space>
</Modal>
{/* 编辑商品明细(保存后后端重算单品金额与订单总价) */}
<Modal
title="编辑商品明细"
open={itemEditOpen}
onCancel={() => setItemEditOpen(false)}
onOk={() => itemForm.submit()}
confirmLoading={itemSaving}
okText="保存"
width={640}
destroyOnHidden
>
<div className="py-2 text-gray-500">
{detail?.order_no}
</div>
<Form form={itemForm} layout="vertical" onFinish={handleItemSave}>
<div className="grid grid-cols-2 gap-x-4">
<Form.Item
label="供应商"
name="supplier_id"
rules={[{ required: true, message: '请选择供应商' }]}
>
<Select
showSearch={{
optionFilterProp: 'label'
}}
placeholder="请选择供应商"
options={suppliers.map((s) => ({ label: s.name, value: s.id }))}
/>
</Form.Item>
<Form.Item
label="品名"
name="product_name"
rules={[{ required: true, message: '请输入品名' }]}
>
<Input placeholder="请输入品名" maxLength={100} />
</Form.Item>
<Form.Item label="规格/包规" name="product_spec">
<Input placeholder="请输入规格/包规" maxLength={100} />
</Form.Item>
<Form.Item
label="计价单位"
name="unit"
rules={[{ required: true, message: '请输入计价单位' }]}
>
<Input placeholder="斤/件/箱等" maxLength={20} />
</Form.Item>
<Form.Item
label="单价(元)"
name="price"
rules={[{ required: true, message: '请输入单价' }]}
>
<InputNumber className="w-full" min={0} precision={2} placeholder="请输入单价" />
</Form.Item>
<Form.Item
label="成本价(元)"
name="cost_price"
rules={[{ required: true, message: '请输入成本价' }]}
>
<InputNumber className="w-full" min={0} precision={2} placeholder="请输入成本价" />
</Form.Item>
<Form.Item
label="订货量"
name="quantity"
rules={[{ required: true, message: '请输入订货量' }]}
>
<InputNumber className="w-full" min={1} precision={0} placeholder="请输入订货量" />
</Form.Item>
<Form.Item
label="重量"
name="weight"
rules={[{ required: true, message: '请输入重量' }]}
>
<InputNumber className="w-full" min={0} precision={3} placeholder="请输入重量" />
</Form.Item>
<Form.Item label="备注" name="remark">
<TextArea className="w-full" placeholder="请输入备注" />
</Form.Item>
</div>
</Form>
</Modal>
</>
);
};