订单详情
This commit is contained in:
+292
-40
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user