feat(产品): 重构产品属性管理并添加分类支持

重构产品属性管理模块,将属性集合从allowedDictNames重命名为attributes并添加category分类
新增AttributeFormItem组件统一处理属性表单字段
优化产品列表和编辑表单,支持属性标签显示和分类管理
添加产品类型识别功能,区分单品和套装
完善库存列表排序和导出功能
This commit is contained in:
tikkhun 2025-11-30 16:00:06 +08:00
parent 0a6ea0396f
commit b2575a11fd
10 changed files with 355 additions and 354 deletions

View File

@ -7,6 +7,7 @@
"format": "prettier --cache --write .", "format": "prettier --cache --write .",
"postinstall": "max setup", "postinstall": "max setup",
"openapi2ts": "openapi2ts", "openapi2ts": "openapi2ts",
"fix:openapi2ts": "sed -i '' 's/\r$//' /Users/zksu/Developer/work/workcode/web/node_modules/@umijs/openapi/dist/cli.js",
"prepare": "husky", "prepare": "husky",
"setup": "max setup", "setup": "max setup",
"start": "npm run dev" "start": "npm run dev"
@ -30,6 +31,7 @@
"xlsx": "^0.18.5" "xlsx": "^0.18.5"
}, },
"devDependencies": { "devDependencies": {
"@types/file-saver": "^2.0.7",
"@types/react": "^18.0.33", "@types/react": "^18.0.33",
"@types/react-dom": "^18.0.11", "@types/react-dom": "^18.0.11",
"code-inspector-plugin": "^1.2.10", "code-inspector-plugin": "^1.2.10",

View File

@ -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;

View File

@ -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']);

View File

@ -6,7 +6,7 @@ import React, { useEffect, useState } from 'react';
const { Sider, Content } = Layout; const { Sider, Content } = Layout;
import { allowedDictNames } from './consts'; import { attributes } from './consts';
const AttributePage: React.FC = () => { const AttributePage: React.FC = () => {
// 中文注释:左侧字典列表状态 // 中文注释:左侧字典列表状态
@ -30,7 +30,7 @@ const AttributePage: React.FC = () => {
try { try {
const res = await request('/dict/list', { params: { title } }); const res = await request('/dict/list', { params: { title } });
// 中文注释:条件判断,过滤只保留 allowedDictNames 中的字典 // 中文注释:条件判断,过滤只保留 allowedDictNames 中的字典
const filtered = (res || []).filter((d: any) => allowedDictNames.has(d?.name)); const filtered = (res || []).filter((d: any) => attributes.has(d?.name));
setDicts(filtered); setDicts(filtered);
} catch (error) { } catch (error) {
message.error('获取字典列表失败'); message.error('获取字典列表失败');

View File

@ -1,19 +1,15 @@
import { import {
productcontrollerCreateproduct, productcontrollerCreateproduct,
productcontrollerCompatsizeall,
productcontrollerDeleteproduct, productcontrollerDeleteproduct,
productcontrollerCompatbrandall,
productcontrollerCompatflavorsall,
productcontrollerGetproductlist, productcontrollerGetproductlist,
productcontrollerCompatstrengthall,
productcontrollerUpdateproductnamecn, productcontrollerUpdateproductnamecn,
productcontrollerUpdateproduct, productcontrollerUpdateproduct,
productcontrollerGetproductcomponents, productcontrollerGetproductcomponents,
productcontrollerSetproductcomponents, productcontrollerSetproductcomponents,
productcontrollerAutobindcomponents, productcontrollerAutobindcomponents,
productcontrollerGetattributeall,
productcontrollerGetattributelist, productcontrollerGetattributelist,
} from '@/servers/api/product'; } from '@/servers/api/product';
import { stockcontrollerGetstocks } from '@/servers/api/stock';
import { request } from '@umijs/max'; import { request } from '@umijs/max';
import { templatecontrollerRendertemplate } from '@/servers/api/template'; import { templatecontrollerRendertemplate } from '@/servers/api/template';
import { PlusOutlined } from '@ant-design/icons'; import { PlusOutlined } from '@ant-design/icons';
@ -31,8 +27,10 @@ import {
ProTable, ProTable,
} from '@ant-design/pro-components'; } from '@ant-design/pro-components';
import { App, Button, Popconfirm, Tag, Upload } from 'antd'; 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); const capitalize = (s: string) => s.charAt(0).toLocaleUpperCase() + s.slice(1);
// TODO // TODO
interface DictItem { interface DictItem {
@ -73,39 +71,34 @@ const NameCn: React.FC<{
}; };
const AttributesCell: React.FC<{ record: any }> = ({ record }) => { 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 ( return (
<div> <div>
{items.length ? items.map((it, idx) => ( {(record.attributes || []).map((data: any, idx: number) => (
<Tag key={idx} color="purple" style={{ marginBottom: 4 }}> <Tag key={idx} color="purple" style={{ marginBottom: 4 }}>
{it.key}: {it.value} {data?.dict?.name}: {data.name}
</Tag> </Tag>
)) : <span>-</span>} ))}
</div> </div>
); );
}; };
const ComponentsCell: React.FC<{ productId: number }> = ({ productId }) => { const ComponentsCell: React.FC<{ productId: number }> = ({ productId }) => {
const [items, setItems] = React.useState<any[]>([]); const [components, setComponents] = React.useState<any[]>([]);
React.useEffect(() => { React.useEffect(() => {
(async () => { (async () => {
const { data = [] } = await productcontrollerGetproductcomponents({ id: productId }); const { data = [] } = await productcontrollerGetproductcomponents({ id: productId });
setItems(data || []); setComponents(data || []);
})(); })();
}, [productId]); }, [productId]);
return ( return (
<div> <div>
{items && items.length ? ( {components && components.length ? (
items.map((c: any) => ( components.map((component: any) => (
<Tag key={c.id} color="blue" style={{ marginBottom: 4 }}> <Tag key={component.id} color="blue" style={{ marginBottom: 4 }}>
{(c.stock && c.stock.productSku) || `#${c.stockId}`} × {c.quantity}{c.stock ? c.stock.quantity : '-'} {(component.productSku) || `#${component.id}`} × {component.quantity}
{component.stock?.map((s: any) => `${s.name}:${s.quantity}`).join(', ') || '-'}
</Tag> </Tag>
)) ))
) : ( ) : (
@ -295,12 +288,8 @@ const CreateForm: React.FC<{
const { message } = App.useApp(); const { message } = App.useApp();
// 表单引用 // 表单引用
const formRef = useRef<ProFormInstance>(); const formRef = useRef<ProFormInstance>();
// 状态存储品牌、强度、口味、规格的选项label 使用标题value 使用名称) const [productType, setProductType] = useState<'single' | 'bundle' | null>(null);
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 }[]>([]);
/** /**
* @description SKU * @description SKU
@ -309,9 +298,9 @@ const CreateForm: React.FC<{
try { try {
// 从表单引用中获取当前表单的值 // 从表单引用中获取当前表单的值
const formValues = formRef.current?.getFieldsValue(); 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('请先选择品牌、强度、口味和干湿'); message.warning('请先选择品牌、强度、口味和干湿');
return; return;
} }
@ -319,7 +308,7 @@ const CreateForm: React.FC<{
// 所选值(用于 SKU 模板传入 name // 所选值(用于 SKU 模板传入 name
const brandName: string = String(brandValues[0]); const brandName: string = String(brandValues[0]);
const strengthName: string = String(strengthValues[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 humidityName: string = String(humidityValues[0]);
// 调用模板渲染API来生成SKU // 调用模板渲染API来生成SKU
@ -350,21 +339,20 @@ const CreateForm: React.FC<{
try { try {
// 从表单引用中获取当前表单的值 // 从表单引用中获取当前表单的值
const formValues = formRef.current?.getFieldsValue(); 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('请先选择品牌、强度、口味和干湿'); message.warning('请先选择品牌、强度、口味和干湿');
return; return;
} }
// 获取标题label若为新输入值则使用原值作为标题
const brandName: string = String(brandValues[0]); const brandName: string = String(brandValues[0]);
const strengthName: string = String(strengthValues[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 humidityName: string = String(humidityValues[0]);
const brandTitle = brandOptions.find(i => i.value === brandName)?.label || brandName; const brandTitle = brandName;
const strengthTitle = strengthOptions.find(i => i.value === strengthName)?.label || strengthName; const strengthTitle = strengthName;
const flavorTitle = flavorOptions.find(i => i.value === flavorName)?.label || flavorName; const flavorTitle = flavorName;
// 调用模板渲染API来生成产品名称 // 调用模板渲染API来生成产品名称
const { message: msg, data: rendered, success } = await templatecontrollerRendertemplate( const { message: msg, data: rendered, success } = await templatecontrollerRendertemplate(
@ -386,7 +374,7 @@ const CreateForm: React.FC<{
message.error(`生成失败: ${error.message}`); message.error(`生成失败: ${error.message}`);
} }
}; };
// TODO 可以输入brand等 // TODO 可以输入brand等
return ( return (
<DrawerForm<any> <DrawerForm<any>
title="新建" title="新建"
@ -401,24 +389,32 @@ const CreateForm: React.FC<{
drawerProps={{ drawerProps={{
destroyOnHidden: true, 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) => { onFinish={async (values) => {
// 中文注释:组装 attributes支持输入新值按标题传入创建/绑定) // 中文注释:组装 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 = [ const attributes = [
...mapWithLabel(brandValues, brandOptions, 'brand'), ...(values.brandValues || []).map((v: string) => ({ dictName: 'brand', name: v })),
...mapWithLabel(strengthValues, strengthOptions, 'strength'), ...(values.strengthValues || []).map((v: string) => ({ dictName: 'strength', name: v })),
...mapWithLabel(flavorsValues, flavorOptions, 'flavor'), ...(values.flavorValues || []).map((v: string) => ({ dictName: 'flavor', name: v })),
...mapWithLabel(sizeValues, sizeOptions, 'size'), ...(values.humidityValues || []).map((v: string) => ({ dictName: 'humidity', name: v })),
...mapWithLabel(humidityValues, humidityOptions, 'humidity'), ...(values.sizeValues || []).map((v: string) => ({ dictName: 'size', name: v })),
].filter(Boolean); ...(values.category ? [{ dictName: 'category', name: values.category }] : []),
];
const payload: any = { const payload: any = {
name: (values as any).name, name: (values as any).name,
description: (values as any).description, description: (values as any).description,
@ -438,129 +434,7 @@ const CreateForm: React.FC<{
return false; 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> <ProForm.Group>
<ProFormText <ProFormText
name="sku" name="sku"
@ -572,6 +446,11 @@ const CreateForm: React.FC<{
<Button style={{ marginTop: '32px' }} onClick={handleGenerateSku}> <Button style={{ marginTop: '32px' }} onClick={handleGenerateSku}>
</Button> </Button>
{productType && (
<Tag style={{ marginTop: '32px' }} color={productType === 'single' ? 'green' : 'orange'}>
{productType === 'single' ? '单品' : '套装'}
</Tag>
)}
</ProForm.Group> </ProForm.Group>
<ProForm.Group> <ProForm.Group>
<ProFormText <ProFormText
@ -585,6 +464,12 @@ const CreateForm: React.FC<{
</Button> </Button>
</ProForm.Group> </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 <ProFormText
name="price" name="price"
label="价格" label="价格"
@ -617,77 +502,89 @@ const EditForm: React.FC<{
}> = ({ tableRef, record }) => { }> = ({ tableRef, record }) => {
const { message } = App.useApp(); const { message } = App.useApp();
const formRef = useRef<ProFormInstance>(); const formRef = useRef<ProFormInstance>();
// 中文注释:各属性的选择项(使用 {label,value}value 用 name便于 tags 输入匹配) const [components, setComponents] = useState<{ productSku: string; quantity: number }[]>([]);
const [brandOptions, setBrandOptions] = useState<{ label: string; value: string }[]>([]); const [productType, setProductType] = useState<'single' | 'bundle' | null>(null);
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 }));
};
React.useEffect(() => { React.useEffect(() => {
// 中文注释:加载干湿选项
(async () => {
const { data = [] } = await productcontrollerGetattributeall({ dictName: 'humidity' } as any);
setHumidityOptions((data || []).map((i: any) => ({ label: i.name, value: i.name })));
})();
// 中文注释:加载当前产品的组成 // 中文注释:加载当前产品的组成
(async () => { (async () => {
const { data = [] } = await productcontrollerGetproductcomponents({ id: (record as any).id }); const { data = [] } = await productcontrollerGetproductcomponents({ id: (record as any).id });
const items = (data || []).map((c: any) => ({ stockId: c.stockId, quantity: c.quantity })); const items = (data || []).map((c: any) => ({ productSku: c.productSku, quantity: c.quantity }));
setComponents(items); setComponents(items as any);
formRef.current?.setFieldsValue({ components: items }); 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 ( return (
<DrawerForm<any> <DrawerForm<any>
formRef={formRef} formRef={formRef}
title="编辑" title="编辑"
trigger={<Button type="link"></Button>} trigger={<Button type="link"></Button>}
initialValues={{ initialValues={initialValues}
name: record.name, onValuesChange={async (changedValues) => {
sku: record.sku, if ('sku' in changedValues) {
description: record.description, const sku = changedValues.sku;
price: record.price, if (sku) {
promotionPrice: record.promotionPrice, const { data } = await stockcontrollerGetstocks({ productSku: sku } as any);
components, if (data && data.items && data.items.length > 0) {
attributes: record.attributes || [], setProductType('single');
// 中文注释:为可搜索可新增的选择框提供初始值(使用 name 作为值) } else {
brandValues: record.brand?.name ? [record.brand.name] : [], setProductType('bundle');
strengthValues: record.strength?.name ? [record.strength.name] : [], }
flavorsValues: record.flavors?.name ? [record.flavors.name] : [], } else {
setProductType(null);
}
}
}} }}
onFinish={async (values) => { onFinish={async (values) => {
// 中文注释:组装 attributes若选择了则发送 const attributes = [
const toArray = (v: any) => (Array.isArray(v) ? v : v ? [v] : []); ...(values.brandValues || []).map((v: string) => ({ dictName: 'brand', name: v })),
const brandValues = toArray((values as any).brandValues); ...(values.strengthValues || []).map((v: string) => ({ dictName: 'strength', name: v })),
const strengthValues = toArray((values as any).strengthValues); ...(values.flavorValues || []).map((v: string) => ({ dictName: 'flavor', name: v })),
const flavorsValues = toArray((values as any).flavorsValues); ...(values.humidityValues || []).map((v: string) => ({ dictName: 'humidity', name: v })),
const attrs = [ ...(values.sizeValues || []).map((v: string) => ({ dictName: 'size', name: v })),
// 条件判断:将 tags 值转为 { dictName, title } ...(values.category ? [{ dictName: 'category', name: values.category }] : []),
...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 updatePayload: any = { const updatePayload: any = {
name: values.name, name: values.name,
sku: values.sku, sku: values.sku,
description: values.description, description: values.description,
price: values.price, price: values.price,
promotionPrice: values.promotionPrice, promotionPrice: values.promotionPrice,
...(attrs.length ? { attributes: attrs } : {}), attributes,
}; };
const { success, message: errMsg } = await productcontrollerUpdateproduct( const { success, message: errMsg } = await productcontrollerUpdateproduct(
{ id: (record as any).id }, { id: (record as any).id },
@ -698,9 +595,9 @@ const EditForm: React.FC<{
const items = (values as any)?.components || []; const items = (values as any)?.components || [];
if (Array.isArray(items)) { if (Array.isArray(items)) {
const payloadItems = items const payloadItems = items
.filter((i: any) => i && i.stockId && i.quantity && i.quantity > 0) .filter((i: any) => i && i.productSku && i.quantity && i.quantity > 0)
.map((i: any) => ({ stockId: Number(i.stockId), quantity: Number(i.quantity) })); .map((i: any) => ({ productSku: i.productSku, quantity: Number(i.quantity) }));
await productcontrollerSetproductcomponents({ id: (record as any).id }, { items: payloadItems }); await productcontrollerSetproductcomponents({ id: (record as any).id }, { items: payloadItems as any });
} }
message.success('更新成功'); message.success('更新成功');
tableRef.current?.reloadAndRest?.(); tableRef.current?.reloadAndRest?.();
@ -719,6 +616,11 @@ const EditForm: React.FC<{
placeholder="请输入SKU" placeholder="请输入SKU"
rules={[{ required: true, message: '请输入SKU' }]} rules={[{ required: true, message: '请输入SKU' }]}
/> />
{productType && (
<Tag style={{ marginTop: '32px' }} color={productType === 'single' ? 'green' : 'orange'}>
{productType === 'single' ? '单品' : '套装'}
</Tag>
)}
<ProFormText <ProFormText
name="name" name="name"
label="名称" label="名称"
@ -727,79 +629,12 @@ const EditForm: React.FC<{
rules={[{ required: true, message: '请输入名称' }]} rules={[{ required: true, message: '请输入名称' }]}
/> />
</ProForm.Group> </ProForm.Group>
{/* 中文注释:品牌(可搜索、可新增) */} <AttributeFormItem dictName="brand" name="brandValues" label="产品品牌" isTag />
<ProFormSelect <AttributeFormItem dictName="strength" name="strengthValues" label="强度" isTag />
name="brandValues" <AttributeFormItem dictName="flavor" name="flavorValues" label="口味" isTag />
width="lg" <AttributeFormItem dictName="humidity" name="humidityValues" label="干湿" isTag />
label="产品品牌" <AttributeFormItem dictName="size" name="sizeValues" label="大小" isTag />
placeholder="请输入或选择产品品牌" <AttributeFormItem dictName="category" name="category" label="分类" />
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}
/>
<ProFormText <ProFormText
name="price" name="price"
label="价格" label="价格"
@ -828,6 +663,7 @@ const EditForm: React.FC<{
hidden hidden
/> />
<Button <Button
disabled={productType === 'single'}
onClick={async () => { onClick={async () => {
await productcontrollerAutobindcomponents({ id: (record as any).id }); await productcontrollerAutobindcomponents({ id: (record as any).id });
const { data = [] } = await productcontrollerGetproductcomponents({ id: (record as any).id }); const { data = [] } = await productcontrollerGetproductcomponents({ id: (record as any).id });
@ -838,34 +674,36 @@ const EditForm: React.FC<{
SKU SKU
</Button> </Button>
</ProForm.Group> </ProForm.Group>
<ProFormList <ProForm.Item hidden={productType === 'single'}>
name="components" <ProFormList
label="组成项" name="components"
creatorButtonProps={{ position: 'bottom', creatorButtonText: '新增组成项' }} label="组成项"
itemRender={({ listDom, action }) => ( creatorButtonProps={{ position: 'bottom', creatorButtonText: '新增组成项' }}
<div style={{ marginBottom: 8 }}> itemRender={({ listDom, action }) => (
{listDom} <div style={{ marginBottom: 8, display: 'flex', flexDirection: 'row', alignItems: 'end' }}>
{action} {listDom}
</div> {action}
)} </div>
> )}
<ProForm.Group> >
<ProFormText <ProForm.Group>
name="stockId" <ProFormText
label="库存ID" name="productSku"
width="md" label="库存SKU"
placeholder="请输入库存ID" width="md"
rules={[{ required: true, message: '请输入库存ID' }]} placeholder="请输入库存SKU"
/> rules={[{ required: true, message: '请输入库存SKU' }]}
<ProFormText />
name="quantity" <ProFormText
label="数量" name="quantity"
width="md" label="数量"
placeholder="请输入数量" width="md"
rules={[{ required: true, message: '请输入数量' }]} placeholder="请输入数量"
/> rules={[{ required: true, message: '请输入数量' }]}
</ProForm.Group> />
</ProFormList> </ProForm.Group>
</DrawerForm> </ProFormList>
</ProForm.Item>
</DrawerForm >
); );
}; };

View File

View File

@ -23,27 +23,31 @@ const ListPage: React.FC = () => {
}); });
}, []); }, []);
const columns: ProColumns<API.StockDTO>[] = [ const columns: ProColumns<API.StockDTO>[] = [
{
title: 'SKU',
dataIndex: 'productSku',
hideInSearch: true,
sorter: true,
},
{ {
title: '产品名称', title: '产品名称',
dataIndex: 'productName', dataIndex: 'productName',
sorter: true,
}, },
{ {
title: '中文名', title: '中文名',
dataIndex: 'productNameCn', dataIndex: 'productNameCn',
hideInSearch: true, hideInSearch: true,
}, },
{
title: 'SKU',
dataIndex: 'productSku',
hideInSearch: true,
},
...points?.map((point: API.StockPoint) => ({ ...points?.map((point: API.StockPoint) => ({
title: point.name, title: point.name,
dataIndex: `point_${point.name}`, dataIndex: `point_${point.id}`,
hideInSearch: true, hideInSearch: true,
sorter: true,
render(_: any, record: API.StockDTO) { render(_: any, record: API.StockDTO) {
const quantity = record.stockPoint?.find( const quantity = record.stockPoint?.find(
(item) => item.id === point.id, (item: any) => item.id === point.id,
)?.quantity; )?.quantity;
return quantity || 0; return quantity || 0;
}, },
@ -74,8 +78,25 @@ const ListPage: React.FC = () => {
actionRef={actionRef} actionRef={actionRef}
rowKey="id" rowKey="id"
request={async (params) => { 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 { return {
total: data?.total || 0, total: data?.total || 0,
data: data?.items || [], data: data?.items || [],
@ -96,12 +117,13 @@ const ListPage: React.FC = () => {
const headers = ['产品名', 'SKU', ...points.map((p) => p.name)]; const headers = ['产品名', 'SKU', ...points.map((p) => p.name)];
// 数据行 // 数据行
const rows = (data?.items || []).map((item) => { const rows = (data?.items || []).map((item: API.StockDTO) => {
const stockMap = new Map( // 处理stockPoint可能为undefined的情况并正确定义类型
item.stockPoint.map((sp) => [sp.id, sp.quantity]), 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); const stockRow = points.map((p) => stockMap.get(p.id || 0) || 0);
return [item.productName, item.productSku, ...stockRow]; return [item.productName || '', item.productSku || '', ...stockRow];
}); });
// 导出 // 导出

View File

@ -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 */ /** 此处后端没有提供注释 GET /product/flavors */
export async function productcontrollerCompatflavors( export async function productcontrollerCompatflavors(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象) // 叠加生成的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 */ /** 此处后端没有提供注释 GET /product/list */
export async function productcontrollerGetproductlist( export async function productcontrollerGetproductlist(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象) // 叠加生成的Param类型 (非body参数swagger默认没有生成对象)

View File

@ -12,6 +12,8 @@ export async function stockcontrollerGetstocks(
method: 'GET', method: 'GET',
params: { params: {
...params, ...params,
order: undefined,
...params['order'],
}, },
...(options || {}), ...(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} */ /** 此处后端没有提供注释 POST /stock/lostTransfer/${param0} */
export async function stockcontrollerLosttransfer( export async function stockcontrollerLosttransfer(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象) // 叠加生成的Param类型 (非body参数swagger默认没有生成对象)

View File

@ -84,6 +84,8 @@ declare namespace API {
price?: number; price?: number;
/** 促销价格 */ /** 促销价格 */
promotionPrice?: number; promotionPrice?: number;
/** 商品类型 */
type?: 'simple' | 'bundle';
}; };
type CreatePurchaseOrderDTO = { type CreatePurchaseOrderDTO = {
@ -688,7 +690,6 @@ declare namespace API {
}; };
type Product = { type Product = {
attributes: never[];
/** ID */ /** ID */
id: number; id: number;
/** 产品名称 */ /** 产品名称 */
@ -718,8 +719,8 @@ declare namespace API {
}; };
type ProductComponentItemDTO = { type ProductComponentItemDTO = {
/** 库存记录ID */ /** 组件 SKU */
stockId?: number; sku?: string;
/** 组成数量 */ /** 组成数量 */
quantity?: number; quantity?: number;
}; };
@ -895,7 +896,8 @@ declare namespace API {
type ProductStockComponent = { type ProductStockComponent = {
id?: number; id?: number;
productId?: number; productId?: number;
stockId?: number; /** 组件所关联的 SKU */
productSku?: string;
/** 组成数量 */ /** 组成数量 */
quantity?: number; quantity?: number;
/** 创建时间 */ /** 创建时间 */
@ -1071,6 +1073,10 @@ declare namespace API {
/** 每页大小 */ /** 每页大小 */
pageSize?: number; pageSize?: number;
productName?: string; productName?: string;
/** 按库存点ID排序 */
sortPointId?: number;
/** 排序对象,格式如 { productName: "asc", productSku: "desc" } */
order?: Record<string, any>;
}; };
type QueryStockRecordDTO = { type QueryStockRecordDTO = {
@ -1334,6 +1340,14 @@ declare namespace API {
/** 每页大小 */ /** 每页大小 */
pageSize?: number; pageSize?: number;
productName?: string; productName?: string;
/** 按库存点ID排序 */
sortPointId?: number;
/** 排序对象,格式如 { productName: "asc", productSku: "desc" } */
order?: Record<string, any>;
};
type stockcontrollerHasstockParams = {
sku: string;
}; };
type stockcontrollerLosttransferParams = { type stockcontrollerLosttransferParams = {
@ -1619,6 +1633,8 @@ declare namespace API {
promotionPrice?: number; promotionPrice?: number;
/** 属性列表 */ /** 属性列表 */
attributes?: any[]; attributes?: any[];
/** 商品类型 */
type?: 'simple' | 'bundle';
}; };
type UpdatePurchaseOrderDTO = { type UpdatePurchaseOrderDTO = {