feat: implement create stock check and adjust modals
Both pages had toolbar buttons with no onClick handler. Added modal forms: - Checks: date + checker + optional detail rows (product + actual qty) - Adjusts: date + reason + required detail rows (product + adjust qty, negative to decrease) Also updated createStockCheck and createStockAdjust API function signatures from Partial<StockCheck/Adjust> to explicit typed request shapes. Fixed a go-zero validation bug: non-optional fields (checker, adjustReason) must be present in the JSON body; antd Form returns undefined for untouched inputs which JSON.stringify omits, causing 400. Solved by setting default empty strings in Form initialValues.
This commit is contained in:
parent
5c903d7abc
commit
c08a9a4854
@ -1,8 +1,9 @@
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { ProColumns, ProTable } from '@ant-design/pro-components';
|
||||
import { Button, Tag } from 'antd';
|
||||
import { listStockAdjusts } from '@/services/api';
|
||||
import type { StockAdjust } from '@/types/api';
|
||||
import { DeleteOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import { ActionType, ProColumns, ProTable } from '@ant-design/pro-components';
|
||||
import { Button, Col, Form, Input, Modal, Row, Select, Table, Tag, message } from 'antd';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { createStockAdjust, listProducts, listStockAdjusts } from '@/services/api';
|
||||
import type { Product, StockAdjust } from '@/types/api';
|
||||
|
||||
const STATUS_MAP: Record<number, { text: string; color: string }> = {
|
||||
0: { text: '待审核', color: 'warning' },
|
||||
@ -10,6 +11,9 @@ const STATUS_MAP: Record<number, { text: string; color: string }> = {
|
||||
2: { text: '已驳回', color: 'error' },
|
||||
};
|
||||
|
||||
interface DetailRow { key: number; productId?: string; adjustQuantity?: string }
|
||||
let rowKey = 0;
|
||||
|
||||
const columns: ProColumns<StockAdjust>[] = [
|
||||
{ title: '调整单号', dataIndex: 'adjustNo' },
|
||||
{ title: '调整日期', dataIndex: 'adjustDate', valueType: 'date', search: false },
|
||||
@ -28,20 +32,160 @@ const columns: ProColumns<StockAdjust>[] = [
|
||||
{ title: '操作', valueType: 'option', render: () => [<a key="view">查看</a>] },
|
||||
];
|
||||
|
||||
const AdjustsPage: React.FC = () => (
|
||||
<ProTable<StockAdjust>
|
||||
headerTitle="库存调整"
|
||||
rowKey="adjustId"
|
||||
columns={columns}
|
||||
search={false}
|
||||
request={async (params) => {
|
||||
const res = await listStockAdjusts({ page: params.current, pageSize: params.pageSize });
|
||||
return { data: res?.data?.list || [], total: res?.data?.total || 0, success: res?.code === 0 };
|
||||
}}
|
||||
toolBarRender={() => [
|
||||
<Button key="add" type="primary" icon={<PlusOutlined />}>创建调整单</Button>,
|
||||
]}
|
||||
/>
|
||||
);
|
||||
const AdjustsPage: React.FC = () => {
|
||||
const actionRef = useRef<ActionType>();
|
||||
const [form] = Form.useForm();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [products, setProducts] = useState<Product[]>([]);
|
||||
const [details, setDetails] = useState<DetailRow[]>([{ key: rowKey++ }]);
|
||||
|
||||
useEffect(() => {
|
||||
listProducts({ page: 1, pageSize: 500 }).then((res) => {
|
||||
if (res?.code === 0) setProducts(res.data?.list || []);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleOpen = () => {
|
||||
setDetails([{ key: rowKey++ }]);
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
let values: { adjustDate: string; adjustReason?: string; remark?: string };
|
||||
try {
|
||||
values = await form.validateFields();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
const validDetails = details.filter((r) => r.productId && r.adjustQuantity);
|
||||
if (validDetails.length === 0) {
|
||||
message.error('请至少添加一条调整明细');
|
||||
return;
|
||||
}
|
||||
setSubmitting(true);
|
||||
try {
|
||||
const res = await createStockAdjust({
|
||||
...values,
|
||||
details: validDetails.map((r) => ({ productId: r.productId!, adjustQuantity: r.adjustQuantity! })),
|
||||
});
|
||||
if (res?.code !== 0) { message.error(res?.msg || '创建失败'); return; }
|
||||
setOpen(false);
|
||||
actionRef.current?.reload();
|
||||
message.success('调整单已创建');
|
||||
} catch {
|
||||
message.error('创建失败');
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const addRow = () => setDetails((prev) => [...prev, { key: rowKey++ }]);
|
||||
const removeRow = (idx: number) => setDetails((prev) => prev.filter((_, i) => i !== idx));
|
||||
const updateRow = (idx: number, field: keyof DetailRow, value: string) =>
|
||||
setDetails((prev) => prev.map((r, i) => (i === idx ? { ...r, [field]: value } : r)));
|
||||
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProTable<StockAdjust>
|
||||
actionRef={actionRef}
|
||||
headerTitle="库存调整"
|
||||
rowKey="adjustId"
|
||||
columns={columns}
|
||||
search={false}
|
||||
request={async (params) => {
|
||||
const res = await listStockAdjusts({ page: params.current, pageSize: params.pageSize });
|
||||
return { data: res?.data?.list || [], total: res?.data?.total || 0, success: res?.code === 0 };
|
||||
}}
|
||||
toolBarRender={() => [
|
||||
<Button key="add" type="primary" icon={<PlusOutlined />} onClick={handleOpen}>创建调整单</Button>,
|
||||
]}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
title="创建调整单"
|
||||
open={open}
|
||||
onOk={handleSubmit}
|
||||
onCancel={() => setOpen(false)}
|
||||
confirmLoading={submitting}
|
||||
width={680}
|
||||
afterOpenChange={(o) => { if (!o) form.resetFields(); }}
|
||||
>
|
||||
<Form form={form} layout="vertical" initialValues={{ adjustDate: today, adjustReason: '', remark: '' }}>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item name="adjustDate" label="调整日期" rules={[{ required: true, message: '请选择日期' }]}>
|
||||
<Input type="date" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name="adjustReason" label="调整原因">
|
||||
<Input placeholder="如:盘亏、损耗、退货入库" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Form.Item name="remark" label="备注">
|
||||
<Input.TextArea rows={2} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
|
||||
<span style={{ fontWeight: 500 }}>调整明细</span>
|
||||
<Button size="small" icon={<PlusOutlined />} onClick={addRow}>添加行</Button>
|
||||
</div>
|
||||
<Table
|
||||
size="small"
|
||||
rowKey="key"
|
||||
dataSource={details}
|
||||
pagination={false}
|
||||
columns={[
|
||||
{
|
||||
title: '产品',
|
||||
render: (_, __, idx) => (
|
||||
<Select
|
||||
style={{ width: '100%' }}
|
||||
showSearch
|
||||
placeholder="选择产品"
|
||||
value={details[idx].productId}
|
||||
optionFilterProp="label"
|
||||
options={products.map((p) => ({ value: p.productId, label: `${p.productName}${p.spec ? ' ' + p.spec : ''}${p.color ? ' ' + p.color : ''}` }))}
|
||||
onChange={(v) => updateRow(idx, 'productId', v)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '调整数量(米)',
|
||||
width: 150,
|
||||
render: (_, __, idx) => (
|
||||
<Input
|
||||
type="number"
|
||||
step="0.01"
|
||||
placeholder="正数增加,负数减少"
|
||||
value={details[idx].adjustQuantity}
|
||||
onChange={(e) => updateRow(idx, 'adjustQuantity', e.target.value)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
width: 40,
|
||||
render: (_, __, idx) => (
|
||||
<Button
|
||||
type="text"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
disabled={details.length === 1}
|
||||
onClick={() => removeRow(idx)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdjustsPage;
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { ProColumns, ProTable } from '@ant-design/pro-components';
|
||||
import { Button, Tag } from 'antd';
|
||||
import { listStockChecks } from '@/services/api';
|
||||
import type { StockCheck } from '@/types/api';
|
||||
import { DeleteOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import { ActionType, ProColumns, ProTable } from '@ant-design/pro-components';
|
||||
import { Button, Col, Form, Input, Modal, Row, Select, Table, Tag, message } from 'antd';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { createStockCheck, listProducts, listStockChecks } from '@/services/api';
|
||||
import type { Product, StockCheck } from '@/types/api';
|
||||
|
||||
const STATUS_MAP: Record<number, { text: string; color: string }> = {
|
||||
0: { text: '草稿', color: 'default' },
|
||||
@ -10,6 +11,9 @@ const STATUS_MAP: Record<number, { text: string; color: string }> = {
|
||||
2: { text: '已完成', color: 'success' },
|
||||
};
|
||||
|
||||
interface DetailRow { key: number; productId?: string; actualQuantity?: string }
|
||||
let rowKey = 0;
|
||||
|
||||
const columns: ProColumns<StockCheck>[] = [
|
||||
{ title: '盘点单号', dataIndex: 'checkNo' },
|
||||
{ title: '盘点日期', dataIndex: 'checkDate', valueType: 'date', search: false },
|
||||
@ -27,20 +31,151 @@ const columns: ProColumns<StockCheck>[] = [
|
||||
{ title: '操作', valueType: 'option', render: () => [<a key="view">查看</a>] },
|
||||
];
|
||||
|
||||
const ChecksPage: React.FC = () => (
|
||||
<ProTable<StockCheck>
|
||||
headerTitle="库存盘点"
|
||||
rowKey="checkId"
|
||||
columns={columns}
|
||||
search={false}
|
||||
request={async (params) => {
|
||||
const res = await listStockChecks({ page: params.current, pageSize: params.pageSize });
|
||||
return { data: res?.data?.list || [], total: res?.data?.total || 0, success: res?.code === 0 };
|
||||
}}
|
||||
toolBarRender={() => [
|
||||
<Button key="add" type="primary" icon={<PlusOutlined />}>创建盘点单</Button>,
|
||||
]}
|
||||
/>
|
||||
);
|
||||
const ChecksPage: React.FC = () => {
|
||||
const actionRef = useRef<ActionType>();
|
||||
const [form] = Form.useForm();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [products, setProducts] = useState<Product[]>([]);
|
||||
const [details, setDetails] = useState<DetailRow[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
listProducts({ page: 1, pageSize: 500 }).then((res) => {
|
||||
if (res?.code === 0) setProducts(res.data?.list || []);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleOpen = () => {
|
||||
setDetails([]);
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
let values: { checkDate: string; checker?: string; remark?: string };
|
||||
try {
|
||||
values = await form.validateFields();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
setSubmitting(true);
|
||||
try {
|
||||
const res = await createStockCheck({
|
||||
...values,
|
||||
details: details
|
||||
.filter((r) => r.productId && r.actualQuantity)
|
||||
.map((r) => ({ productId: r.productId!, actualQuantity: r.actualQuantity! })),
|
||||
});
|
||||
if (res?.code !== 0) { message.error(res?.msg || '创建失败'); return; }
|
||||
setOpen(false);
|
||||
actionRef.current?.reload();
|
||||
message.success('盘点单已创建');
|
||||
} catch {
|
||||
message.error('创建失败');
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const addRow = () => setDetails((prev) => [...prev, { key: rowKey++ }]);
|
||||
const removeRow = (idx: number) => setDetails((prev) => prev.filter((_, i) => i !== idx));
|
||||
const updateRow = (idx: number, field: keyof DetailRow, value: string) =>
|
||||
setDetails((prev) => prev.map((r, i) => (i === idx ? { ...r, [field]: value } : r)));
|
||||
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProTable<StockCheck>
|
||||
actionRef={actionRef}
|
||||
headerTitle="库存盘点"
|
||||
rowKey="checkId"
|
||||
columns={columns}
|
||||
search={false}
|
||||
request={async (params) => {
|
||||
const res = await listStockChecks({ page: params.current, pageSize: params.pageSize });
|
||||
return { data: res?.data?.list || [], total: res?.data?.total || 0, success: res?.code === 0 };
|
||||
}}
|
||||
toolBarRender={() => [
|
||||
<Button key="add" type="primary" icon={<PlusOutlined />} onClick={handleOpen}>创建盘点单</Button>,
|
||||
]}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
title="创建盘点单"
|
||||
open={open}
|
||||
onOk={handleSubmit}
|
||||
onCancel={() => setOpen(false)}
|
||||
confirmLoading={submitting}
|
||||
width={680}
|
||||
afterOpenChange={(o) => { if (!o) form.resetFields(); }}
|
||||
>
|
||||
<Form form={form} layout="vertical" initialValues={{ checkDate: today, checker: '', remark: '' }}>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item name="checkDate" label="盘点日期" rules={[{ required: true, message: '请选择日期' }]}>
|
||||
<Input type="date" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name="checker" label="盘点人">
|
||||
<Input placeholder="可选" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Form.Item name="remark" label="备注">
|
||||
<Input.TextArea rows={2} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
|
||||
<span style={{ fontWeight: 500 }}>盘点明细(可选)</span>
|
||||
<Button size="small" icon={<PlusOutlined />} onClick={addRow}>添加行</Button>
|
||||
</div>
|
||||
<Table
|
||||
size="small"
|
||||
rowKey="key"
|
||||
dataSource={details}
|
||||
pagination={false}
|
||||
columns={[
|
||||
{
|
||||
title: '产品',
|
||||
render: (_, __, idx) => (
|
||||
<Select
|
||||
style={{ width: '100%' }}
|
||||
showSearch
|
||||
placeholder="选择产品"
|
||||
value={details[idx].productId}
|
||||
optionFilterProp="label"
|
||||
options={products.map((p) => ({ value: p.productId, label: `${p.productName}${p.spec ? ' ' + p.spec : ''}${p.color ? ' ' + p.color : ''}` }))}
|
||||
onChange={(v) => updateRow(idx, 'productId', v)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '实盘数量(米)',
|
||||
width: 140,
|
||||
render: (_, __, idx) => (
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
step="0.01"
|
||||
value={details[idx].actualQuantity}
|
||||
onChange={(e) => updateRow(idx, 'actualQuantity', e.target.value)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
width: 40,
|
||||
render: (_, __, idx) => (
|
||||
<Button type="text" danger icon={<DeleteOutlined />} onClick={() => removeRow(idx)} />
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChecksPage;
|
||||
|
||||
@ -200,8 +200,12 @@ export const listStockChecks = (params: PaginationParams) =>
|
||||
export const getStockCheck = (id: string) =>
|
||||
request<ApiResponse<StockCheck>>(`/api/v1/inventory/checks/${id}`, { method: 'GET' });
|
||||
|
||||
export const createStockCheck = (data: Partial<StockCheck>) =>
|
||||
request<ApiResponse>('/api/v1/inventory/checks', { method: 'POST', data });
|
||||
export const createStockCheck = (data: {
|
||||
checkDate: string;
|
||||
checker?: string;
|
||||
remark?: string;
|
||||
details?: Array<{ productId: string; actualQuantity: string; remark?: string }>;
|
||||
}) => request<ApiResponse<{ id: string }>>('/api/v1/inventory/checks', { method: 'POST', data });
|
||||
|
||||
export const confirmStockCheck = (id: string) =>
|
||||
request<ApiResponse>(`/api/v1/inventory/checks/${id}/confirm`, { method: 'POST' });
|
||||
@ -214,8 +218,12 @@ export const listStockAdjusts = (params: PaginationParams) =>
|
||||
export const getStockAdjust = (id: string) =>
|
||||
request<ApiResponse<StockAdjust>>(`/api/v1/inventory/adjusts/${id}`, { method: 'GET' });
|
||||
|
||||
export const createStockAdjust = (data: Partial<StockAdjust>) =>
|
||||
request<ApiResponse>('/api/v1/inventory/adjusts', { method: 'POST', data });
|
||||
export const createStockAdjust = (data: {
|
||||
adjustDate: string;
|
||||
adjustReason?: string;
|
||||
remark?: string;
|
||||
details?: Array<{ productId: string; adjustQuantity: string; remark?: string }>;
|
||||
}) => request<ApiResponse<{ id: string }>>('/api/v1/inventory/adjusts', { method: 'POST', data });
|
||||
|
||||
export const approveStockAdjust = (id: string, action: number) =>
|
||||
request<ApiResponse>(`/api/v1/inventory/adjusts/${id}/approve`, { method: 'POST', data: { action } });
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user