feat: 添加产品工具, 重构产品 #31
|
|
@ -7,6 +7,7 @@
|
|||
"format": "prettier --cache --write .",
|
||||
"postinstall": "max setup",
|
||||
"openapi2ts": "openapi2ts",
|
||||
"fix:openapi2ts": "sed -i '' 's/\r$//' /Users/zksu/Developer/work/workcode/web/node_modules/@umijs/openapi/dist/cli.js",
|
||||
"prepare": "husky",
|
||||
"setup": "max setup",
|
||||
"start": "npm run dev"
|
||||
|
|
@ -30,6 +31,7 @@
|
|||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/react": "^18.0.33",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"code-inspector-plugin": "^1.2.10",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,58 @@
|
|||
|
||||
import { ProFormSelect } from '@ant-design/pro-components';
|
||||
import { useState } from 'react';
|
||||
import { productcontrollerGetattributeall, productcontrollerGetattributelist } from '@/servers/api/product';
|
||||
|
||||
interface AttributeFormItemProps {
|
||||
dictName: string;
|
||||
name: string;
|
||||
label: string;
|
||||
isTag?: boolean;
|
||||
}
|
||||
|
||||
const fetchDictOptions = async (dictName: string, keyword?: string) => {
|
||||
const { data } = await productcontrollerGetattributelist({ dictName, name: keyword });
|
||||
return (data?.items || []).map((item: any) => ({ label: item.name, value: item.name }));
|
||||
};
|
||||
|
||||
const AttributeFormItem: React.FC<AttributeFormItemProps> = ({ dictName, name, label, isTag = false }) => {
|
||||
const [options, setOptions] = useState<{ label: string; value: string }[]>([]);
|
||||
|
||||
if (isTag) {
|
||||
return (
|
||||
<ProFormSelect
|
||||
name={name}
|
||||
width="lg"
|
||||
label={label}
|
||||
placeholder={`请输入或选择${label}`}
|
||||
fieldProps={{
|
||||
mode: 'tags',
|
||||
showSearch: true,
|
||||
filterOption: false,
|
||||
onSearch: async (val) => {
|
||||
const opts = await fetchDictOptions(dictName, val);
|
||||
setOptions(opts);
|
||||
},
|
||||
}}
|
||||
request={async () => {
|
||||
const opts = await fetchDictOptions(dictName);
|
||||
setOptions(opts);
|
||||
return opts;
|
||||
}}
|
||||
options={options}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ProFormSelect
|
||||
name={name}
|
||||
width="lg"
|
||||
label={label}
|
||||
placeholder={`请选择${label}`}
|
||||
request={() => fetchDictOptions(dictName)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default AttributeFormItem;
|
||||
|
|
@ -1,2 +1,2 @@
|
|||
// 中文注释:限定允许管理的字典名称集合
|
||||
export const allowedDictNames = new Set(['brand', 'strength', 'flavor', 'size', 'humidity']);
|
||||
export const attributes = new Set(['brand', 'strength', 'flavor', 'size', 'humidity','category']);
|
||||
|
|
@ -6,7 +6,7 @@ import React, { useEffect, useState } from 'react';
|
|||
|
||||
const { Sider, Content } = Layout;
|
||||
|
||||
import { allowedDictNames } from './consts';
|
||||
import { attributes } from './consts';
|
||||
|
||||
const AttributePage: React.FC = () => {
|
||||
// 中文注释:左侧字典列表状态
|
||||
|
|
@ -30,7 +30,7 @@ const AttributePage: React.FC = () => {
|
|||
try {
|
||||
const res = await request('/dict/list', { params: { title } });
|
||||
// 中文注释:条件判断,过滤只保留 allowedDictNames 中的字典
|
||||
const filtered = (res || []).filter((d: any) => allowedDictNames.has(d?.name));
|
||||
const filtered = (res || []).filter((d: any) => attributes.has(d?.name));
|
||||
setDicts(filtered);
|
||||
} catch (error) {
|
||||
message.error('获取字典列表失败');
|
||||
|
|
|
|||
|
|
@ -1,19 +1,15 @@
|
|||
import {
|
||||
productcontrollerCreateproduct,
|
||||
productcontrollerCompatsizeall,
|
||||
productcontrollerDeleteproduct,
|
||||
productcontrollerCompatbrandall,
|
||||
productcontrollerCompatflavorsall,
|
||||
productcontrollerGetproductlist,
|
||||
productcontrollerCompatstrengthall,
|
||||
productcontrollerUpdateproductnamecn,
|
||||
productcontrollerUpdateproduct,
|
||||
productcontrollerGetproductcomponents,
|
||||
productcontrollerSetproductcomponents,
|
||||
productcontrollerAutobindcomponents,
|
||||
productcontrollerGetattributeall,
|
||||
productcontrollerGetattributelist,
|
||||
} from '@/servers/api/product';
|
||||
import { stockcontrollerGetstocks } from '@/servers/api/stock';
|
||||
import { request } from '@umijs/max';
|
||||
import { templatecontrollerRendertemplate } from '@/servers/api/template';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
|
|
@ -31,8 +27,10 @@ import {
|
|||
ProTable,
|
||||
} from '@ant-design/pro-components';
|
||||
import { App, Button, Popconfirm, Tag, Upload } from 'antd';
|
||||
import { allowedDictNames } from '@/pages/Product/Attribute/consts';
|
||||
import React, { useRef, useState } from 'react';
|
||||
|
||||
import AttributeFormItem from '@/pages/Product/Attribute/components/AttributeFormItem';
|
||||
|
||||
import React, { useMemo, useRef, useState } from 'react';
|
||||
const capitalize = (s: string) => s.charAt(0).toLocaleUpperCase() + s.slice(1);
|
||||
// TODO
|
||||
interface DictItem {
|
||||
|
|
@ -73,39 +71,34 @@ const NameCn: React.FC<{
|
|||
};
|
||||
|
||||
const AttributesCell: React.FC<{ record: any }> = ({ record }) => {
|
||||
const items: { key: string; value: string }[] = [];
|
||||
// 中文注释:按允许的属性集合收集展示值
|
||||
if (allowedDictNames.has('brand') && record?.brand?.name) items.push({ key: '品牌', value: record.brand.name });
|
||||
if (allowedDictNames.has('strength') && record?.strength?.name) items.push({ key: '强度', value: record.strength.name });
|
||||
if (allowedDictNames.has('flavor') && record?.flavors?.name) items.push({ key: '口味', value: record.flavors.name });
|
||||
if (allowedDictNames.has('size') && record?.size?.name) items.push({ key: '规格', value: record.size.name });
|
||||
if (allowedDictNames.has('humidity') && record?.humidity) items.push({ key: '湿度', value: record.humidity });
|
||||
return (
|
||||
<div>
|
||||
{items.length ? items.map((it, idx) => (
|
||||
{(record.attributes || []).map((data: any, idx: number) => (
|
||||
<Tag key={idx} color="purple" style={{ marginBottom: 4 }}>
|
||||
{it.key}: {it.value}
|
||||
{data?.dict?.name}: {data.name}
|
||||
</Tag>
|
||||
)) : <span>-</span>}
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
const ComponentsCell: React.FC<{ productId: number }> = ({ productId }) => {
|
||||
const [items, setItems] = React.useState<any[]>([]);
|
||||
const [components, setComponents] = React.useState<any[]>([]);
|
||||
React.useEffect(() => {
|
||||
(async () => {
|
||||
const { data = [] } = await productcontrollerGetproductcomponents({ id: productId });
|
||||
setItems(data || []);
|
||||
setComponents(data || []);
|
||||
})();
|
||||
}, [productId]);
|
||||
return (
|
||||
<div>
|
||||
{items && items.length ? (
|
||||
items.map((c: any) => (
|
||||
<Tag key={c.id} color="blue" style={{ marginBottom: 4 }}>
|
||||
{(c.stock && c.stock.productSku) || `#${c.stockId}`} × {c.quantity}(库存:{c.stock ? c.stock.quantity : '-'})
|
||||
{components && components.length ? (
|
||||
components.map((component: any) => (
|
||||
<Tag key={component.id} color="blue" style={{ marginBottom: 4 }}>
|
||||
{(component.productSku) || `#${component.id}`} × {component.quantity}(库存:
|
||||
{component.stock?.map((s: any) => `${s.name}:${s.quantity}`).join(', ') || '-'}
|
||||
)
|
||||
</Tag>
|
||||
))
|
||||
) : (
|
||||
|
|
@ -295,12 +288,8 @@ const CreateForm: React.FC<{
|
|||
const { message } = App.useApp();
|
||||
// 表单引用
|
||||
const formRef = useRef<ProFormInstance>();
|
||||
// 状态:存储品牌、强度、口味、规格的选项(label 使用标题,value 使用名称)
|
||||
const [brandOptions, setBrandOptions] = useState<{ label: string; value: string }[]>([]);
|
||||
const [strengthOptions, setStrengthOptions] = useState<{ label: string; value: string }[]>([]);
|
||||
const [flavorOptions, setFlavorOptions] = useState<{ label: string; value: string }[]>([]);
|
||||
const [sizeOptions, setSizeOptions] = useState<{ label: string; value: string }[]>([]);
|
||||
const [humidityOptions, setHumidityOptions] = useState<{ label: string; value: string }[]>([]);
|
||||
const [productType, setProductType] = useState<'single' | 'bundle' | null>(null);
|
||||
|
||||
|
||||
/**
|
||||
* @description 生成 SKU
|
||||
|
|
@ -309,9 +298,9 @@ const CreateForm: React.FC<{
|
|||
try {
|
||||
// 从表单引用中获取当前表单的值
|
||||
const formValues = formRef.current?.getFieldsValue();
|
||||
const { humidityValues, brandValues, strengthValues, flavorsValues } = formValues;
|
||||
const { humidityValues, brandValues, strengthValues, flavorValues } = formValues;
|
||||
// 检查是否所有必需的字段都已选择
|
||||
if (!brandValues?.length || !strengthValues?.length || !flavorsValues?.length || !humidityValues?.length) {
|
||||
if (!brandValues?.length || !strengthValues?.length || !flavorValues?.length || !humidityValues?.length) {
|
||||
message.warning('请先选择品牌、强度、口味和干湿');
|
||||
return;
|
||||
}
|
||||
|
|
@ -319,7 +308,7 @@ const CreateForm: React.FC<{
|
|||
// 所选值(用于 SKU 模板传入 name)
|
||||
const brandName: string = String(brandValues[0]);
|
||||
const strengthName: string = String(strengthValues[0]);
|
||||
const flavorName: string = String(flavorsValues[0]);
|
||||
const flavorName: string = String(flavorValues[0]);
|
||||
const humidityName: string = String(humidityValues[0]);
|
||||
|
||||
// 调用模板渲染API来生成SKU
|
||||
|
|
@ -350,21 +339,20 @@ const CreateForm: React.FC<{
|
|||
try {
|
||||
// 从表单引用中获取当前表单的值
|
||||
const formValues = formRef.current?.getFieldsValue();
|
||||
const { humidityValues, brandValues, strengthValues, flavorsValues } = formValues;
|
||||
const { humidityValues, brandValues, strengthValues, flavorValues } = formValues;
|
||||
// 检查是否所有必需的字段都已选择
|
||||
if (!brandValues?.length || !strengthValues?.length || !flavorsValues?.length || !humidityValues?.length) {
|
||||
if (!brandValues?.length || !strengthValues?.length || !flavorValues?.length || !humidityValues?.length) {
|
||||
message.warning('请先选择品牌、强度、口味和干湿');
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取标题(label),若为新输入值则使用原值作为标题
|
||||
const brandName: string = String(brandValues[0]);
|
||||
const strengthName: string = String(strengthValues[0]);
|
||||
const flavorName: string = String(flavorsValues[0]);
|
||||
const flavorName: string = String(flavorValues[0]);
|
||||
const humidityName: string = String(humidityValues[0]);
|
||||
const brandTitle = brandOptions.find(i => i.value === brandName)?.label || brandName;
|
||||
const strengthTitle = strengthOptions.find(i => i.value === strengthName)?.label || strengthName;
|
||||
const flavorTitle = flavorOptions.find(i => i.value === flavorName)?.label || flavorName;
|
||||
const brandTitle = brandName;
|
||||
const strengthTitle = strengthName;
|
||||
const flavorTitle = flavorName;
|
||||
|
||||
// 调用模板渲染API来生成产品名称
|
||||
const { message: msg, data: rendered, success } = await templatecontrollerRendertemplate(
|
||||
|
|
@ -401,24 +389,32 @@ const CreateForm: React.FC<{
|
|||
drawerProps={{
|
||||
destroyOnHidden: true,
|
||||
}}
|
||||
onValuesChange={async (changedValues) => {
|
||||
if ('sku' in changedValues) {
|
||||
const sku = changedValues.sku;
|
||||
if (sku) {
|
||||
const { data } = await stockcontrollerGetstocks({ productSku: sku } as any);
|
||||
if (data && data.items && data.items.length > 0) {
|
||||
setProductType('single');
|
||||
} else {
|
||||
setProductType('bundle');
|
||||
}
|
||||
} else {
|
||||
setProductType(null);
|
||||
}
|
||||
}
|
||||
}}
|
||||
onFinish={async (values) => {
|
||||
// 中文注释:组装 attributes(支持输入新值,按标题传入创建/绑定)
|
||||
const toArray = (v: any) => (Array.isArray(v) ? v : v ? [v] : []);
|
||||
const brandValues = toArray((values as any).brandValues);
|
||||
const strengthValues = toArray((values as any).strengthValues);
|
||||
const flavorsValues = toArray((values as any).flavorsValues);
|
||||
const sizeValues = toArray((values as any).sizeValues);
|
||||
const humidityValues = toArray((values as any).humidityValues);
|
||||
const mapWithLabel = (vals: string[], opts: { label: string; value: string }[], dictName: string) => (
|
||||
vals.map(v => ({ dictName, title: (opts.find(o => o.value === v)?.label || v) }))
|
||||
);
|
||||
|
||||
const attributes = [
|
||||
...mapWithLabel(brandValues, brandOptions, 'brand'),
|
||||
...mapWithLabel(strengthValues, strengthOptions, 'strength'),
|
||||
...mapWithLabel(flavorsValues, flavorOptions, 'flavor'),
|
||||
...mapWithLabel(sizeValues, sizeOptions, 'size'),
|
||||
...mapWithLabel(humidityValues, humidityOptions, 'humidity'),
|
||||
].filter(Boolean);
|
||||
...(values.brandValues || []).map((v: string) => ({ dictName: 'brand', name: v })),
|
||||
...(values.strengthValues || []).map((v: string) => ({ dictName: 'strength', name: v })),
|
||||
...(values.flavorValues || []).map((v: string) => ({ dictName: 'flavor', name: v })),
|
||||
...(values.humidityValues || []).map((v: string) => ({ dictName: 'humidity', name: v })),
|
||||
...(values.sizeValues || []).map((v: string) => ({ dictName: 'size', name: v })),
|
||||
...(values.category ? [{ dictName: 'category', name: values.category }] : []),
|
||||
];
|
||||
const payload: any = {
|
||||
name: (values as any).name,
|
||||
description: (values as any).description,
|
||||
|
|
@ -438,129 +434,7 @@ const CreateForm: React.FC<{
|
|||
return false;
|
||||
}}
|
||||
>
|
||||
{/* 品牌(可搜索、可新增) */}
|
||||
<ProFormSelect
|
||||
name="brandValues"
|
||||
width="lg"
|
||||
label="产品品牌"
|
||||
placeholder="请输入或选择产品品牌"
|
||||
fieldProps={{
|
||||
mode: 'tags',
|
||||
showSearch: true,
|
||||
filterOption: false,
|
||||
onSearch: async (val) => {
|
||||
const items = await productcontrollerGetattributelist({ dictName: 'brand', name: val, current: 1, pageSize: 20 } as any);
|
||||
const options = (items?.data?.items || []).map((it: any) => ({ label: it.title, value: it.name }));
|
||||
setBrandOptions(options);
|
||||
},
|
||||
}}
|
||||
request={async () => {
|
||||
const items = await productcontrollerGetattributelist({ dictName: 'brand', current: 1, pageSize: 20 } as any);
|
||||
const options = (items?.data?.items || []).map((it: any) => ({ label: it.title, value: it.name }));
|
||||
setBrandOptions(options);
|
||||
return options;
|
||||
}}
|
||||
options={brandOptions}
|
||||
rules={[{ required: true, message: '请选择产品品牌' }]}
|
||||
/>
|
||||
{/* 强度(可搜索、可新增) */}
|
||||
<ProFormSelect
|
||||
name="strengthValues"
|
||||
width="lg"
|
||||
label="强度"
|
||||
placeholder="请输入或选择强度"
|
||||
fieldProps={{
|
||||
mode: 'tags',
|
||||
showSearch: true,
|
||||
filterOption: false,
|
||||
onSearch: async (val) => {
|
||||
const items = await productcontrollerGetattributelist({ dictName: 'strength', name: val, current: 1, pageSize: 20 } as any);
|
||||
const options = (items?.data?.items || []).map((it: any) => ({ label: it.title, value: it.name }));
|
||||
setStrengthOptions(options);
|
||||
},
|
||||
}}
|
||||
request={async () => {
|
||||
const items = await productcontrollerGetattributelist({ dictName: 'strength', current: 1, pageSize: 20 } as any);
|
||||
const options = (items?.data?.items || []).map((it: any) => ({ label: it.title, value: it.name }));
|
||||
setStrengthOptions(options);
|
||||
return options;
|
||||
}}
|
||||
options={strengthOptions}
|
||||
rules={[{ required: true, message: '请选择强度' }]}
|
||||
/>
|
||||
{/* 口味(可搜索、可新增) */}
|
||||
<ProFormSelect
|
||||
name="flavorsValues"
|
||||
width="lg"
|
||||
label="口味"
|
||||
placeholder="请输入或选择口味"
|
||||
fieldProps={{
|
||||
mode: 'tags',
|
||||
showSearch: true,
|
||||
filterOption: false,
|
||||
onSearch: async (val) => {
|
||||
const items = await productcontrollerGetattributelist({ dictName: 'flavor', name: val, current: 1, pageSize: 20 } as any);
|
||||
const options = (items?.data?.items || []).map((it: any) => ({ label: it.title, value: it.name }));
|
||||
setFlavorOptions(options);
|
||||
},
|
||||
}}
|
||||
request={async () => {
|
||||
const items = await productcontrollerGetattributelist({ dictName: 'flavor', current: 1, pageSize: 20 } as any);
|
||||
const options = (items?.data?.items || []).map((it: any) => ({ label: it.title, value: it.name }));
|
||||
setFlavorOptions(options);
|
||||
return options;
|
||||
}}
|
||||
options={flavorOptions}
|
||||
rules={[{ required: true, message: '请选择口味' }]}
|
||||
/>
|
||||
<ProFormSelect
|
||||
name="sizeValues"
|
||||
width="lg"
|
||||
label="规格"
|
||||
placeholder="请输入或选择规格"
|
||||
fieldProps={{
|
||||
mode: 'tags',
|
||||
showSearch: true,
|
||||
filterOption: false,
|
||||
onSearch: async (val) => {
|
||||
const items = await productcontrollerGetattributelist({ dictName: 'size', name: val, current: 1, pageSize: 20 } as any);
|
||||
const options = (items?.data?.items || []).map((it: any) => ({ label: it.title, value: it.name }));
|
||||
setSizeOptions(options);
|
||||
},
|
||||
}}
|
||||
request={async () => {
|
||||
const items = await productcontrollerGetattributelist({ dictName: 'size', current: 1, pageSize: 20 } as any);
|
||||
const options = (items?.data?.items || []).map((it: any) => ({ label: it.title, value: it.name }));
|
||||
setSizeOptions(options);
|
||||
return options;
|
||||
}}
|
||||
options={sizeOptions}
|
||||
rules={[{ required: false }]}
|
||||
/>
|
||||
<ProFormSelect
|
||||
name="humidityValues"
|
||||
width="lg"
|
||||
label="干湿"
|
||||
placeholder="请输入或选择干湿"
|
||||
fieldProps={{
|
||||
mode: 'tags',
|
||||
showSearch: true,
|
||||
filterOption: false,
|
||||
onSearch: async (val) => {
|
||||
const items = await productcontrollerGetattributelist({ dictName: 'humidity', name: val, current: 1, pageSize: 20 } as any);
|
||||
const options = (items?.data?.items || []).map((it: any) => ({ label: it.title, value: it.name }));
|
||||
setHumidityOptions(options);
|
||||
},
|
||||
}}
|
||||
request={async () => {
|
||||
const items = await productcontrollerGetattributelist({ dictName: 'humidity', current: 1, pageSize: 20 } as any);
|
||||
const options = (items?.data?.items || []).map((it: any) => ({ label: it.title, value: it.name }));
|
||||
setHumidityOptions(options);
|
||||
return options;
|
||||
}}
|
||||
options={humidityOptions}
|
||||
rules={[{ required: true, message: '请选择干湿' }]}
|
||||
/>
|
||||
|
||||
<ProForm.Group>
|
||||
<ProFormText
|
||||
name="sku"
|
||||
|
|
@ -572,6 +446,11 @@ const CreateForm: React.FC<{
|
|||
<Button style={{ marginTop: '32px' }} onClick={handleGenerateSku}>
|
||||
自动生成
|
||||
</Button>
|
||||
{productType && (
|
||||
<Tag style={{ marginTop: '32px' }} color={productType === 'single' ? 'green' : 'orange'}>
|
||||
{productType === 'single' ? '单品' : '套装'}
|
||||
</Tag>
|
||||
)}
|
||||
</ProForm.Group>
|
||||
<ProForm.Group>
|
||||
<ProFormText
|
||||
|
|
@ -585,6 +464,12 @@ const CreateForm: React.FC<{
|
|||
自动生成
|
||||
</Button>
|
||||
</ProForm.Group>
|
||||
<AttributeFormItem dictName="brand" name="brandValues" label="产品品牌" isTag />
|
||||
<AttributeFormItem dictName="strength" name="strengthValues" label="强度" isTag />
|
||||
<AttributeFormItem dictName="flavor" name="flavorValues" label="口味" isTag />
|
||||
<AttributeFormItem dictName="humidity" name="humidityValues" label="干湿" isTag />
|
||||
<AttributeFormItem dictName="size" name="sizeValues" label="大小" isTag />
|
||||
<AttributeFormItem dictName="category" name="category" label="分类" isTag />
|
||||
<ProFormText
|
||||
name="price"
|
||||
label="价格"
|
||||
|
|
@ -617,77 +502,89 @@ const EditForm: React.FC<{
|
|||
}> = ({ tableRef, record }) => {
|
||||
const { message } = App.useApp();
|
||||
const formRef = useRef<ProFormInstance>();
|
||||
// 中文注释:各属性的选择项(使用 {label,value},value 用 name,便于 tags 输入匹配)
|
||||
const [brandOptions, setBrandOptions] = useState<{ label: string; value: string }[]>([]);
|
||||
const [strengthOptions, setStrengthOptions] = useState<{ label: string; value: string }[]>([]);
|
||||
const [flavorOptions, setFlavorOptions] = useState<{ label: string; value: string }[]>([]);
|
||||
const [humidityOptions, setHumidityOptions] = useState<{ label: string; value: string }[]>([]);
|
||||
const [components, setComponents] = useState<{ stockId: number; quantity: number }[]>([]);
|
||||
|
||||
// 中文注释:通用远程选项加载器(支持关键词搜索)
|
||||
const fetchDictOptions = async (dictName: string, keyword?: string) => {
|
||||
// 条件判断:构造查询参数
|
||||
const params: any = { dictName, current: 1, pageSize: 20 };
|
||||
if (keyword) params.name = keyword;
|
||||
const { data } = await productcontrollerGetattributelist(params);
|
||||
const items = data?.items || [];
|
||||
// 中文注释:统一转为 {label,value},value 使用 name
|
||||
return items.map((it: DictItem) => ({ label: it.name, value: it.name }));
|
||||
};
|
||||
const [components, setComponents] = useState<{ productSku: string; quantity: number }[]>([]);
|
||||
const [productType, setProductType] = useState<'single' | 'bundle' | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
// 中文注释:加载干湿选项
|
||||
(async () => {
|
||||
const { data = [] } = await productcontrollerGetattributeall({ dictName: 'humidity' } as any);
|
||||
setHumidityOptions((data || []).map((i: any) => ({ label: i.name, value: i.name })));
|
||||
})();
|
||||
// 中文注释:加载当前产品的组成
|
||||
(async () => {
|
||||
const { data = [] } = await productcontrollerGetproductcomponents({ id: (record as any).id });
|
||||
const items = (data || []).map((c: any) => ({ stockId: c.stockId, quantity: c.quantity }));
|
||||
setComponents(items);
|
||||
const items = (data || []).map((c: any) => ({ productSku: c.productSku, quantity: c.quantity }));
|
||||
setComponents(items as any);
|
||||
formRef.current?.setFieldsValue({ components: items });
|
||||
})();
|
||||
}, []);
|
||||
|
||||
// 检查产品类型
|
||||
if (record.sku) {
|
||||
const { data: stockData } = await stockcontrollerGetstocks({ productSku: record.sku } as any);
|
||||
if (stockData && stockData.items && stockData.items.length > 0) {
|
||||
setProductType('single');
|
||||
} else {
|
||||
setProductType('bundle');
|
||||
}
|
||||
}
|
||||
})();
|
||||
}, [record]);
|
||||
const initialValues = useMemo(() => {
|
||||
if (!record) return {};
|
||||
const attributes = record.attributes || [];
|
||||
|
||||
const attributesGroupedByName = (attributes as any[]).reduce((group, attr) => {
|
||||
const dictName = attr.dict.name;
|
||||
if (!group[dictName]) {
|
||||
group[dictName] = [];
|
||||
}
|
||||
group[dictName].push(attr.name);
|
||||
return group;
|
||||
}, {} as Record<string, string[]>);
|
||||
|
||||
const values: any = {
|
||||
...record,
|
||||
brandValues: attributesGroupedByName.brand || [],
|
||||
strengthValues: attributesGroupedByName.strength || [],
|
||||
flavorValues: attributesGroupedByName.flavor || [],
|
||||
humidityValues: attributesGroupedByName.humidity || [],
|
||||
sizeValues: attributesGroupedByName.size || [],
|
||||
category: attributesGroupedByName.category?.[0] || '',
|
||||
};
|
||||
return values;
|
||||
}, [record]);
|
||||
return (
|
||||
<DrawerForm<any>
|
||||
formRef={formRef}
|
||||
title="编辑"
|
||||
trigger={<Button type="link">编辑</Button>}
|
||||
initialValues={{
|
||||
name: record.name,
|
||||
sku: record.sku,
|
||||
description: record.description,
|
||||
price: record.price,
|
||||
promotionPrice: record.promotionPrice,
|
||||
components,
|
||||
attributes: record.attributes || [],
|
||||
// 中文注释:为可搜索可新增的选择框提供初始值(使用 name 作为值)
|
||||
brandValues: record.brand?.name ? [record.brand.name] : [],
|
||||
strengthValues: record.strength?.name ? [record.strength.name] : [],
|
||||
flavorsValues: record.flavors?.name ? [record.flavors.name] : [],
|
||||
initialValues={initialValues}
|
||||
onValuesChange={async (changedValues) => {
|
||||
if ('sku' in changedValues) {
|
||||
const sku = changedValues.sku;
|
||||
if (sku) {
|
||||
const { data } = await stockcontrollerGetstocks({ productSku: sku } as any);
|
||||
if (data && data.items && data.items.length > 0) {
|
||||
setProductType('single');
|
||||
} else {
|
||||
setProductType('bundle');
|
||||
}
|
||||
} else {
|
||||
setProductType(null);
|
||||
}
|
||||
}
|
||||
}}
|
||||
onFinish={async (values) => {
|
||||
// 中文注释:组装 attributes(若选择了则发送)
|
||||
const toArray = (v: any) => (Array.isArray(v) ? v : v ? [v] : []);
|
||||
const brandValues = toArray((values as any).brandValues);
|
||||
const strengthValues = toArray((values as any).strengthValues);
|
||||
const flavorsValues = toArray((values as any).flavorsValues);
|
||||
const attrs = [
|
||||
// 条件判断:将 tags 值转为 { dictName, title }
|
||||
...brandValues.map((t: string) => ({ dictName: 'brand', title: String(t).trim() })),
|
||||
...strengthValues.map((t: string) => ({ dictName: 'strength', title: String(t).trim() })),
|
||||
...flavorsValues.map((t: string) => ({ dictName: 'flavor', title: String(t).trim() })),
|
||||
values.humidity ? { dictName: 'humidity', name: values.humidity } : null,
|
||||
].filter(Boolean);
|
||||
const attributes = [
|
||||
...(values.brandValues || []).map((v: string) => ({ dictName: 'brand', name: v })),
|
||||
...(values.strengthValues || []).map((v: string) => ({ dictName: 'strength', name: v })),
|
||||
...(values.flavorValues || []).map((v: string) => ({ dictName: 'flavor', name: v })),
|
||||
...(values.humidityValues || []).map((v: string) => ({ dictName: 'humidity', name: v })),
|
||||
...(values.sizeValues || []).map((v: string) => ({ dictName: 'size', name: v })),
|
||||
...(values.category ? [{ dictName: 'category', name: values.category }] : []),
|
||||
];
|
||||
const updatePayload: any = {
|
||||
name: values.name,
|
||||
sku: values.sku,
|
||||
description: values.description,
|
||||
price: values.price,
|
||||
promotionPrice: values.promotionPrice,
|
||||
...(attrs.length ? { attributes: attrs } : {}),
|
||||
attributes,
|
||||
};
|
||||
const { success, message: errMsg } = await productcontrollerUpdateproduct(
|
||||
{ id: (record as any).id },
|
||||
|
|
@ -698,9 +595,9 @@ const EditForm: React.FC<{
|
|||
const items = (values as any)?.components || [];
|
||||
if (Array.isArray(items)) {
|
||||
const payloadItems = items
|
||||
.filter((i: any) => i && i.stockId && i.quantity && i.quantity > 0)
|
||||
.map((i: any) => ({ stockId: Number(i.stockId), quantity: Number(i.quantity) }));
|
||||
await productcontrollerSetproductcomponents({ id: (record as any).id }, { items: payloadItems });
|
||||
.filter((i: any) => i && i.productSku && i.quantity && i.quantity > 0)
|
||||
.map((i: any) => ({ productSku: i.productSku, quantity: Number(i.quantity) }));
|
||||
await productcontrollerSetproductcomponents({ id: (record as any).id }, { items: payloadItems as any });
|
||||
}
|
||||
message.success('更新成功');
|
||||
tableRef.current?.reloadAndRest?.();
|
||||
|
|
@ -719,6 +616,11 @@ const EditForm: React.FC<{
|
|||
placeholder="请输入SKU"
|
||||
rules={[{ required: true, message: '请输入SKU' }]}
|
||||
/>
|
||||
{productType && (
|
||||
<Tag style={{ marginTop: '32px' }} color={productType === 'single' ? 'green' : 'orange'}>
|
||||
{productType === 'single' ? '单品' : '套装'}
|
||||
</Tag>
|
||||
)}
|
||||
<ProFormText
|
||||
name="name"
|
||||
label="名称"
|
||||
|
|
@ -727,79 +629,12 @@ const EditForm: React.FC<{
|
|||
rules={[{ required: true, message: '请输入名称' }]}
|
||||
/>
|
||||
</ProForm.Group>
|
||||
{/* 中文注释:品牌(可搜索、可新增) */}
|
||||
<ProFormSelect
|
||||
name="brandValues"
|
||||
width="lg"
|
||||
label="产品品牌"
|
||||
placeholder="请输入或选择产品品牌"
|
||||
fieldProps={{
|
||||
mode: 'tags',
|
||||
showSearch: true,
|
||||
filterOption: false,
|
||||
onSearch: async (val) => {
|
||||
const options = await fetchDictOptions('brand', val);
|
||||
setBrandOptions(options);
|
||||
},
|
||||
}}
|
||||
request={async () => {
|
||||
const options = await fetchDictOptions('brand');
|
||||
setBrandOptions(options);
|
||||
return options;
|
||||
}}
|
||||
options={brandOptions}
|
||||
/>
|
||||
{/* 中文注释:强度(可搜索、可新增) */}
|
||||
<ProFormSelect
|
||||
name="strengthValues"
|
||||
width="lg"
|
||||
label="强度"
|
||||
placeholder="请输入或选择强度"
|
||||
fieldProps={{
|
||||
mode: 'tags',
|
||||
showSearch: true,
|
||||
filterOption: false,
|
||||
onSearch: async (val) => {
|
||||
const options = await fetchDictOptions('strength', val);
|
||||
setStrengthOptions(options);
|
||||
},
|
||||
}}
|
||||
request={async () => {
|
||||
const options = await fetchDictOptions('strength');
|
||||
setStrengthOptions(options);
|
||||
return options;
|
||||
}}
|
||||
options={strengthOptions}
|
||||
/>
|
||||
{/* 中文注释:口味(可搜索、可新增) */}
|
||||
<ProFormSelect
|
||||
name="flavorsValues"
|
||||
width="lg"
|
||||
label="口味"
|
||||
placeholder="请输入或选择口味"
|
||||
fieldProps={{
|
||||
mode: 'tags',
|
||||
showSearch: true,
|
||||
filterOption: false,
|
||||
onSearch: async (val) => {
|
||||
const options = await fetchDictOptions('flavor', val);
|
||||
setFlavorOptions(options);
|
||||
},
|
||||
}}
|
||||
request={async () => {
|
||||
const options = await fetchDictOptions('flavor');
|
||||
setFlavorOptions(options);
|
||||
return options;
|
||||
}}
|
||||
options={flavorOptions}
|
||||
/>
|
||||
<ProFormSelect
|
||||
name="humidity"
|
||||
width="lg"
|
||||
label="干湿"
|
||||
placeholder="请选择干湿"
|
||||
options={humidityOptions}
|
||||
/>
|
||||
<AttributeFormItem dictName="brand" name="brandValues" label="产品品牌" isTag />
|
||||
<AttributeFormItem dictName="strength" name="strengthValues" label="强度" isTag />
|
||||
<AttributeFormItem dictName="flavor" name="flavorValues" label="口味" isTag />
|
||||
<AttributeFormItem dictName="humidity" name="humidityValues" label="干湿" isTag />
|
||||
<AttributeFormItem dictName="size" name="sizeValues" label="大小" isTag />
|
||||
<AttributeFormItem dictName="category" name="category" label="分类" />
|
||||
<ProFormText
|
||||
name="price"
|
||||
label="价格"
|
||||
|
|
@ -828,6 +663,7 @@ const EditForm: React.FC<{
|
|||
hidden
|
||||
/>
|
||||
<Button
|
||||
disabled={productType === 'single'}
|
||||
onClick={async () => {
|
||||
await productcontrollerAutobindcomponents({ id: (record as any).id });
|
||||
const { data = [] } = await productcontrollerGetproductcomponents({ id: (record as any).id });
|
||||
|
|
@ -838,12 +674,13 @@ const EditForm: React.FC<{
|
|||
自动绑定组成(同 SKU)
|
||||
</Button>
|
||||
</ProForm.Group>
|
||||
<ProForm.Item hidden={productType === 'single'}>
|
||||
<ProFormList
|
||||
name="components"
|
||||
label="组成项"
|
||||
creatorButtonProps={{ position: 'bottom', creatorButtonText: '新增组成项' }}
|
||||
itemRender={({ listDom, action }) => (
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<div style={{ marginBottom: 8, display: 'flex', flexDirection: 'row', alignItems: 'end' }}>
|
||||
{listDom}
|
||||
{action}
|
||||
</div>
|
||||
|
|
@ -851,11 +688,11 @@ const EditForm: React.FC<{
|
|||
>
|
||||
<ProForm.Group>
|
||||
<ProFormText
|
||||
name="stockId"
|
||||
label="库存ID"
|
||||
name="productSku"
|
||||
label="库存SKU"
|
||||
width="md"
|
||||
placeholder="请输入库存ID"
|
||||
rules={[{ required: true, message: '请输入库存ID' }]}
|
||||
placeholder="请输入库存SKU"
|
||||
rules={[{ required: true, message: '请输入库存SKU' }]}
|
||||
/>
|
||||
<ProFormText
|
||||
name="quantity"
|
||||
|
|
@ -866,6 +703,7 @@ const EditForm: React.FC<{
|
|||
/>
|
||||
</ProForm.Group>
|
||||
</ProFormList>
|
||||
</ProForm.Item>
|
||||
</DrawerForm >
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -23,27 +23,31 @@ const ListPage: React.FC = () => {
|
|||
});
|
||||
}, []);
|
||||
const columns: ProColumns<API.StockDTO>[] = [
|
||||
{
|
||||
title: 'SKU',
|
||||
dataIndex: 'productSku',
|
||||
hideInSearch: true,
|
||||
sorter: true,
|
||||
},
|
||||
{
|
||||
title: '产品名称',
|
||||
dataIndex: 'productName',
|
||||
sorter: true,
|
||||
},
|
||||
{
|
||||
title: '中文名',
|
||||
dataIndex: 'productNameCn',
|
||||
hideInSearch: true,
|
||||
},
|
||||
{
|
||||
title: 'SKU',
|
||||
dataIndex: 'productSku',
|
||||
hideInSearch: true,
|
||||
},
|
||||
|
||||
...points?.map((point: API.StockPoint) => ({
|
||||
title: point.name,
|
||||
dataIndex: `point_${point.name}`,
|
||||
dataIndex: `point_${point.id}`,
|
||||
hideInSearch: true,
|
||||
sorter: true,
|
||||
render(_: any, record: API.StockDTO) {
|
||||
const quantity = record.stockPoint?.find(
|
||||
(item) => item.id === point.id,
|
||||
(item: any) => item.id === point.id,
|
||||
)?.quantity;
|
||||
return quantity || 0;
|
||||
},
|
||||
|
|
@ -74,8 +78,25 @@ const ListPage: React.FC = () => {
|
|||
actionRef={actionRef}
|
||||
rowKey="id"
|
||||
request={async (params) => {
|
||||
const { data, success } = await stockcontrollerGetstocks(params);
|
||||
const { sorter, ...rest } = params;
|
||||
const queryParams: any = { ...rest };
|
||||
|
||||
if (sorter) {
|
||||
const order: Record<string, 'asc' | 'desc'> = {};
|
||||
for (const key in sorter) {
|
||||
const value = sorter[key];
|
||||
if (value === 'ascend') {
|
||||
order[key] = 'asc';
|
||||
} else if (value === 'descend') {
|
||||
order[key] = 'desc';
|
||||
}
|
||||
}
|
||||
if (Object.keys(order).length > 0) {
|
||||
queryParams.order = order;
|
||||
}
|
||||
}
|
||||
|
||||
const { data, success } = await stockcontrollerGetstocks(queryParams);
|
||||
return {
|
||||
total: data?.total || 0,
|
||||
data: data?.items || [],
|
||||
|
|
@ -96,12 +117,13 @@ const ListPage: React.FC = () => {
|
|||
const headers = ['产品名', 'SKU', ...points.map((p) => p.name)];
|
||||
|
||||
// 数据行
|
||||
const rows = (data?.items || []).map((item) => {
|
||||
const stockMap = new Map(
|
||||
item.stockPoint.map((sp) => [sp.id, sp.quantity]),
|
||||
const rows = (data?.items || []).map((item: API.StockDTO) => {
|
||||
// 处理stockPoint可能为undefined的情况,并正确定义类型
|
||||
const stockMap = new Map<number, number>(
|
||||
(item.stockPoint || []).map((sp: any) => [sp.id || 0, sp.quantity || 0]),
|
||||
);
|
||||
const stockRow = points.map((p) => stockMap.get(p.id) || 0);
|
||||
return [item.productName, item.productSku, ...stockRow];
|
||||
const stockRow = points.map((p) => stockMap.get(p.id || 0) || 0);
|
||||
return [item.productName || '', item.productSku || '', ...stockRow];
|
||||
});
|
||||
|
||||
// 导出
|
||||
|
|
|
|||
|
|
@ -263,6 +263,16 @@ export async function productcontrollerCompatbrands(
|
|||
});
|
||||
}
|
||||
|
||||
/** 此处后端没有提供注释 GET /product/export */
|
||||
export async function productcontrollerExportproductscsv(options?: {
|
||||
[key: string]: any;
|
||||
}) {
|
||||
return request<any>('/product/export', {
|
||||
method: 'GET',
|
||||
...(options || {}),
|
||||
});
|
||||
}
|
||||
|
||||
/** 此处后端没有提供注释 GET /product/flavors */
|
||||
export async function productcontrollerCompatflavors(
|
||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
||||
|
|
@ -340,6 +350,45 @@ export async function productcontrollerCompatflavorsall(options?: {
|
|||
});
|
||||
}
|
||||
|
||||
/** 此处后端没有提供注释 POST /product/import */
|
||||
export async function productcontrollerImportproductscsv(
|
||||
body: {},
|
||||
files?: File[],
|
||||
options?: { [key: string]: any },
|
||||
) {
|
||||
const formData = new FormData();
|
||||
|
||||
if (files) {
|
||||
files.forEach((f) => formData.append('files', f || ''));
|
||||
}
|
||||
|
||||
Object.keys(body).forEach((ele) => {
|
||||
const item = (body as any)[ele];
|
||||
|
||||
if (item !== undefined && item !== null) {
|
||||
if (typeof item === 'object' && !(item instanceof File)) {
|
||||
if (item instanceof Array) {
|
||||
item.forEach((f) => formData.append(ele, f || ''));
|
||||
} else {
|
||||
formData.append(
|
||||
ele,
|
||||
new Blob([JSON.stringify(item)], { type: 'application/json' }),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
formData.append(ele, item);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return request<any>('/product/import', {
|
||||
method: 'POST',
|
||||
data: formData,
|
||||
requestType: 'form',
|
||||
...(options || {}),
|
||||
});
|
||||
}
|
||||
|
||||
/** 此处后端没有提供注释 GET /product/list */
|
||||
export async function productcontrollerGetproductlist(
|
||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ export async function stockcontrollerGetstocks(
|
|||
method: 'GET',
|
||||
params: {
|
||||
...params,
|
||||
order: undefined,
|
||||
...params['order'],
|
||||
},
|
||||
...(options || {}),
|
||||
});
|
||||
|
|
@ -31,6 +33,20 @@ export async function stockcontrollerCanceltransfer(
|
|||
});
|
||||
}
|
||||
|
||||
/** 此处后端没有提供注释 GET /stock/has/${param0} */
|
||||
export async function stockcontrollerHasstock(
|
||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
||||
params: API.stockcontrollerHasstockParams,
|
||||
options?: { [key: string]: any },
|
||||
) {
|
||||
const { sku: param0, ...queryParams } = params;
|
||||
return request<API.BooleanRes>(`/stock/has/${param0}`, {
|
||||
method: 'GET',
|
||||
params: { ...queryParams },
|
||||
...(options || {}),
|
||||
});
|
||||
}
|
||||
|
||||
/** 此处后端没有提供注释 POST /stock/lostTransfer/${param0} */
|
||||
export async function stockcontrollerLosttransfer(
|
||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
||||
|
|
|
|||
|
|
@ -84,6 +84,8 @@ declare namespace API {
|
|||
price?: number;
|
||||
/** 促销价格 */
|
||||
promotionPrice?: number;
|
||||
/** 商品类型 */
|
||||
type?: 'simple' | 'bundle';
|
||||
};
|
||||
|
||||
type CreatePurchaseOrderDTO = {
|
||||
|
|
@ -688,7 +690,6 @@ declare namespace API {
|
|||
};
|
||||
|
||||
type Product = {
|
||||
attributes: never[];
|
||||
/** ID */
|
||||
id: number;
|
||||
/** 产品名称 */
|
||||
|
|
@ -718,8 +719,8 @@ declare namespace API {
|
|||
};
|
||||
|
||||
type ProductComponentItemDTO = {
|
||||
/** 库存记录ID */
|
||||
stockId?: number;
|
||||
/** 组件 SKU */
|
||||
sku?: string;
|
||||
/** 组成数量 */
|
||||
quantity?: number;
|
||||
};
|
||||
|
|
@ -895,7 +896,8 @@ declare namespace API {
|
|||
type ProductStockComponent = {
|
||||
id?: number;
|
||||
productId?: number;
|
||||
stockId?: number;
|
||||
/** 组件所关联的 SKU */
|
||||
productSku?: string;
|
||||
/** 组成数量 */
|
||||
quantity?: number;
|
||||
/** 创建时间 */
|
||||
|
|
@ -1071,6 +1073,10 @@ declare namespace API {
|
|||
/** 每页大小 */
|
||||
pageSize?: number;
|
||||
productName?: string;
|
||||
/** 按库存点ID排序 */
|
||||
sortPointId?: number;
|
||||
/** 排序对象,格式如 { productName: "asc", productSku: "desc" } */
|
||||
order?: Record<string, any>;
|
||||
};
|
||||
|
||||
type QueryStockRecordDTO = {
|
||||
|
|
@ -1334,6 +1340,14 @@ declare namespace API {
|
|||
/** 每页大小 */
|
||||
pageSize?: number;
|
||||
productName?: string;
|
||||
/** 按库存点ID排序 */
|
||||
sortPointId?: number;
|
||||
/** 排序对象,格式如 { productName: "asc", productSku: "desc" } */
|
||||
order?: Record<string, any>;
|
||||
};
|
||||
|
||||
type stockcontrollerHasstockParams = {
|
||||
sku: string;
|
||||
};
|
||||
|
||||
type stockcontrollerLosttransferParams = {
|
||||
|
|
@ -1619,6 +1633,8 @@ declare namespace API {
|
|||
promotionPrice?: number;
|
||||
/** 属性列表 */
|
||||
attributes?: any[];
|
||||
/** 商品类型 */
|
||||
type?: 'simple' | 'bundle';
|
||||
};
|
||||
|
||||
type UpdatePurchaseOrderDTO = {
|
||||
|
|
|
|||
Loading…
Reference in New Issue