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

Closed
zksu wants to merge 37 commits from (deleted):main into main
1 changed files with 274 additions and 118 deletions
Showing only changes of commit 0a6ea0396f - Show all commits

View File

@ -12,7 +12,9 @@ import {
productcontrollerSetproductcomponents,
productcontrollerAutobindcomponents,
productcontrollerGetattributeall,
productcontrollerGetattributelist,
} from '@/servers/api/product';
import { request } from '@umijs/max';
import { templatecontrollerRendertemplate } from '@/servers/api/template';
import { PlusOutlined } from '@ant-design/icons';
import {
@ -28,7 +30,7 @@ import {
ProFormTextArea,
ProTable,
} from '@ant-design/pro-components';
import { App, Button, Popconfirm, Tag } from 'antd';
import { App, Button, Popconfirm, Tag, Upload } from 'antd';
import { allowedDictNames } from '@/pages/Product/Attribute/consts';
import React, { useRef, useState } from 'react';
const capitalize = (s: string) => s.charAt(0).toLocaleUpperCase() + s.slice(1);
@ -119,6 +121,28 @@ const List: React.FC = () => {
const [selectedRows, setSelectedRows] = React.useState<API.Product[]>([]);
const { message } = App.useApp();
// 中文注释:导出产品 CSV带认证请求
const handleDownloadProductsCSV = async () => {
try {
// 中文注释:发起认证请求获取 CSV Blob
const blob = await request('/product/export', { responseType: 'blob' });
// 中文注释:构建下载文件名
const d = new Date();
const pad = (n: number) => String(n).padStart(2, '0');
const filename = `products-${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}.csv`;
// 中文注释:创建临时链接并触发下载
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
window.URL.revokeObjectURL(url);
} catch (error) {
message.error('导出失败');
}
};
const columns: ProColumns<API.Product>[] = [
{
title: 'sku',
@ -215,7 +239,30 @@ const List: React.FC = () => {
headerTitle="查询表格"
actionRef={actionRef}
rowKey="id"
toolBarRender={() => [<CreateForm tableRef={actionRef} />]}
toolBarRender={() => [
// 中文注释:新建按钮
<CreateForm tableRef={actionRef} />,
// 中文注释:导出 CSV后端返回 text/csv直接新窗口下载
<Button onClick={handleDownloadProductsCSV}>CSV</Button>,
// 中文注释:导入 CSV上传文件成功后刷新表格
<Upload
name="file"
action="/product/import"
accept=".csv"
showUploadList={false}
maxCount={1}
onChange={({ file }) => {
if (file.status === 'done') {
message.success('导入完成');
actionRef.current?.reload();
} else if (file.status === 'error') {
message.error('导入失败');
}
}}
>
<Button>CSV</Button>
</Upload>,
]}
request={async (params) => {
const { data, success } = await productcontrollerGetproductlist(
params,
@ -248,10 +295,12 @@ const CreateForm: React.FC<{
const { message } = App.useApp();
// 表单引用
const formRef = useRef<ProFormInstance>();
// 状态:存储品牌、强度和口味的选项
const [brandOptions, setBrandOptions] = useState<DictItem[]>([]);
const [strengthOptions, setStrengthOptions] = useState<DictItem[]>([]);
const [flavorOptions, setFlavorOptions] = useState<DictItem[]>([]);
// 状态存储品牌、强度、口味、规格的选项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 }[]>([]);
/**
* @description SKU
@ -260,26 +309,27 @@ const CreateForm: React.FC<{
try {
// 从表单引用中获取当前表单的值
const formValues = formRef.current?.getFieldsValue();
const { humidity, brandId, strengthId, flavorsId } = formValues;
const { humidityValues, brandValues, strengthValues, flavorsValues } = formValues;
// 检查是否所有必需的字段都已选择
if (!brandId || !strengthId || !flavorsId || !humidity) {
if (!brandValues?.length || !strengthValues?.length || !flavorsValues?.length || !humidityValues?.length) {
message.warning('请先选择品牌、强度、口味和干湿');
return;
}
// 从选项中查找所选品牌、强度和口味的完整对象
const brand = brandOptions.find((item) => item.id === brandId);
const strength = strengthOptions.find((item) => item.id === strengthId);
const flavor = flavorOptions.find((item) => item.id === flavorsId);
// 所选值(用于 SKU 模板传入 name
const brandName: string = String(brandValues[0]);
const strengthName: string = String(strengthValues[0]);
const flavorName: string = String(flavorsValues[0]);
const humidityName: string = String(humidityValues[0]);
// 调用模板渲染API来生成SKU
const { data: rendered, message: msg, success } = await templatecontrollerRendertemplate(
{ name: 'product.sku' },
{
brand: brand ? brand.name : "",
strength: strength ? strength.name : '',
flavor: flavor ? flavor.name : '',
humidity: humidity ? capitalize(humidity) : '',
brand: brandName || "",
strength: strengthName || '',
flavor: flavorName || '',
humidity: humidityName ? capitalize(humidityName) : '',
},
);
if (!success) {
@ -300,27 +350,31 @@ const CreateForm: React.FC<{
try {
// 从表单引用中获取当前表单的值
const formValues = formRef.current?.getFieldsValue();
const { humidity, brandId, strengthId, flavorsId } = formValues;
const { humidityValues, brandValues, strengthValues, flavorsValues } = formValues;
// 检查是否所有必需的字段都已选择
if (!brandId || !strengthId || !flavorsId || !humidity) {
if (!brandValues?.length || !strengthValues?.length || !flavorsValues?.length || !humidityValues?.length) {
message.warning('请先选择品牌、强度、口味和干湿');
return;
}
// 从选项中查找所选品牌、强度和口味的完整对象
const brand = brandOptions.find((item) => item.id === brandId);
const strength = strengthOptions.find((item) => item.id === strengthId);
const flavor = flavorOptions.find((item) => item.id === flavorsId);
// 获取标题label若为新输入值则使用原值作为标题
const brandName: string = String(brandValues[0]);
const strengthName: string = String(strengthValues[0]);
const flavorName: string = String(flavorsValues[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;
// 调用模板渲染API来生成产品名称
const { message: msg, data: rendered, success } = await templatecontrollerRendertemplate(
{ name: 'product.title' },
{
brand: brand ? brand.title : "",
strength: strength ? strength.title : '',
flavor: flavor ? flavor.title : '',
brand: brandTitle,
strength: strengthTitle,
flavor: flavorTitle,
model: '',
humidity: humidity === 'dry' ? 'Dry' : 'Moisture',
humidity: humidityName === 'dry' ? 'Dry' : humidityName === 'moisture' ? 'Moisture' : capitalize(humidityName),
},
);
if (!success) {
@ -334,7 +388,7 @@ const CreateForm: React.FC<{
};
// TODO 可以输入brand等
return (
<DrawerForm<API.CreateProductDTO>
<DrawerForm<any>
title="新建"
formRef={formRef} // Pass formRef
trigger={
@ -348,20 +402,30 @@ const CreateForm: React.FC<{
destroyOnHidden: true,
}}
onFinish={async (values) => {
// 中文注释将选择的字典项ID与干湿属性组装为后端需要的 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 = [
...mapWithLabel(brandValues, brandOptions, 'brand'),
...mapWithLabel(strengthValues, strengthOptions, 'strength'),
...mapWithLabel(flavorsValues, flavorOptions, 'flavor'),
...mapWithLabel(sizeValues, sizeOptions, 'size'),
...mapWithLabel(humidityValues, humidityOptions, 'humidity'),
].filter(Boolean);
const payload: any = {
name: values.name,
description: values.description,
sku: values.sku,
price: values.price,
promotionPrice: values.promotionPrice,
attributes: [
values.brandId ? { id: values.brandId } : null,
values.strengthId ? { id: values.strengthId } : null,
values.flavorsId ? { id: values.flavorsId } : null,
values.sizeId ? { id: values.sizeId } : null,
values.humidity ? { id: values.humidityId } : null,
].filter(Boolean),
name: (values as any).name,
description: (values as any).description,
sku: (values as any).sku,
price: (values as any).price,
promotionPrice: (values as any).promotionPrice,
attributes,
};
const { success, message: errMsg } =
await productcontrollerCreateproduct(payload);
@ -374,71 +438,127 @@ const CreateForm: React.FC<{
return false;
}}
>
{/* 品牌(可搜索、可新增) */}
<ProFormSelect
name="brandId"
name="brandValues"
width="lg"
label="产品品牌"
placeholder="请选择产品品牌"
request={async () => {
const { data = [] } = await productcontrollerCompatbrandall();
setBrandOptions(data);
return data.map((item: DictItem) => ({
label: item.name,
value: item.id,
}));
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="strengthId"
name="strengthValues"
width="lg"
label="强度"
placeholder="请选择强度"
request={async () => {
const { data = [] } = await productcontrollerCompatstrengthall();
setStrengthOptions(data);
return data.map((item: DictItem) => ({
label: item.name,
value: item.id,
}));
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="flavorsId"
name="flavorsValues"
width="lg"
label="口味"
placeholder="请选择口味"
request={async () => {
const { data = [] } = await productcontrollerCompatflavorsall();
setFlavorOptions(data);
return data.map((item: DictItem) => ({
label: item.name,
value: item.id,
}));
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="sizeId"
name="sizeValues"
width="lg"
label="规格"
placeholder="请选择规格"
request={async () => {
const { data = [] } = await productcontrollerCompatsizeall();
return (data || []).map((item: any) => ({ label: item.name, value: item.id }));
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="humidity"
name="humidityValues"
width="lg"
label="干湿"
placeholder="请选择干湿"
request={async () => {
const { data = [] } = await productcontrollerGetattributeall({ dictName: 'humidity' } as any);
return (data || []).map((item: any) => ({ label: item.name, value: item.name }));
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>
@ -497,29 +617,24 @@ const EditForm: React.FC<{
}> = ({ tableRef, record }) => {
const { message } = App.useApp();
const formRef = useRef<ProFormInstance>();
const [brandOptions, setBrandOptions] = useState<DictItem[]>([]);
const [strengthOptions, setStrengthOptions] = useState<DictItem[]>([]);
const [flavorOptions, setFlavorOptions] = useState<DictItem[]>([]);
// 中文注释:各属性的选择项(使用 {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 setInitialIds = () => {
const brand = brandOptions.find((item) => item.title === (record.brand?.name));
const strength = strengthOptions.find((item) => item.title === (record.strength?.name));
const flavor = flavorOptions.find((item) => item.title === (record.flavors?.name));
formRef.current?.setFieldsValue({
brandId: brand?.id,
strengthId: strength?.id,
flavorsId: flavor?.id,
});
// 中文注释:通用远程选项加载器(支持关键词搜索)
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(() => {
if (brandOptions.length && strengthOptions.length && flavorOptions.length) {
setInitialIds();
}
}, [brandOptions, strengthOptions, flavorOptions]);
React.useEffect(() => {
// 中文注释:加载干湿选项
(async () => {
@ -536,7 +651,7 @@ const EditForm: React.FC<{
}, []);
return (
<DrawerForm<API.UpdateProductDTO>
<DrawerForm<any>
formRef={formRef}
title="编辑"
trigger={<Button type="link"></Button>}
@ -548,14 +663,22 @@ const EditForm: React.FC<{
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] : [],
}}
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 = [
values.brandId ? { id: values.brandId } : null,
values.strengthId ? { id: values.strengthId } : null,
values.flavorsId ? { id: values.flavorsId } : null,
values.sizeId ? { id: values.sizeId } : null,
// 条件判断:将 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 updatePayload: any = {
@ -604,38 +727,71 @@ const EditForm: React.FC<{
rules={[{ required: true, message: '请输入名称' }]}
/>
</ProForm.Group>
{/* 中文注释:品牌(可搜索、可新增) */}
<ProFormSelect
name="brandId"
name="brandValues"
width="lg"
label="产品品牌"
placeholder="请选择产品品牌"
request={async () => {
const { data = [] } = await productcontrollerCompatbrandall();
setBrandOptions(data);
return data.map((item: DictItem) => ({ label: item.name, value: item.id }));
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="strengthId"
name="strengthValues"
width="lg"
label="强度"
placeholder="请选择强度"
request={async () => {
const { data = [] } = await productcontrollerCompatstrengthall();
setStrengthOptions(data);
return data.map((item: DictItem) => ({ label: item.name, value: item.id }));
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="flavorsId"
name="flavorsValues"
width="lg"
label="口味"
placeholder="请选择口味"
request={async () => {
const { data = [] } = await productcontrollerCompatflavorsall();
setFlavorOptions(data);
return data.map((item: DictItem) => ({ label: item.name, value: item.id }));
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"