diff --git a/package.json b/package.json index d77a71f..8d155d5 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/pages/Product/Attribute/components/AttributeFormItem.tsx b/src/pages/Product/Attribute/components/AttributeFormItem.tsx new file mode 100644 index 0000000..8a966d5 --- /dev/null +++ b/src/pages/Product/Attribute/components/AttributeFormItem.tsx @@ -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 = ({ dictName, name, label, isTag = false }) => { + const [options, setOptions] = useState<{ label: string; value: string }[]>([]); + + if (isTag) { + return ( + { + const opts = await fetchDictOptions(dictName, val); + setOptions(opts); + }, + }} + request={async () => { + const opts = await fetchDictOptions(dictName); + setOptions(opts); + return opts; + }} + options={options} + /> + ); + } + + return ( + fetchDictOptions(dictName)} + /> + ); +}; + +export default AttributeFormItem; diff --git a/src/pages/Product/Attribute/consts.ts b/src/pages/Product/Attribute/consts.ts index fd4af07..9f5c4c4 100644 --- a/src/pages/Product/Attribute/consts.ts +++ b/src/pages/Product/Attribute/consts.ts @@ -1,2 +1,2 @@ // 中文注释:限定允许管理的字典名称集合 -export const allowedDictNames = new Set(['brand', 'strength', 'flavor', 'size', 'humidity']); \ No newline at end of file +export const attributes = new Set(['brand', 'strength', 'flavor', 'size', 'humidity','category']); \ No newline at end of file diff --git a/src/pages/Product/Attribute/index.tsx b/src/pages/Product/Attribute/index.tsx index 8c4d9dc..9951179 100644 --- a/src/pages/Product/Attribute/index.tsx +++ b/src/pages/Product/Attribute/index.tsx @@ -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('获取字典列表失败'); diff --git a/src/pages/Product/List/index.tsx b/src/pages/Product/List/index.tsx index 4446f5e..991d33b 100644 --- a/src/pages/Product/List/index.tsx +++ b/src/pages/Product/List/index.tsx @@ -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 (
- {items.length ? items.map((it, idx) => ( + {(record.attributes || []).map((data: any, idx: number) => ( - {it.key}: {it.value} + {data?.dict?.name}: {data.name} - )) : -} + ))}
); }; const ComponentsCell: React.FC<{ productId: number }> = ({ productId }) => { - const [items, setItems] = React.useState([]); + const [components, setComponents] = React.useState([]); React.useEffect(() => { (async () => { const { data = [] } = await productcontrollerGetproductcomponents({ id: productId }); - setItems(data || []); + setComponents(data || []); })(); }, [productId]); return (
- {items && items.length ? ( - items.map((c: any) => ( - - {(c.stock && c.stock.productSku) || `#${c.stockId}`} × {c.quantity}(库存:{c.stock ? c.stock.quantity : '-'}) + {components && components.length ? ( + components.map((component: any) => ( + + {(component.productSku) || `#${component.id}`} × {component.quantity}(库存: + {component.stock?.map((s: any) => `${s.name}:${s.quantity}`).join(', ') || '-'} + ) )) ) : ( @@ -295,12 +288,8 @@ const CreateForm: React.FC<{ const { message } = App.useApp(); // 表单引用 const formRef = useRef(); - // 状态:存储品牌、强度、口味、规格的选项(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( @@ -386,7 +374,7 @@ const CreateForm: React.FC<{ message.error(`生成失败: ${error.message}`); } }; - // TODO 可以输入brand等 + // TODO 可以输入brand等 return ( title="新建" @@ -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; }} > - {/* 品牌(可搜索、可新增) */} - { - 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 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 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 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 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: '请选择干湿' }]} - /> + 自动生成 + {productType && ( + + {productType === 'single' ? '单品' : '套装'} + + )} + + + + + + = ({ tableRef, record }) => { const { message } = App.useApp(); const formRef = useRef(); - // 中文注释:各属性的选择项(使用 {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); + + 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 ( formRef={formRef} title="编辑" trigger={} - 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 && ( + + {productType === 'single' ? '单品' : '套装'} + + )} - {/* 中文注释:品牌(可搜索、可新增) */} - { - const options = await fetchDictOptions('brand', val); - setBrandOptions(options); - }, - }} - request={async () => { - const options = await fetchDictOptions('brand'); - setBrandOptions(options); - return options; - }} - options={brandOptions} - /> - {/* 中文注释:强度(可搜索、可新增) */} - { - const options = await fetchDictOptions('strength', val); - setStrengthOptions(options); - }, - }} - request={async () => { - const options = await fetchDictOptions('strength'); - setStrengthOptions(options); - return options; - }} - options={strengthOptions} - /> - {/* 中文注释:口味(可搜索、可新增) */} - { - const options = await fetchDictOptions('flavor', val); - setFlavorOptions(options); - }, - }} - request={async () => { - const options = await fetchDictOptions('flavor'); - setFlavorOptions(options); - return options; - }} - options={flavorOptions} - /> - + + + + + +