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} |
))}