生成采购单基础

This commit is contained in:
liu
2026-08-11 21:08:01 +08:00
parent 06c7bd9f88
commit 5d26f04ba2
13 changed files with 362 additions and 68 deletions
+11 -2
View File
@@ -10,7 +10,7 @@ export async function getStoreOrder(id: number) {
});
}
/** 订单状态流转(2配送中 3已完成 9取消) */
/** 订单状态流转(1已接单 3配送中 4已完成 9取消;已接单→采购中由生成采购单完成 */
export async function updateOrderStatus(id: number, status: number) {
return createAxios({
url: `/order/store/${id}/status`,
@@ -19,6 +19,15 @@ export async function updateOrderStatus(id: number, status: number) {
});
}
/** 批量订单状态流转(全量校验,任一订单不允许流转则整批中止) */
export async function batchUpdateOrderStatus(ids: number[], status: number) {
return createAxios<{ success: number }>({
url: '/order/store/batchStatus',
method: 'put',
data: { ids, status },
});
}
/** 修改周转框/周转托盘数量(自动重算附加金额与订单总金额,仅已接单/采购中/配送中可改) */
export async function updateOrderContainer(id: number, data: { box_num: number; tray_num: number }) {
return createAxios({
@@ -28,7 +37,7 @@ export async function updateOrderContainer(id: number, data: { box_num: number;
});
}
/** 待汇总预览(按商品聚合) */
/** 已接单预览(按商品聚合,生成采购单前确认 */
export async function getOrderSummary() {
return createAxios<IOrderSummaryRow[]>({
url: '/order/store/summary',
+3 -3
View File
@@ -16,12 +16,12 @@ export interface PurchaseItemUpdateParams {
remark?: string;
}
/** C1 门店订单汇总生成采购单 */
export async function generatePurchase(purchase_date: string) {
/** C1 合并「已接单」门店订单生成采购单order_ids 为空 = 全部已接单;生成后源订单转采购中) */
export async function generatePurchase(purchase_date: string, order_ids?: number[]) {
return createAxios<{ id: number; purchase_no: string }>({
url: '/purchase/order/generate',
method: 'post',
data: { purchase_date },
data: { purchase_date, order_ids },
});
}
+159 -6
View File
@@ -1,6 +1,7 @@
import React, { useEffect, useRef, useState } from 'react';
import {
Button,
DatePicker,
Descriptions,
Drawer,
Form,
@@ -8,12 +9,14 @@ import {
message,
Modal,
Popconfirm,
Radio,
Space,
Table,
Tag,
Typography,
Image,
} from 'antd';
import dayjs from 'dayjs';
import type { TableProps } from 'antd';
import XinTable from '@/components/XinTable';
import type {
@@ -24,7 +27,8 @@ import type {
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 { getStoreOrder, updateOrderContainer, updateOrderStatus, batchUpdateOrderStatus } from '@/api/order/store.ts';
import { generatePurchase } from '@/api/purchase/order.ts';
import { getStoreOptions } from '@/api/customer/store.ts';
import type IStore from '@/domain/iStore.ts';
import AuthButton from '@/components/AuthButton';
@@ -36,17 +40,29 @@ 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: 1, label: '接单' },
{ status: 9, label: '取消订单', danger: true },
],
1: [{ status: 2, label: '开始配送' }],
2: [{ status: 3, label: '完成订单' }],
2: [
{ status: 3, label: '开始配送' },
{ status: 4, label: '完成订单' },
],
3: [{ status: 4, label: '完成订单' }],
};
/** 批量流转可选目标状态(弹窗单选,后端全量校验,任一不允许则整批中止) */
const BATCH_TARGET_OPTIONS = [
{ value: 1, label: '接单(待接单 → 已接单)' },
{ value: 3, label: '开始配送(采购中 → 配送中)' },
{ value: 4, label: '完成订单(采购中/配送中 → 已完成)' },
{ value: 9, label: '取消订单(待接单 → 已取消)' },
];
/**
* 门店订单管理(只读 + 状态流转,订单由小程序端创建)
*/
@@ -63,6 +79,17 @@ const StoreOrderPage: React.FC = () => {
const [containerSaving, setContainerSaving] = useState(false);
const [containerForm] = Form.useForm<{ box_num: number; tray_num: number }>();
/** 勾选行(批量流转/生成采购单用) */
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
const [batchOpen, setBatchOpen] = useState(false);
const [batchTarget, setBatchTarget] = useState<number | null>(null);
const [batchLoading, setBatchLoading] = useState(false);
/** 生成采购单(合并已接单订单,生成后源订单转采购中) */
const [generateOpen, setGenerateOpen] = useState(false);
const [generateLoading, setGenerateLoading] = useState(false);
const [generateForm] = Form.useForm<{ purchase_date: dayjs.Dayjs }>();
useEffect(() => {
getStoreOptions().then((res) => setStores(res.data.data ?? []));
}, []);
@@ -85,6 +112,40 @@ const StoreOrderPage: React.FC = () => {
await tableRef.current?.reload();
};
/** 批量状态流转:后端全量校验,任一订单不允许流转则整批中止 */
const handleBatchStatus = async () => {
if (batchTarget === null) {
message.warning('请选择要流转的目标状态');
return;
}
setBatchLoading(true);
try {
const res = await batchUpdateOrderStatus(selectedRowKeys as number[], batchTarget);
message.success(`已批量流转 ${res.data.data?.success ?? 0}`);
setBatchOpen(false);
setBatchTarget(null);
setSelectedRowKeys([]);
await tableRef.current?.reload();
} finally {
setBatchLoading(false);
}
};
/** 生成采购单:勾选时合并所选订单(须均为已接单),未勾选时合并全部已接单订单 */
const handleGeneratePurchase = async (values: { purchase_date: dayjs.Dayjs }) => {
setGenerateLoading(true);
try {
const orderIds = selectedRowKeys.length > 0 ? selectedRowKeys.map(Number) : undefined;
const res = await generatePurchase(values.purchase_date.format('YYYY-MM-DD'), orderIds);
message.success(`采购单 ${res.data.data?.purchase_no} 已生成,源订单已转为采购中`);
setGenerateOpen(false);
setSelectedRowKeys([]);
await tableRef.current?.reload();
} finally {
setGenerateLoading(false);
}
};
const openContainer = (record: IStoreOrder) => {
setContainerOrder(record);
containerForm.setFieldsValue({ box_num: record.box_num, tray_num: record.tray_num });
@@ -294,6 +355,20 @@ const StoreOrderPage: React.FC = () => {
<Button key="detail" type={'primary'} icon={<UnorderedListOutlined />} onClick={() => openDetail(record.id!)}>
</Button>
{
(NEXT_STATUS[record.status ?? -1] ?? []).map((action) => (
<AuthButton key={action.status} auth="order.store.update">
<Popconfirm
title={`确认将订单状态更新为「${action.label}」?`}
onConfirm={() => handleStatusChange(record.id!, action.status)}
>
<Button type={action.danger ? undefined : 'primary'} danger={action.danger}>
{action.label}
</Button>
</Popconfirm>
</AuthButton>
))
}
{
CONTAINER_EDITABLE_STATUS.includes(record.status ?? -1) ? (
<AuthButton key="container" auth="order.store.update">
@@ -316,6 +391,28 @@ const StoreOrderPage: React.FC = () => {
tableRef,
operateShow: false,
formProps: false,
rowSelection: {
selectedRowKeys,
onChange: (keys) => setSelectedRowKeys(keys),
},
actionBarRender: (dom) => [
dom.search,
<AuthButton key="generate" auth="purchase.order.generate">
<Button type="primary" onClick={() => setGenerateOpen(true)}>
</Button>
</AuthButton>,
<AuthButton key="batch" auth="order.store.update">
<Button
type="primary"
disabled={selectedRowKeys.length === 0}
onClick={() => setBatchOpen(true)}
>
{selectedRowKeys.length > 0 ? `${selectedRowKeys.length}` : ''}
</Button>
</AuthButton>,
dom.keywordSearch,
],
};
return (
@@ -323,7 +420,7 @@ const StoreOrderPage: React.FC = () => {
<div className="mb-5">
<Title level={3}></Title>
<Text type="secondary">
</Text>
</div>
<XinTable<IStoreOrder> {...tableProps} />
@@ -397,6 +494,62 @@ const StoreOrderPage: React.FC = () => {
) : null}
</Drawer>
{/* 生成采购单:合并已接单订单 */}
<Modal
title="生成采购单"
open={generateOpen}
onCancel={() => setGenerateOpen(false)}
onOk={() => generateForm.submit()}
confirmLoading={generateLoading}
okText="确认生成"
destroyOnHidden
>
<div className="py-2 text-gray-500">
{selectedRowKeys.length > 0
? `将合并选中的 ${selectedRowKeys.length} 笔订单生成一张采购单(须均为已接单状态,否则整批中止)。`
: '未勾选订单时,将合并全部「已接单」订单生成一张采购单。'}
</div>
<Form form={generateForm} layout="vertical" onFinish={handleGeneratePurchase}>
<Form.Item
label="采购日期"
name="purchase_date"
initialValue={dayjs()}
rules={[{ required: true, message: '请选择采购日期' }]}
>
<DatePicker className="w-full" allowClear={false} />
</Form.Item>
</Form>
</Modal>
{/* 批量状态流转 */}
<Modal
title="批量状态流转"
open={batchOpen}
onCancel={() => setBatchOpen(false)}
onOk={handleBatchStatus}
confirmLoading={batchLoading}
okText="确认流转"
destroyOnHidden
>
<div className="py-2 text-gray-500">
{selectedRowKeys.length}
</div>
<Radio.Group
className="w-full py-2"
value={batchTarget}
onChange={(e) => setBatchTarget(e.target.value as number)}
>
<Space orientation="vertical">
{BATCH_TARGET_OPTIONS.map((opt) => (
<Radio key={opt.value} value={opt.value}>
{opt.label}
</Radio>
))}
</Space>
</Radio.Group>
</Modal>
{/* 修改周转框/托盘数量 */}
<Modal
title="修改周转框/托盘"
+2 -2
View File
@@ -457,7 +457,7 @@ const PurchaseOrderPage: React.FC = () => {
destroyOnHidden
>
<div className="py-2 text-gray-500">
</div>
<Form
form={generateForm}
@@ -480,7 +480,7 @@ const PurchaseOrderPage: React.FC = () => {
title={detail ? `采购单 ${detail.purchase_no}` : '采购单详情'}
open={detailOpen}
onClose={() => setDetailOpen(false)}
width={1080}
size={1080}
loading={detailLoading}
>
{detail ? (