Files
xin-procurement/web/pages/order/store.tsx
T
2026-08-11 18:35:54 +08:00

349 lines
11 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,
message,
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, 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';
const { Title, Text } = Typography;
/**
* 状态流转合法路径:待汇总→配送中/取消;已汇总→配送中;配送中→已完成
*/
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);
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 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,
},
];
const operateRender: XinTableProps<IStoreOrder>['operateRender'] = (record) => [
<Button key="detail" size="small" onClick={() => openDetail(record.id!)}>
详情
</Button>,
];
const tableProps: XinTableProps<IStoreOrder> = {
api: '/order/store',
columns,
rowKey: 'id',
accessName: 'order.store',
tableRef,
operateRender,
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>
</>
);
};
export default StoreOrderPage;