diff --git a/src/pages/Product/Category/index.tsx b/src/pages/Product/Category/index.tsx index 7c516cf..254387a 100644 --- a/src/pages/Product/Category/index.tsx +++ b/src/pages/Product/Category/index.tsx @@ -246,7 +246,7 @@ const CategoryPage: React.FC = () => { > )} @@ -255,7 +255,7 @@ const CategoryPage: React.FC = () => { {selectedCategory ? ( 添加关联属性 @@ -319,6 +319,9 @@ const CategoryPage: React.FC = () => { + + + diff --git a/src/pages/Product/CsvTool/index.tsx b/src/pages/Product/CsvTool/index.tsx index 44f1f8d..35350f6 100644 --- a/src/pages/Product/CsvTool/index.tsx +++ b/src/pages/Product/CsvTool/index.tsx @@ -3,10 +3,9 @@ import { PageContainer, ProForm, ProFormSelect, - ProFormText, } from '@ant-design/pro-components'; import { request } from '@umijs/max'; -import { Button, Card, Col, Input, message, Row, Upload } from 'antd'; +import { Button, Card, Checkbox, Col, Input, message, Row, Upload } from 'antd'; import React, { useEffect, useState } from 'react'; import * as XLSX from 'xlsx'; import { productcontrollerGetcategoriesall } from '@/servers/api/product'; @@ -20,14 +19,22 @@ interface Site { isDisabled?: boolean; } +// 定义选项接口,用于下拉选择框的选项 +interface Option { + name: string; // 显示名称 + shortName: string; // 短名称,用于生成SKU +} + // 定义配置接口 interface SkuConfig { - brands: string[]; - categories: string[]; - flavors: string[]; - strengths: string[]; - humidities: string[]; - versions: string[]; + brands: Option[]; + categories: Option[]; + flavors: Option[]; + strengths: Option[]; + humidities: Option[]; + versions: Option[]; + sizes: Option[]; + quantities: Option[]; } // 定义通用属性映射接口,用于存储属性名称和shortName的对应关系 @@ -43,6 +50,8 @@ interface AttributeMappings { strengths: AttributeMapping; humidities: AttributeMapping; versions: AttributeMapping; + sizes: AttributeMapping; + quantities: AttributeMapping; } /** @@ -57,6 +66,7 @@ const CsvTool: React.FC = () => { const [isProcessing, setIsProcessing] = useState(false); const [sites, setSites] = useState([]); const [selectedSites, setSelectedSites] = useState([]); // 现在使用多选 + const [generateBundleSkuForSingle, setGenerateBundleSkuForSingle] = useState(true); // 是否为type为single的记录生成包含quantity的bundle SKU const [config, setConfig] = useState({ brands: [], categories: [], @@ -64,6 +74,8 @@ const CsvTool: React.FC = () => { strengths: [], humidities: [], versions: [], + sizes: [], + quantities: [], }); // 所有属性名称到shortName的映射 const [attributeMappings, setAttributeMappings] = useState({ @@ -72,7 +84,9 @@ const CsvTool: React.FC = () => { flavors: {}, strengths: {}, humidities: {}, - versions: {} + versions: {}, + sizes: {}, + quantities: {}, }); // 在组件加载时获取站点列表和字典数据 @@ -92,21 +106,24 @@ const CsvTool: React.FC = () => { const dictListResponse = await request('/dict/list'); const dictList = dictListResponse?.data || dictListResponse || []; - // 3. 根据字典名称获取字典项,返回包含name和shortName的对象数组 + // 3. 根据字典名称获取字典项,返回包含name和shortName的完整对象数组 const getDictItems = async (dictName: string) => { try { const dict = dictList.find((d: any) => d.name === dictName); if (!dict) { console.warn(`Dictionary ${dictName} not found`); - return { names: [], mapping: {} }; + return { options: [], mapping: {} }; } const itemsResponse = await request('/dict/items', { params: { dictId: dict.id }, }); const items = itemsResponse?.data || itemsResponse || []; - // 提取名称数组 - const names = items.map((item: any) => item.name); + // 创建完整的选项数组 + const options = items.map((item: any) => ({ + name: item.name, + shortName: item.shortName || item.name + })); // 创建name到shortName的映射 const mapping = items.reduce((acc: AttributeMapping, item: any) => { @@ -114,25 +131,30 @@ const CsvTool: React.FC = () => { return acc; }, {}); - return { names, mapping }; + return { options, mapping }; } catch (error) { console.error(`Failed to fetch items for ${dictName}:`, error); - return { names: [], mapping: {} }; + return { options: [], mapping: {} }; } }; - // 4. 获取所有字典项(品牌、口味、强度、湿度、版本) - const [brandResult, flavorResult, strengthResult, humidityResult, versionResult] = await Promise.all([ + // 4. 获取所有字典项(品牌、口味、强度、湿度、版本、尺寸、数量) + const [brandResult, flavorResult, strengthResult, humidityResult, versionResult, sizeResult, quantityResult] = await Promise.all([ getDictItems('brand'), getDictItems('flavor'), getDictItems('strength'), getDictItems('humidity'), getDictItems('version'), + getDictItems('size'), + getDictItems('quantity'), ]); // 5. 获取商品分类列表 const categoriesResponse = await productcontrollerGetcategoriesall(); - const categories = categoriesResponse?.data?.map((category: any) => category.name) || []; + const categoryOptions = categoriesResponse?.data?.map((category: any) => ({ + name: category.name, + shortName: category.shortName || category.name + })) || []; // 商品分类的映射(如果分类有shortName的话) const categoryMapping = categoriesResponse?.data?.reduce((acc: AttributeMapping, category: any) => { @@ -140,27 +162,42 @@ const CsvTool: React.FC = () => { return acc; }, {}) || {}; - // 7. 设置所有属性映射 + // 6. 设置所有属性映射 setAttributeMappings({ brands: brandResult.mapping, categories: categoryMapping, flavors: flavorResult.mapping, strengths: strengthResult.mapping, humidities: humidityResult.mapping, - versions: versionResult.mapping + versions: versionResult.mapping, + sizes: sizeResult.mapping, + quantities: quantityResult.mapping }); // 更新配置状态 const newConfig = { - brands: brandResult.names, - categories, - flavors: flavorResult.names, - strengths: strengthResult.names, - humidities: humidityResult.names, - versions: versionResult.names, + brands: brandResult.options, + categories: categoryOptions, + flavors: flavorResult.options, + strengths: strengthResult.options, + humidities: humidityResult.options, + versions: versionResult.options, + sizes: sizeResult.options, + quantities: quantityResult.options, }; setConfig(newConfig); - form.setFieldsValue(newConfig); + // 设置表单值时只需要name数组 + form.setFieldsValue({ + brands: brandResult.options.map(opt => opt.name), + categories: categoryOptions.map(opt => opt.name), + flavors: flavorResult.options.map(opt => opt.name), + strengths: strengthResult.options.map(opt => opt.name), + humidities: humidityResult.options.map(opt => opt.name), + versions: versionResult.options.map(opt => opt.name), + sizes: sizeResult.options.map(opt => opt.name), + quantities: quantityResult.options.map(opt => opt.name), + generateBundleSkuForSingle: true, + }); message.success({ content: '数据加载成功', key: 'loading' }); } catch (error) { @@ -184,46 +221,95 @@ const CsvTool: React.FC = () => { setFile(uploadedFile); const reader = new FileReader(); - reader.onload = (e) => { - try { - const data = e.target?.result; - // 如果是ArrayBuffer,使用type: 'array'来处理 - const workbook = XLSX.read(data, { type: 'array' }); - const sheetName = workbook.SheetNames[0]; - const worksheet = workbook.Sheets[sheetName]; - const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 }); - - if (jsonData.length < 2) { - message.error('文件为空或缺少表头!'); - setCsvData([]); - return; - } - - // 将数组转换为对象数组 - const headers = jsonData[0] as string[]; - const rows = jsonData.slice(1).map((rowArray: any) => { - const rowData: { [key: string]: any } = {}; - headers.forEach((header, index) => { - rowData[header] = rowArray[index]; + + // 检查是否为CSV文件 + const isCsvFile = uploadedFile.name.match(/\.csv$/i); + + if (isCsvFile) { + // 对于CSV文件,使用readAsText并指定UTF-8编码以正确处理中文 + reader.onload = (e) => { + try { + const textData = e.target?.result as string; + // 使用XLSX.read处理CSV文本数据,指定type为'csv'并设置编码 + const workbook = XLSX.read(textData, { + type: 'string', + codepage: 65001, // UTF-8 encoding + cellText: true, + cellDates: true }); - return rowData; - }); + const sheetName = workbook.SheetNames[0]; + const worksheet = workbook.Sheets[sheetName]; + const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 }); - message.success(`成功解析 ${rows.length} 条数据.`); - setCsvData(rows); - setProcessedData([]); // 清空旧的处理结果 - } catch (error) { - message.error('文件解析失败,请检查文件格式!'); - console.error('File Parse Error:', error); - setCsvData([]); - } - }; + if (jsonData.length < 2) { + message.error('文件为空或缺少表头!'); + setCsvData([]); + return; + } + + // 将数组转换为对象数组 + const headers = jsonData[0] as string[]; + const rows = jsonData.slice(1).map((rowArray: any) => { + const rowData: { [key: string]: any } = {}; + headers.forEach((header, index) => { + rowData[header] = rowArray[index]; + }); + return rowData; + }); + + message.success(`成功解析 ${rows.length} 条数据.`); + setCsvData(rows); + setProcessedData([]); // 清空旧的处理结果 + } catch (error) { + message.error('CSV文件解析失败,请检查文件格式和编码!'); + console.error('CSV Parse Error:', error); + setCsvData([]); + } + }; + reader.readAsText(uploadedFile, 'UTF-8'); + } else { + // 对于Excel文件,继续使用readAsArrayBuffer + reader.onload = (e) => { + try { + const data = e.target?.result; + // 如果是ArrayBuffer,使用type: 'array'来处理 + const workbook = XLSX.read(data, { type: 'array' }); + const sheetName = workbook.SheetNames[0]; + const worksheet = workbook.Sheets[sheetName]; + const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 }); + + if (jsonData.length < 2) { + message.error('文件为空或缺少表头!'); + setCsvData([]); + return; + } + + // 将数组转换为对象数组 + const headers = jsonData[0] as string[]; + const rows = jsonData.slice(1).map((rowArray: any) => { + const rowData: { [key: string]: any } = {}; + headers.forEach((header, index) => { + rowData[header] = rowArray[index]; + }); + return rowData; + }); + + message.success(`成功解析 ${rows.length} 条数据.`); + setCsvData(rows); + setProcessedData([]); // 清空旧的处理结果 + } catch (error) { + message.error('Excel文件解析失败,请检查文件格式!'); + console.error('Excel Parse Error:', error); + setCsvData([]); + } + }; + reader.readAsArrayBuffer(uploadedFile); + } + reader.onerror = (error) => { message.error('文件读取失败!'); console.error('File Read Error:', error); }; - // 使用readAsArrayBuffer替代已弃用的readAsBinaryString - reader.readAsArrayBuffer(uploadedFile); return false; // 阻止antd Upload组件的默认上传行为 }; @@ -252,15 +338,19 @@ const CsvTool: React.FC = () => { * @param {string} strength - 强度 * @param {string} humidity - 湿度 * @param {string} version - 版本 + * @param {string} type - 产品类型 * @returns {string} 生成的SKU */ const generateSku = ( brand: string, + version: string, category: string, flavor: string, strength: string, humidity: string, - version: string + size: string, + quantity?: any, + type?: string ): string => { // 构建SKU组件,不包含站点前缀 const skuComponents: string[] = []; @@ -271,6 +361,11 @@ const CsvTool: React.FC = () => { const brandShortName = attributeMappings.brands[brand] || brand; skuComponents.push(brandShortName); } + if (version) { + // 使用版本的shortName,如果没有则使用版本名称 + const versionShortName = attributeMappings.versions[version] || version; + skuComponents.push(versionShortName); + } if (category) { // 使用分类的shortName,如果没有则使用分类名称 const categoryShortName = attributeMappings.categories[category] || category; @@ -291,10 +386,22 @@ const CsvTool: React.FC = () => { const humidityShortName = attributeMappings.humidities[humidity] || humidity; skuComponents.push(humidityShortName); } - if (version) { - // 使用版本的shortName,如果没有则使用版本名称 - const versionShortName = attributeMappings.versions[version] || version; - skuComponents.push(versionShortName); + + if (size) { + // 使用尺寸的shortName,如果没有则使用尺寸名称 + const sizeShortName = attributeMappings.sizes[size] || size; + skuComponents.push(sizeShortName); + } + + // 如果type为single且启用了生成bundle SKU,则添加quantity + if ((!type || type === 'single') && generateBundleSkuForSingle && quantity) { + // 将quantity转换为数字,然后格式化为四位数(前导零) + const formattedQuantity = Number(quantity).toString().padStart(4, '0'); + skuComponents.push(formattedQuantity); + } else if (type === 'bundle' && quantity) { + // 对于bundle类型,始终添加quantity + const formattedQuantity = Number(quantity).toString().padStart(4, '0'); + skuComponents.push(formattedQuantity); } // 合并所有组件,使用短横线分隔 @@ -352,22 +459,82 @@ const CsvTool: React.FC = () => { const strength = row.attribute_strength || ''; const humidity = row.attribute_humidity || ''; const version = row.attribute_version || ''; + const size = row.attribute_size || row.size || ''; + // 将quantity保存到attribute_quantity字段 + const quantity = row.attribute_quantity || row.quantity; + // 获取产品类型 + const type = row.type || ''; // 生成基础SKU(不包含站点前缀) - const baseSku = generateSku(brand, category, flavor, strength, humidity, version); + const baseSku = generateSku(brand, version, category, flavor, strength, humidity, size, quantity, type); // 为所有站点生成带前缀的siteSkus const siteSkus = generateSiteSkus(baseSku); - // 返回包含新SKU和siteSkus的行数据 - return { ...row, GeneratedSKU: baseSku, siteSkus }; + // 返回包含新SKU和siteSkus的行数据,将SKU直接保存到sku字段 + return { + ...row, + sku: baseSku, // 直接生成在sku栏 + siteSkus, + attribute_quantity: quantity // 确保quantity保存到attribute_quantity + }; }); - setProcessedData(dataWithSku); + // Determine which data to use for processing and download + let finalData = dataWithSku; + + // If generateBundleSkuForSingle is enabled, generate bundle products for single products + if(generateBundleSkuForSingle) { + // Filter out single records + const singleRecords = dataWithSku.filter(row => row.type === 'single'); + + // Get quantity values from the config (same source as other attributes like brand) + const quantityValues = config.quantities + .map(quantity => Number(quantity.name)) // Extract name and convert to number + .filter(quantity => !isNaN(quantity)); // Filter out invalid numbers + + // Generate bundle products for each single record and quantity + const generatedBundleRecords = singleRecords.flatMap(singleRecord => { + return quantityValues.map(quantity => { + // Extract all necessary attributes from the single record + const brand = singleRecord.attribute_brand || ''; + const version = singleRecord.attribute_version || ''; + const category = singleRecord.category || ''; + const flavor = singleRecord.attribute_flavor || ''; + const strength = singleRecord.attribute_strength || ''; + const humidity = singleRecord.attribute_humidity || ''; + const size = singleRecord.attribute_size || singleRecord.size || ''; + + // Generate bundle SKU with the quantity + const bundleSku = generateSku(brand, version, category, flavor, strength, humidity, size, quantity, 'bundle'); + + // Generate siteSkus for the bundle + const bundleSiteSkus = generateSiteSkus(bundleSku); + + // Create the bundle record + return { + ...singleRecord, + type: 'bundle', // Change type to bundle + sku: bundleSku, // Use the new bundle SKU + siteSkus: bundleSiteSkus, + attribute_quantity: quantity, // Set the attribute_quantity + component_1_sku: singleRecord.sku, // Set component_1_sku to the single product's sku + component_1_quantity: quantity, // Set component_1_quantity to the same as attribute_quantity + }; + }); + }); + + // Combine original dataWithSku with generated bundle records + finalData = [...dataWithSku, ...generatedBundleRecords]; + } + + // Set the processed data + setProcessedData(finalData); + message.success({ content: 'SKU生成成功!正在自动下载...', key: 'processing' }); - // 自动下载 - downloadData(dataWithSku); + // 自动下载 the final data (with or without generated bundle products) + downloadData(finalData); } catch (error) { message.error({ content: '处理失败,请检查配置或文件.', key: 'processing' }); console.error('Processing Error:', error); @@ -397,6 +564,10 @@ const CsvTool: React.FC = () => { placeholder="请输入品牌,按回车确认" rules={[{ required: true, message: '至少需要一个品牌' }]} tooltip="品牌名称会作为SKU的第一个组成部分" + options={config.brands.map(opt => ({ + label: `${opt.name} (${opt.shortName})`, + value: opt.name + }))} /> { placeholder="请输入分类,按回车确认" rules={[{ required: true, message: '至少需要一个分类' }]} tooltip="分类名称会作为SKU的第二个组成部分" + options={config.categories.map(opt => ({ + label: `${opt.name} (${opt.shortName})`, + value: opt.name + }))} /> { placeholder="请输入口味,按回车确认" rules={[{ required: true, message: '至少需要一个口味' }]} tooltip="口味名称会作为SKU的第三个组成部分" + options={config.flavors.map(opt => ({ + label: `${opt.name} (${opt.shortName})`, + value: opt.name + }))} /> { mode="tags" placeholder="请输入强度,按回车确认" tooltip="强度信息会作为SKU的第四个组成部分" + options={config.strengths.map(opt => ({ + label: `${opt.name} (${opt.shortName})`, + value: opt.name + }))} /> { mode="tags" placeholder="请输入湿度,按回车确认" tooltip="湿度信息会作为SKU的第五个组成部分" + options={config.humidities.map(opt => ({ + label: `${opt.name} (${opt.shortName})`, + value: opt.name + }))} /> { mode="tags" placeholder="请输入版本,按回车确认" tooltip="版本信息会作为SKU的第六个组成部分" + options={config.versions.map(opt => ({ + label: `${opt.name} (${opt.shortName})`, + value: opt.name + }))} /> + ({ + label: `${opt.name} (${opt.shortName})`, + value: opt.name + }))} + /> + + ({ + label: `${opt.name} (${opt.shortName})`, + value: opt.name + }))} + fieldProps={{ allowClear: true }} + /> + + + + 启用为single类型生成bundle SKU + + @@ -453,7 +680,7 @@ const CsvTool: React.FC = () => { {sites.map(site => ( {site.name} - {site.shortName} + {site.skuPrefix} ))}