diff --git a/src/pages/Inventory/Import/index.tsx b/src/pages/Inventory/Import/index.tsx
index d05981a..a3d9b11 100644
--- a/src/pages/Inventory/Import/index.tsx
+++ b/src/pages/Inventory/Import/index.tsx
@@ -5,6 +5,55 @@ import { Upload, Card, message, Button, Alert, Space } from 'antd';
const { Dragger } = Upload;
+const templateHeaders = [
+ '产品名称',
+ '规格型号',
+ '颜色',
+ '色号',
+ '产品码',
+ '销售价',
+ '批次号',
+ '纱线配比',
+ '经线克重(g/m)',
+ '纬线克重(g/m)',
+ '生产工艺',
+ '备注',
+];
+
+const templateSample = [
+ '示例布料',
+ '120cm',
+ '本色',
+ '01',
+ '',
+ '0',
+ '',
+ '{"warp":[],"weft":[]}',
+ '0',
+ '0',
+ '无',
+ '',
+];
+
+const escapeHtml = (value: string) =>
+ value.replace(/&/g, '&').replace(//g, '>');
+
+const downloadTemplate = () => {
+ const rows = [templateHeaders, templateSample]
+ .map((row) => `
${row.map((cell) => `| ${escapeHtml(cell)} | `).join('')}
`)
+ .join('');
+ const html = ``;
+ const blob = new Blob([html], { type: 'application/vnd.ms-excel;charset=utf-8' });
+ const url = window.URL.createObjectURL(blob);
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = '产品导入模板.xls';
+ document.body.appendChild(link);
+ link.click();
+ link.remove();
+ window.URL.revokeObjectURL(url);
+};
+
const ImportPage: React.FC = () => {
const token = localStorage.getItem('token');
@@ -14,15 +63,15 @@ const ImportPage: React.FC = () => {
- }>下载导入模板
+ } onClick={downloadTemplate}>下载导入模板
{
if (info.file.status === 'done') {
@@ -39,7 +88,7 @@ const ImportPage: React.FC = () => {
>
点击或拖拽文件到此处上传
- 支持 .xlsx、.xls、.csv 格式
+ 支持 .xlsx、.xls 格式
diff --git a/src/pages/Inventory/Products/index.test.tsx b/src/pages/Inventory/Products/index.test.tsx
index 7faf4c3..c30774b 100644
--- a/src/pages/Inventory/Products/index.test.tsx
+++ b/src/pages/Inventory/Products/index.test.tsx
@@ -7,7 +7,12 @@ vi.mock('@/services/api');
import { smokeTest } from '../../../../test/smoke';
import Page from './index';
-import { createProduct, getProductSummary } from '@/services/api';
+import { createProduct, getProductSummary, listYarns } from '@/services/api';
+
+const clickDialogSubmit = async (user: ReturnType, dialog: HTMLElement) => {
+ const submitBtn = within(dialog).getByRole('button', { name: /确\s*定|确\s*认|提交|OK/i });
+ await user.click(submitBtn);
+};
describe('Inventory / Products page', () => {
beforeEach(() => {
@@ -56,19 +61,111 @@ describe('Inventory / Products page', () => {
const nameInput = within(dialog).getByRole('textbox', { name: /产品名称/ });
await user.type(nameInput, '测试产品');
- // Click the submit/OK button – find the non-close button in the dialog footer
- const buttons = within(dialog).getAllByRole('button');
- const submitBtn = buttons.find((b) => {
- const text = b.textContent?.trim() ?? '';
- return text && !text.includes('×') && !text.includes('✕') && !/取/.test(text) && !/重/.test(text) && !/Cancel/i.test(text);
- });
- if (submitBtn) await user.click(submitBtn);
+ await clickDialogSubmit(user, dialog);
await waitFor(() => {
expect(vi.mocked(createProduct)).toHaveBeenCalled();
});
});
+ it('renders yarn ratio tables and blocks weight calculation until both sides are complete', async () => {
+ const user = userEvent.setup();
+ render();
+ await act(async () => {
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+
+ await user.click(screen.getByRole('button', { name: /新增产品/ }));
+ await waitFor(() => screen.getByRole('dialog'));
+
+ const dialog = screen.getByRole('dialog');
+ expect(within(dialog).getAllByText('纱线名称/纱线颜色')).toHaveLength(2);
+ expect(within(dialog).getAllByText('使用配比(合计 10)')).toHaveLength(2);
+ expect(within(dialog).getAllByRole('button', { name: '根据纱线配比计算' })).toHaveLength(1);
+
+ await user.click(within(dialog).getByRole('button', { name: '根据纱线配比计算' }));
+ expect(await within(dialog).findByText('请先添加经线和纬线纱线,并确保两边配比合计均为 10')).toBeInTheDocument();
+ });
+
+ it('submits selected yarn ratio rows as initial batch yarnRatio', async () => {
+ vi.mocked(listYarns).mockResolvedValue({
+ code: 0,
+ msg: 'ok',
+ data: {
+ total: 1,
+ list: [
+ { yarnId: 'yarn-1', yarnName: 'A纱', color: '红', weightGM: '20', supplierId: 'supplier-1' },
+ ],
+ },
+ } as any);
+ const user = userEvent.setup();
+ render();
+ await act(async () => {
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+
+ await user.click(screen.getByRole('button', { name: /新增产品/ }));
+ await waitFor(() => screen.getByRole('dialog'));
+
+ const dialog = screen.getByRole('dialog');
+ await user.type(within(dialog).getByRole('textbox', { name: /产品名称/ }), '测试产品');
+ await waitFor(() => expect(vi.mocked(listYarns)).toHaveBeenCalled());
+
+ await user.click(within(dialog).getByRole('combobox', { name: '添加经线纱线' }));
+ await user.click(await screen.findByText('A纱 / 红'));
+ await clickDialogSubmit(user, dialog);
+
+ await waitFor(() => {
+ expect(vi.mocked(createProduct)).toHaveBeenCalled();
+ });
+ const payload = vi.mocked(createProduct).mock.calls.at(-1)?.[0] as any;
+ const yarnRatio = JSON.parse(payload.initialBatch.yarnRatio);
+ expect(yarnRatio.warp).toEqual([
+ { yarn_id: 'yarn-1', yarn_name: 'A纱', color: '红', ratio: 10 },
+ ]);
+ expect(yarnRatio.weft).toEqual([]);
+ });
+
+ it('rejects product creation when an existing yarn side does not add up to ten', async () => {
+ vi.mocked(listYarns).mockResolvedValue({
+ code: 0,
+ msg: 'ok',
+ data: {
+ total: 1,
+ list: [
+ { yarnId: 'yarn-1', yarnName: 'A纱', color: '红', weightGM: '20', supplierId: 'supplier-1' },
+ ],
+ },
+ } as any);
+ const user = userEvent.setup();
+ render();
+ await act(async () => {
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+
+ await user.click(screen.getByRole('button', { name: /新增产品/ }));
+ await waitFor(() => screen.getByRole('dialog'));
+
+ const dialog = screen.getByRole('dialog');
+ await user.type(within(dialog).getByRole('textbox', { name: /产品名称/ }), '测试产品');
+ await waitFor(() => expect(vi.mocked(listYarns)).toHaveBeenCalled());
+ await user.click(within(dialog).getByRole('combobox', { name: '添加经线纱线' }));
+ await user.click(await screen.findByText('A纱 / 红'));
+
+ const ratioInput = within(dialog).getByRole('spinbutton', { name: '经线A纱使用配比' });
+ await user.click(ratioInput);
+ await user.keyboard('{Control>}a{/Control}9');
+ await clickDialogSubmit(user, dialog);
+
+ await waitFor(() => {
+ expect(screen.getAllByText('经线配比合计需为 10,当前为 9').length).toBeGreaterThan(0);
+ });
+ expect(vi.mocked(createProduct)).not.toHaveBeenCalled();
+ });
+
it('renders Excel export button', async () => {
render();
await act(async () => {
@@ -133,12 +230,7 @@ describe('Inventory / Products page', () => {
const nameInput = within(dialog).getByRole('textbox', { name: /产品名称/ });
await user.type(nameInput, '新布料');
- const buttons = within(dialog).getAllByRole('button');
- const submitBtn = buttons.find((b) => {
- const text = b.textContent?.trim() ?? '';
- return text && !text.includes('×') && !text.includes('✕') && !/取/.test(text) && !/重/.test(text) && !/Cancel/i.test(text);
- });
- if (submitBtn) await user.click(submitBtn);
+ await clickDialogSubmit(user, dialog);
await waitFor(() => {
expect(vi.mocked(createProduct)).toHaveBeenCalled();
diff --git a/src/pages/Inventory/Products/index.tsx b/src/pages/Inventory/Products/index.tsx
index 41a5eea..0a61216 100644
--- a/src/pages/Inventory/Products/index.tsx
+++ b/src/pages/Inventory/Products/index.tsx
@@ -1,6 +1,6 @@
import { PlusOutlined, UploadOutlined, DownloadOutlined, ArrowLeftOutlined } from '@ant-design/icons';
-import { ActionType, ModalForm, ProColumns, ProFormText, ProFormTextArea, ProTable } from '@ant-design/pro-components';
-import { Button, Card, Descriptions, Drawer, Input, InputNumber, List, message, Popconfirm, Space, Tag } from 'antd';
+import { ActionType, ModalForm, ProColumns, ProFormSelect, ProFormText, ProFormTextArea, ProTable } from '@ant-design/pro-components';
+import { Alert, Button, Card, Descriptions, Drawer, Form, Input, InputNumber, List, message, Popconfirm, Segmented, Select, Space, Table, Tag } from 'antd';
import { useRef, useState, useCallback } from 'react';
import {
getProductSummary,
@@ -9,14 +9,47 @@ import {
createProduct,
updateProduct,
deleteProduct,
+ exportProducts,
savePans,
+ listYarns,
+ createYarn,
+ listSuppliers,
+ listProductBatches,
+ createProductBatch,
+ updateProductBatch,
+ deleteProductBatch,
} from '@/services/api';
-import type { ProductSummaryItem, ColorDetailItem, ProductTree, PanInput } from '@/types/api';
+import type { ProductSummaryItem, ColorDetailItem, ProductTree, PanInput, ProductBatchInfo, YarnInfo } from '@/types/api';
type ViewLevel = 'summary' | 'colors' | 'tree';
+type YarnSide = 'warp' | 'weft';
+
+interface YarnRatioRow {
+ key: string;
+ yarnId: string;
+ yarnName: string;
+ yarnColor: string;
+ weightGM: string;
+ ratio: number;
+}
+
+const CREATE_YARN_OPTION = '__create_yarn__';
+const ratioSideLabels: Record = {
+ warp: '经线',
+ weft: '纬线',
+};
+const prominentErrorTextStyle = { color: '#ff1f1f', fontWeight: 600 };
+
+const getRatioTotal = (rows: YarnRatioRow[]) => rows.reduce((sum, row) => sum + Number(row.ratio || 0), 0);
+
+const formatWeight = (value: number) => {
+ const fixed = value.toFixed(4);
+ return fixed.replace(/\.?0+$/, '');
+};
const ProductsPage: React.FC = () => {
const summaryRef = useRef();
+ const [createForm] = Form.useForm();
const [viewLevel, setViewLevel] = useState('summary');
const [selectedProductName, setSelectedProductName] = useState('');
const [selectedProductId, setSelectedProductId] = useState('');
@@ -26,20 +59,306 @@ const ProductsPage: React.FC = () => {
const [editTreeDrawer, setEditTreeDrawer] = useState(false);
const [currentColor, setCurrentColor] = useState(null);
const [panInputs, setPanInputs] = useState([]);
+ const [batches, setBatches] = useState([]);
+ const [batchOpen, setBatchOpen] = useState(false);
+ const [currentBatch, setCurrentBatch] = useState(null);
+ const [yarnOpen, setYarnOpen] = useState(false);
+ const [yarnCreateTarget, setYarnCreateTarget] = useState(null);
+ const [yarnOptions, setYarnOptions] = useState([]);
+ const [warpYarns, setWarpYarns] = useState([]);
+ const [weftYarns, setWeftYarns] = useState([]);
+ const [ratioSubmitError, setRatioSubmitError] = useState('');
+ const [weightCalcError, setWeightCalcError] = useState('');
+ const [unit, setUnit] = useState<'cm' | 'in'>('cm');
+
+ const formatCmSpec = (spec?: string) => {
+ if (!spec || unit === 'cm') return spec;
+ return spec.replace(/(\d+(?:\.\d+)?)\s*cm/gi, (_, n) => `${(Number(n) / 2.54).toFixed(2)}in`);
+ };
+
+ const handleExport = async () => {
+ try {
+ const data = await exportProducts();
+ const blob = data instanceof Blob ? data : new Blob([data as BlobPart]);
+ const url = window.URL.createObjectURL(blob);
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = `products-${Date.now()}.xlsx`;
+ document.body.appendChild(link);
+ link.click();
+ link.remove();
+ window.URL.revokeObjectURL(url);
+ } catch {
+ message.error('导出失败');
+ }
+ };
const loadTree = useCallback(async (productId: string) => {
const res = await getProductTree(productId);
if (res?.code === 0) {
+ const batchRes = await listProductBatches(productId);
+ setBatches(batchRes?.data?.list || []);
setTreeData(res.data);
setSelectedProductId(productId);
setViewLevel('tree');
}
}, []);
+ const loadYarnOptions = useCallback(async (keyWords?: string) => {
+ const res = await listYarns({ page: 1, pageSize: 50, yarnName: keyWords, status: 1 });
+ if (res?.code === 0) {
+ setYarnOptions(res.data?.list || []);
+ }
+ }, []);
+
+ const resetCreateProductState = useCallback(() => {
+ createForm.resetFields();
+ setWarpYarns([]);
+ setWeftYarns([]);
+ setRatioSubmitError('');
+ setWeightCalcError('');
+ setYarnCreateTarget(null);
+ }, [createForm]);
+
+ const handleCreateOpenChange = useCallback((open: boolean) => {
+ setCreateOpen(open);
+ if (open) {
+ resetCreateProductState();
+ loadYarnOptions();
+ } else {
+ resetCreateProductState();
+ }
+ }, [loadYarnOptions, resetCreateProductState]);
+
+ const getYarnRows = useCallback((side: YarnSide) => (
+ side === 'warp' ? warpYarns : weftYarns
+ ), [warpYarns, weftYarns]);
+
+ const setYarnRows = useCallback((side: YarnSide, updater: (rows: YarnRatioRow[]) => YarnRatioRow[]) => {
+ const wrappedUpdater = (rows: YarnRatioRow[]) => updater(rows);
+ if (side === 'warp') {
+ setWarpYarns(wrappedUpdater);
+ return;
+ }
+ setWeftYarns(wrappedUpdater);
+ }, []);
+
+ const clearRatioFeedback = useCallback(() => {
+ setRatioSubmitError('');
+ setWeightCalcError('');
+ }, []);
+
+ const appendYarnRatioRow = useCallback((side: YarnSide, yarn: YarnInfo) => {
+ setYarnRows(side, (rows) => {
+ if (rows.some((row) => row.yarnId === yarn.yarnId)) {
+ return rows;
+ }
+ const remaining = 10 - getRatioTotal(rows);
+ const ratio = remaining > 0 ? Math.min(10, remaining) : 1;
+ return [
+ ...rows,
+ {
+ key: `${side}-${yarn.yarnId}-${Date.now()}`,
+ yarnId: yarn.yarnId,
+ yarnName: yarn.yarnName,
+ yarnColor: yarn.color || '未设置颜色',
+ weightGM: yarn.weightGM,
+ ratio,
+ },
+ ];
+ });
+ clearRatioFeedback();
+ }, [clearRatioFeedback, setYarnRows]);
+
+ const handleSelectYarn = (side: YarnSide, value: string) => {
+ if (value === CREATE_YARN_OPTION) {
+ setYarnCreateTarget(side);
+ setYarnOpen(true);
+ return;
+ }
+ const yarn = yarnOptions.find((item) => item.yarnId === value);
+ if (yarn) {
+ appendYarnRatioRow(side, yarn);
+ }
+ };
+
+ const updateYarnRatio = (side: YarnSide, key: string, value: number | null) => {
+ const nextRatio = Math.max(1, Math.min(10, Math.round(Number(value || 1))));
+ setYarnRows(side, (rows) => rows.map((row) => (
+ row.key === key ? { ...row, ratio: nextRatio } : row
+ )));
+ clearRatioFeedback();
+ };
+
+ const removeYarnRatioRow = (side: YarnSide, key: string) => {
+ setYarnRows(side, (rows) => rows.filter((row) => row.key !== key));
+ clearRatioFeedback();
+ };
+
+ const getRatioError = (side: YarnSide, rows: YarnRatioRow[]) => {
+ if (rows.length === 0) {
+ return '';
+ }
+ if (rows.some((row) => !row.yarnId)) {
+ return `${ratioSideLabels[side]}纱线不能为空`;
+ }
+ if (rows.some((row) => !Number.isInteger(row.ratio) || row.ratio < 1 || row.ratio > 10)) {
+ return `${ratioSideLabels[side]}使用配比必须为 1-10 的整数`;
+ }
+ const total = getRatioTotal(rows);
+ if (total !== 10) {
+ return `${ratioSideLabels[side]}配比合计需为 10,当前为 ${total}`;
+ }
+ return '';
+ };
+
+ const getRatioErrors = () => (
+ [
+ getRatioError('warp', warpYarns),
+ getRatioError('weft', weftYarns),
+ ].filter(Boolean)
+ );
+
+ const serializeYarnRatio = () => {
+ if (warpYarns.length === 0 && weftYarns.length === 0) {
+ return '';
+ }
+ return JSON.stringify({
+ warp: warpYarns.map((row) => ({
+ yarn_id: row.yarnId,
+ yarn_name: row.yarnName,
+ color: row.yarnColor,
+ ratio: row.ratio,
+ })),
+ weft: weftYarns.map((row) => ({
+ yarn_id: row.yarnId,
+ yarn_name: row.yarnName,
+ color: row.yarnColor,
+ ratio: row.ratio,
+ })),
+ });
+ };
+
+ const calculateSideWeight = (rows: YarnRatioRow[]) => rows.reduce((sum, row) => {
+ const weight = Number(row.weightGM || 0);
+ return sum + (Number.isFinite(weight) ? weight * row.ratio / 10 : 0);
+ }, 0);
+
+ const calculateBatchWeight = () => {
+ const ratioErrors = getRatioErrors();
+ if (warpYarns.length === 0 || weftYarns.length === 0) {
+ setWeightCalcError('请先添加经线和纬线纱线,并确保两边配比合计均为 10');
+ return;
+ }
+ if (ratioErrors.length > 0) {
+ setWeightCalcError(ratioErrors.join(';'));
+ return;
+ }
+ const initialBatch = createForm.getFieldValue('initialBatch') || {};
+ createForm.setFieldsValue({
+ initialBatch: {
+ ...initialBatch,
+ warpWeightGM: formatWeight(calculateSideWeight(warpYarns)),
+ weftWeightGM: formatWeight(calculateSideWeight(weftYarns)),
+ },
+ });
+ setWeightCalcError('');
+ message.success('已根据纱线配比计算克重');
+ };
+
+ const renderYarnRatioTable = (side: YarnSide) => {
+ const rows = getYarnRows(side);
+ const label = ratioSideLabels[side];
+ const error = getRatioError(side, rows);
+ const selectedYarnIds = new Set(rows.map((row) => row.yarnId));
+ const options = [
+ { label: `+ 创建纱线`, value: CREATE_YARN_OPTION },
+ ...yarnOptions
+ .filter((yarn) => !selectedYarnIds.has(yarn.yarnId))
+ .map((yarn) => ({
+ label: `${yarn.yarnName} / ${yarn.color || '未设置颜色'}`,
+ value: yarn.yarnId,
+ })),
+ ];
+ const total = getRatioTotal(rows);
+
+ return (
+
+
+ {label}
+
+
+ rowKey="key"
+ size="small"
+ pagination={false}
+ dataSource={rows}
+ locale={{ emptyText: '尚未添加纱线' }}
+ columns={[
+ {
+ title: '纱线名称/纱线颜色',
+ dataIndex: 'yarnName',
+ render: (_, row) => (
+
+ {row.yarnName}
+ / {row.yarnColor}
+
+ ),
+ },
+ {
+ title: '使用配比(合计 10)',
+ dataIndex: 'ratio',
+ width: 180,
+ render: (_, row) => (
+ updateYarnRatio(side, row.key, value)}
+ />
+ ),
+ },
+ {
+ title: '操作',
+ width: 80,
+render: (_, row) => (
+
+),
+ },
+ ]}
+ footer={() => (
+
+ 合计 {total} / 10{rows.length === 0 ? '(未添加)' : ''}
+
+ )}
+ />
+ {error && (
+
+ {error}
+
+ )}
+
+ );
+ };
+
// ── Panel 1: Product Summary ──
const summaryColumns: ProColumns[] = [
{ title: '产品名称', dataIndex: 'productName', ellipsis: true },
- { title: '规格型号', dataIndex: 'spec', search: false },
+ { title: '规格型号', dataIndex: 'spec', search: false, render: (_, r) => formatCmSpec(r.spec) },
{ title: '颜色数', dataIndex: 'colorCount', search: false },
{ title: '总盘数', dataIndex: 'totalPanCount', search: false },
{ title: '总匹数', dataIndex: 'totalBoltCount', search: false },
@@ -57,7 +376,10 @@ const ProductsPage: React.FC = () => {
// ── Panel 2: Color Detail ──
const colorColumns: ProColumns[] = [
{ title: '颜色', dataIndex: 'color' },
- { title: '规格', dataIndex: 'spec', search: false },
+ { title: '色号', dataIndex: 'colorNo', search: false },
+ { title: '产品码', dataIndex: 'productCode', search: false, copyable: true },
+ { title: '规格', dataIndex: 'spec', search: false, render: (_, r) => formatCmSpec(r.spec) },
+ { title: '批次数', dataIndex: 'batchCount', search: false },
{ title: '盘数', dataIndex: 'panCount', search: false },
{ title: '匹数', dataIndex: 'boltCount', search: false },
{ title: '库存(m)', dataIndex: 'totalLengthM', search: false },
@@ -83,14 +405,48 @@ const ProductsPage: React.FC = () => {
return (
- {product.spec}
+ {product.productCode || '-'}
+ {product.colorNo || '-'}
+ {formatCmSpec(product.spec)}
{totalPanCount}
{totalBoltCount}
{totalLengthM}m
+
{ setCurrentBatch(null); setBatchOpen(true); }}>新增批次}>
+ (
+ { setCurrentBatch(batch); setBatchOpen(true); }}>编辑,
+ {
+ const res = await deleteProductBatch(selectedProductId, batch.batchId);
+ if (res?.code === 0) {
+ message.success('删除成功');
+ loadTree(selectedProductId);
+ }
+ }}
+ >
+ 删除
+ ,
+ ]}
+ >
+ {batch.batchNo}{Number(batch.warpWeightGM || 0) + Number(batch.weftWeightGM || 0)}g/m}
+ description={batch.productionProcess || '无'}
+ />
+
+ )}
+ />
+
{pans.map((pan) => (
- {pan.position || '未设置位置'}}>
+ {pan.batchNo || '未归属批次'}{pan.position || '未设置位置'}}>
{
setPanInputs(pans.map((p) => ({
name: p.name,
position: p.position,
+ batchId: p.batchId,
boltLengths: p.bolts.map((b) => b.lengthM),
})));
setEditTreeDrawer(true);
@@ -144,9 +501,10 @@ const ProductsPage: React.FC = () => {
return { data: res?.data?.list || [], success: res?.code === 0, total: res?.data?.list?.length || 0 };
}}
toolBarRender={() => [
- } onClick={() => setCreateOpen(true)}>新增产品,
+ setUnit(value as 'cm' | 'in')} />,
+ } onClick={() => handleCreateOpenChange(true)}>新增产品,
} onClick={() => window.location.assign('/inventory/import')}>Excel导入,
- }>Excel导出,
+ } onClick={handleExport}>Excel导出,
]}
/>
)}
@@ -170,9 +528,24 @@ const ProductsPage: React.FC = () => {
{
- const res = await createProduct({ ...values, salesPrice: String(values.salesPrice || '0') });
+ const ratioErrors = getRatioErrors();
+ if (ratioErrors.length > 0) {
+ const errorText = ratioErrors.join(';');
+ setRatioSubmitError(errorText);
+ message.error(errorText);
+ return false;
+ }
+ const initialBatch = values.initialBatch || {};
+ const yarnRatio = serializeYarnRatio();
+ const productValues = { ...values };
+ const res = await createProduct({
+ ...productValues,
+ salesPrice: String(values.salesPrice || '0'),
+ initialBatch: { ...initialBatch, yarnRatio },
+ });
if (res?.code === 0) { message.success('创建成功'); summaryRef.current?.reload(); return true; }
message.error(res?.msg || '创建失败');
return false;
@@ -180,9 +553,28 @@ const ProductsPage: React.FC = () => {
>
-
+
+
-
+ 纱线配比
+ {ratioSubmitError && (
+ {ratioSubmitError}} style={{ marginBottom: 12 }} />
+ )}
+ {renderYarnRatioTable('warp')}
+ {renderYarnRatioTable('weft')}
+ 克重数据
+
+ {weightCalcError && (
+
+ {weightCalcError}
+
+ )}
+
+
+
+
{/* Edit product modal */}
@@ -194,6 +586,8 @@ const ProductsPage: React.FC = () => {
productName: currentColor.productName,
spec: currentColor.spec,
color: currentColor.color,
+ colorNo: currentColor.colorNo,
+ productCode: currentColor.productCode,
salesPrice: currentColor.salesPrice,
} : undefined}
onFinish={async (values) => {
@@ -205,12 +599,95 @@ const ProductsPage: React.FC = () => {
}}
>
+
+
+ {
+ const res = currentBatch
+ ? await updateProductBatch(selectedProductId, currentBatch.batchId, values)
+ : await createProductBatch(selectedProductId, values);
+ if (res?.code === 0) {
+ message.success('保存成功');
+ loadTree(selectedProductId);
+ return true;
+ }
+ message.error(res?.msg || '保存失败');
+ return false;
+ }}
+ >
+ {currentBatch && }
+
+
+
+
+
+
+
+ {
+ setYarnOpen(open);
+ if (!open) setYarnCreateTarget(null);
+ }}
+ onFinish={async (values) => {
+ const res = await createYarn(values);
+ if (res?.code === 0) {
+ const createdYarnId = res.data?.id || '';
+ const createdYarn: YarnInfo = {
+ yarnId: createdYarnId,
+ yarnName: values.yarnName,
+ color: values.color || '原纱本色',
+ weightGM: values.weightGM,
+ supplierId: values.supplierId,
+ dyeFactory: values.dyeFactory,
+ remark: values.remark,
+ };
+ if (createdYarn.yarnId) {
+ setYarnOptions((options) => [
+ createdYarn,
+ ...options.filter((item) => item.yarnId !== createdYarn.yarnId),
+ ]);
+ if (yarnCreateTarget) {
+ appendYarnRatioRow(yarnCreateTarget, createdYarn);
+ }
+ } else {
+ loadYarnOptions();
+ }
+ message.success('创建成功');
+ return true;
+ }
+ message.error(res?.msg || '创建失败');
+ return false;
+ }}
+ >
+
+
+
+ {
+ const res = await listSuppliers({ page: 1, pageSize: 50, supplierName: keyWords, status: 1 });
+ return (res?.data?.list || []).map((supplier) => ({ label: supplier.supplierName, value: supplier.supplierId }));
+ }}
+ fieldProps={{ showSearch: true }}
+ />
+
+
+
+
{/* Edit Pans/Bolts Drawer */}
{
}}
style={{ width: 120 }}
/>
+