import { productcontrollerGetcategoriesall } from '@/servers/api/product'; import { UploadOutlined } from '@ant-design/icons'; import { PageContainer, ProForm, ProFormSelect, } from '@ant-design/pro-components'; import { request } from '@umijs/max'; import { Button, Card, Checkbox, Col, Input, message, Row, Upload } from 'antd'; import React, { useEffect, useState } from 'react'; import * as XLSX from 'xlsx'; // 定义站点接口 interface Site { id: number; name: string; skuPrefix?: string; isDisabled?: boolean; } // 定义选项接口,用于下拉选择框的选项 interface Option { name: string; // 显示名称 shortName: string; // 短名称,用于生成SKU } // 定义配置接口 interface SkuConfig { brands: Option[]; categories: Option[]; flavors: Option[]; strengths: Option[]; humidities: Option[]; versions: Option[]; sizes: Option[]; quantities: Option[]; } // 定义通用属性映射接口,用于存储属性名称和shortName的对应关系 interface AttributeMapping { [attributeName: string]: string; // key: 属性名称, value: 属性shortName } // 定义所有属性映射的接口 interface AttributeMappings { brands: AttributeMapping; categories: AttributeMapping; flavors: AttributeMapping; strengths: AttributeMapping; humidities: AttributeMapping; versions: AttributeMapping; sizes: AttributeMapping; quantities: AttributeMapping; } /** * @description 产品CSV工具页面,用于批量生成SKU */ const CsvTool: React.FC = () => { // 状态管理 const [form] = ProForm.useForm(); const [file, setFile] = useState(null); const [csvData, setCsvData] = useState([]); const [processedData, setProcessedData] = useState([]); 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: [], flavors: [], strengths: [], humidities: [], versions: [], sizes: [], quantities: [], }); // 所有属性名称到shortName的映射 const [attributeMappings, setAttributeMappings] = useState( { brands: {}, categories: {}, flavors: {}, strengths: {}, humidities: {}, versions: {}, sizes: {}, quantities: {}, }, ); // 在组件加载时获取站点列表和字典数据 useEffect(() => { const fetchAllData = async () => { try { message.loading({ content: '正在加载数据...', key: 'loading' }); // 1. 获取站点列表 const sitesResponse = await request('/site/all'); const siteList = sitesResponse?.data || sitesResponse || []; setSites(siteList); // 默认选择所有站点 setSelectedSites(siteList); // 2. 获取字典数据 const dictListResponse = await request('/dict/list'); const dictList = dictListResponse?.data || dictListResponse || []; // 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 { options: [], mapping: {} }; } const itemsResponse = await request('/dict/items', { params: { dictId: dict.id }, }); const items = itemsResponse?.data || itemsResponse || []; // 创建完整的选项数组 const options = items.map((item: any) => ({ name: item.name, shortName: item.shortName || item.name, })); // 创建name到shortName的映射 const mapping = items.reduce((acc: AttributeMapping, item: any) => { acc[item.name] = item.shortName || item.name; return acc; }, {}); return { options, mapping }; } catch (error) { console.error(`Failed to fetch items for ${dictName}:`, error); return { options: [], mapping: {} }; } }; // 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 categoryOptions = categoriesResponse?.data?.map((category: any) => ({ name: category.name, shortName: category.shortName || category.name, })) || []; // 商品分类的映射(如果分类有shortName的话) const categoryMapping = categoriesResponse?.data?.reduce( (acc: AttributeMapping, category: any) => { acc[category.name] = category.shortName || category.name; return acc; }, {}, ) || {}; // 6. 设置所有属性映射 setAttributeMappings({ brands: brandResult.mapping, categories: categoryMapping, flavors: flavorResult.mapping, strengths: strengthResult.mapping, humidities: humidityResult.mapping, versions: versionResult.mapping, sizes: sizeResult.mapping, quantities: quantityResult.mapping, }); // 更新配置状态 const newConfig = { 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); // 设置表单值时只需要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) { console.error('Failed to fetch data:', error); message.error({ content: '数据加载失败,请刷新页面重试', key: 'loading', }); } }; fetchAllData(); }, [form]); /** * @description 处理文件上传 */ const handleFileUpload = (uploadedFile: File) => { // 检查文件类型 if (!uploadedFile.name.match(/\.(csv|xlsx|xls)$/)) { message.error('请上传 CSV 或 Excel 格式的文件!'); return false; } setFile(uploadedFile); const reader = new FileReader(); // 检查是否为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, }); 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('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); }; return false; // 阻止antd Upload组件的默认上传行为 }; /** * @description 将数据转换回CSV并触发下载 */ const downloadData = (data: any[]) => { if (data.length === 0) return; const workbook = XLSX.utils.book_new(); const worksheet = XLSX.utils.json_to_sheet(data); XLSX.utils.book_append_sheet(workbook, worksheet, 'Products with SKU'); const fileName = `products_with_sku_${Date.now()}.xlsx`; XLSX.writeFile(workbook, fileName); message.success('下载任务已开始!'); }; /** * @description 根据配置生成SKU(不包含站点前缀) * @param {string} brand - 品牌 * @param {string} category - 分类 * @param {string} flavor - 口味 * @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, size: string, quantity?: any, type?: string, ): string => { // 构建SKU组件,不包含站点前缀 const skuComponents: string[] = []; // 按顺序添加SKU组件,所有属性都使用shortName if (brand) { // 使用品牌的shortName,如果没有则使用品牌名称 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; skuComponents.push(categoryShortName); } if (flavor) { // 使用口味的shortName,如果没有则使用口味名称 const flavorShortName = attributeMappings.flavors[flavor] || flavor; skuComponents.push(flavorShortName); } if (strength) { // 使用强度的shortName,如果没有则使用强度名称 const strengthShortName = attributeMappings.strengths[strength] || strength; skuComponents.push(strengthShortName); } if (humidity) { // 使用湿度的shortName,如果没有则使用湿度名称 const humidityShortName = attributeMappings.humidities[humidity] || humidity; skuComponents.push(humidityShortName); } if (size) { // 使用尺寸的shortName,如果没有则使用尺寸名称 const sizeShortName = attributeMappings.sizes[size] || size; skuComponents.push(sizeShortName); } // 如果type为single且启用了生成bundle SKU,则添加quantity if ( quantity ) { // 使用quantity的shortName,如果没有则使用quantity但匹配 4 个零 const quantityShortName = attributeMappings.quantities[quantity] || Number(quantity).toString().padStart(4, '0'); skuComponents.push(quantityShortName); } // 合并所有组件,使用短横线分隔 return skuComponents.join('-').toUpperCase(); }; /** * @description 根据配置生成产品名称(使用属性的完整名称,空格分隔) * @param {string} brand - 品牌 * @param {string} version - 版本 * @param {string} category - 分类 * @param {string} flavor - 口味 * @param {string} strength - 强度 * @param {string} humidity - 湿度 * @param {string} size - 型号 * @param {any} quantity - 数量 * @param {string} type - 产品类型 * @returns {string} 生成的产品名称 */ const generateName = ( brand: string, version: string, category: string, flavor: string, strength: string, humidity: string, size: string, quantity?: any, type?: string, ): string => { // 构建产品名称组件数组 const nameComponents: string[] = []; // 按顺序添加组件:品牌 -> 版本 -> 品类 -> 风味 -> 毫克数(强度) -> 湿度 -> 型号 -> 数量 if (brand) nameComponents.push(brand); if (version) nameComponents.push(version); if (category) nameComponents.push(category); if (flavor) nameComponents.push(flavor); if (strength) nameComponents.push(strength); if (humidity) nameComponents.push(humidity); if (size) nameComponents.push(size); // 如果有数量且类型为bundle或者生成bundle的single产品,则添加数量 if ( type==='bundle' && quantity ) { nameComponents.push(String(quantity)); } // 使用空格连接所有组件 return nameComponents.join(' '); }; /** * @description 为所有站点生成带前缀的siteSkus * @param {string} baseSku - 基础SKU(不包含站点前缀) * @returns {string} 所有站点的siteSkus,以分号分隔 */ const generateSiteSkus = (baseSku: string): string => { // 如果没有站点或基础SKU为空,返回空字符串 if (selectedSites.length === 0 || !baseSku) return ''; // 为每个站点生成siteSku const siteSkus = selectedSites.map((site) => { // 如果站点有shortName,则添加前缀,否则使用基础SKU if (site.skuPrefix) { return `${site.skuPrefix}-${baseSku}`; } return baseSku; }); // 使用分号分隔所有站点的siteSkus return [baseSku, ...siteSkus].join(';').toUpperCase(); }; /** * @description 核心逻辑:根据配置处理CSV数据并生成SKU */ const handleProcessData = async () => { if (csvData.length === 0) { message.warning('请先上传并成功解析一个CSV文件.'); return; } if (selectedSites.length === 0) { message.warning('没有可用的站点.'); return; } setIsProcessing(true); message.loading({ content: '正在生成SKU...', key: 'processing' }); try { // 获取表单中的最新配置 await form.validateFields(); // 处理每条数据,生成SKU和siteSkus const dataWithSku = csvData.map((row) => { const brand = row.attribute_brand || ''; const category = row.category || ''; const flavor = row.attribute_flavor || ''; 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, version, category, flavor, strength, humidity, size, quantity, type, ); const name = generateName( brand, version, category, flavor, strength, humidity, size, quantity, type, ); // 为所有站点生成带前缀的siteSkus const siteSkus = generateSiteSkus(baseSku); // 返回包含新SKU和siteSkus的行数据,将SKU直接保存到sku栏 return { ...row, sku: baseSku, // 直接生成在sku栏 generatedName: name, // name: name, // 生成的产品名称 siteSkus, attribute_quantity: quantity, // 确保quantity保存到attribute_quantity }; }); // Determine which data to use for processing and download let finalData = dataWithSku; console.log('generateBundleSkuForSingle',generateBundleSkuForSingle) // 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=>quantity.name) // 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 bundle name with the quantity const bundleName = generateName( 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 name: bundleName, // Use the new bundle name 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: Number(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', }); // 自动下载 the final data (with or without generated bundle products) downloadData(finalData); } catch (error) { message.error({ content: '处理失败,请检查配置或文件.', key: 'processing', }); console.error('Processing Error:', error); } finally { setIsProcessing(false); } }; return ( {/* 左侧:配置表单 */} ({ label: `${opt.name} (${opt.shortName})`, value: opt.name, }))} /> ({ label: `${opt.name} (${opt.shortName})`, value: opt.name, }))} /> ({ label: `${opt.name} (${opt.shortName})`, value: opt.name, }))} /> ({ label: `${opt.name} (${opt.shortName})`, value: opt.name, }))} /> ({ label: `${opt.name} (${opt.shortName})`, value: opt.name, }))} /> ({ 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 }} /> setGenerateBundleSkuForSingle(e.target.checked)}> 启用为single类型生成bundle SKU {/* 显示所有站点及其shortname */}
{sites.length > 0 ? ( {sites.map((site) => ( ))}
站点名称 ShortName
{site.name} {site.skuPrefix}
) : (

暂无站点信息

)}

说明:所有站点的shortName将作为前缀添加到生成的SKU中,以分号分隔。

{/* 右侧:文件上传与操作 */} { setFile(null); setCsvData([]); setProcessedData([]); }} >
{/* 显示处理结果摘要 */} {processedData.length > 0 && (

已成功为 {processedData.length} 条产品记录生成SKU!

)}
); }; export default CsvTool;