feat: 添加产品工具, 重构产品 #31

Closed
zksu wants to merge 37 commits from (deleted):main into main
10 changed files with 355 additions and 354 deletions
Showing only changes of commit b2575a11fd - Show all commits

View File

@ -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",

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;
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('获取字典列表失败');

View File

@ -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>
</DrawerForm>
</ProForm.Item>
</DrawerForm >
);
};

View File

View File

@ -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];
});
// 导出

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 */
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默认没有生成对象)

View File

@ -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默认没有生成对象)

View File

@ -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 = {