diff --git a/src/pages/Product/List/index.tsx b/src/pages/Product/List/index.tsx index c23ee0f..4446f5e 100644 --- a/src/pages/Product/List/index.tsx +++ b/src/pages/Product/List/index.tsx @@ -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([]); 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[] = [ { title: 'sku', @@ -215,7 +239,30 @@ const List: React.FC = () => { headerTitle="查询表格" actionRef={actionRef} rowKey="id" - toolBarRender={() => []} + toolBarRender={() => [ + // 中文注释:新建按钮 + , + // 中文注释:导出 CSV(后端返回 text/csv,直接新窗口下载) + , + // 中文注释:导入 CSV(上传文件成功后刷新表格) + { + if (file.status === 'done') { + message.success('导入完成'); + actionRef.current?.reload(); + } else if (file.status === 'error') { + message.error('导入失败'); + } + }} + > + + , + ]} request={async (params) => { const { data, success } = await productcontrollerGetproductlist( params, @@ -248,10 +295,12 @@ const CreateForm: React.FC<{ const { message } = App.useApp(); // 表单引用 const formRef = useRef(); - // 状态:存储品牌、强度和口味的选项 - const [brandOptions, setBrandOptions] = useState([]); - const [strengthOptions, setStrengthOptions] = useState([]); - const [flavorOptions, setFlavorOptions] = useState([]); + // 状态:存储品牌、强度、口味、规格的选项(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 ( - + 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; }} > + {/* 品牌(可搜索、可新增) */} { - 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: '请选择产品品牌' }]} /> + {/* 强度(可搜索、可新增) */} { - 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: '请选择强度' }]} /> + {/* 口味(可搜索、可新增) */} { - 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: '请选择口味' }]} /> { - 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 }]} /> { - 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: '请选择干湿' }]} /> @@ -497,29 +617,24 @@ const EditForm: React.FC<{ }> = ({ tableRef, record }) => { const { message } = App.useApp(); const formRef = useRef(); - const [brandOptions, setBrandOptions] = useState([]); - const [strengthOptions, setStrengthOptions] = useState([]); - const [flavorOptions, setFlavorOptions] = useState([]); + // 中文注释:各属性的选择项(使用 {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 ( - + formRef={formRef} title="编辑" trigger={} @@ -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: '请输入名称' }]} /> + {/* 中文注释:品牌(可搜索、可新增) */} { - 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} /> + {/* 中文注释:强度(可搜索、可新增) */} { - 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} /> + {/* 中文注释:口味(可搜索、可新增) */} { - 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} />