744 lines
28 KiB
TypeScript
744 lines
28 KiB
TypeScript
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';
|
||
import { productcontrollerGetcategoriesall } from '@/servers/api/product';
|
||
|
||
|
||
// 定义站点接口
|
||
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<File | null>(null);
|
||
const [csvData, setCsvData] = useState<any[]>([]);
|
||
const [processedData, setProcessedData] = useState<any[]>([]);
|
||
const [isProcessing, setIsProcessing] = useState(false);
|
||
const [sites, setSites] = useState<Site[]>([]);
|
||
const [selectedSites, setSelectedSites] = useState<Site[]>([]); // 现在使用多选
|
||
const [generateBundleSkuForSingle, setGenerateBundleSkuForSingle] = useState(true); // 是否为type为single的记录生成包含quantity的bundle SKU
|
||
const [config, setConfig] = useState<SkuConfig>({
|
||
brands: [],
|
||
categories: [],
|
||
flavors: [],
|
||
strengths: [],
|
||
humidities: [],
|
||
versions: [],
|
||
sizes: [],
|
||
quantities: [],
|
||
});
|
||
// 所有属性名称到shortName的映射
|
||
const [attributeMappings, setAttributeMappings] = useState<AttributeMappings>({
|
||
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 ((!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);
|
||
}
|
||
|
||
// 合并所有组件,使用短横线分隔
|
||
return skuComponents.join('-').toUpperCase();
|
||
};
|
||
|
||
/**
|
||
* @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 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);
|
||
|
||
// 为所有站点生成带前缀的siteSkus
|
||
const siteSkus = generateSiteSkus(baseSku);
|
||
|
||
// 返回包含新SKU和siteSkus的行数据,将SKU直接保存到sku字段
|
||
return {
|
||
...row,
|
||
sku: baseSku, // 直接生成在sku栏
|
||
siteSkus,
|
||
attribute_quantity: quantity // 确保quantity保存到attribute_quantity
|
||
};
|
||
});
|
||
|
||
// 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' });
|
||
|
||
// 自动下载 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 (
|
||
<PageContainer title="产品SKU批量生成工具">
|
||
<Row gutter={[16, 16]}>
|
||
{/* 左侧:配置表单 */}
|
||
<Col xs={24} md={10}>
|
||
<Card title="1. 配置SKU生成规则">
|
||
<ProForm
|
||
form={form}
|
||
initialValues={config}
|
||
onFinish={handleProcessData}
|
||
submitter={false}
|
||
>
|
||
<ProFormSelect
|
||
name="brands"
|
||
label="品牌列表"
|
||
mode="tags"
|
||
placeholder="请输入品牌,按回车确认"
|
||
rules={[{ required: true, message: '至少需要一个品牌' }]}
|
||
tooltip="品牌名称会作为SKU的第一个组成部分"
|
||
options={config.brands.map(opt => ({
|
||
label: `${opt.name} (${opt.shortName})`,
|
||
value: opt.name
|
||
}))}
|
||
/>
|
||
<ProFormSelect
|
||
name="categories"
|
||
label="商品分类"
|
||
mode="tags"
|
||
placeholder="请输入分类,按回车确认"
|
||
rules={[{ required: true, message: '至少需要一个分类' }]}
|
||
tooltip="分类名称会作为SKU的第二个组成部分"
|
||
options={config.categories.map(opt => ({
|
||
label: `${opt.name} (${opt.shortName})`,
|
||
value: opt.name
|
||
}))}
|
||
/>
|
||
<ProFormSelect
|
||
name="flavors"
|
||
label="口味列表"
|
||
mode="tags"
|
||
placeholder="请输入口味,按回车确认"
|
||
rules={[{ required: true, message: '至少需要一个口味' }]}
|
||
tooltip="口味名称会作为SKU的第三个组成部分"
|
||
options={config.flavors.map(opt => ({
|
||
label: `${opt.name} (${opt.shortName})`,
|
||
value: opt.name
|
||
}))}
|
||
/>
|
||
<ProFormSelect
|
||
name="strengths"
|
||
label="强度列表"
|
||
mode="tags"
|
||
placeholder="请输入强度,按回车确认"
|
||
tooltip="强度信息会作为SKU的第四个组成部分"
|
||
options={config.strengths.map(opt => ({
|
||
label: `${opt.name} (${opt.shortName})`,
|
||
value: opt.name
|
||
}))}
|
||
/>
|
||
<ProFormSelect
|
||
name="humidities"
|
||
label="湿度列表"
|
||
mode="tags"
|
||
placeholder="请输入湿度,按回车确认"
|
||
tooltip="湿度信息会作为SKU的第五个组成部分"
|
||
options={config.humidities.map(opt => ({
|
||
label: `${opt.name} (${opt.shortName})`,
|
||
value: opt.name
|
||
}))}
|
||
/>
|
||
<ProFormSelect
|
||
name="versions"
|
||
label="版本列表"
|
||
mode="tags"
|
||
placeholder="请输入版本,按回车确认"
|
||
tooltip="版本信息会作为SKU的第六个组成部分"
|
||
options={config.versions.map(opt => ({
|
||
label: `${opt.name} (${opt.shortName})`,
|
||
value: opt.name
|
||
}))}
|
||
/>
|
||
<ProFormSelect
|
||
name="sizes"
|
||
label="尺寸列表"
|
||
mode="tags"
|
||
placeholder="请输入尺寸,按回车确认"
|
||
tooltip="尺寸信息会作为SKU的第七个组成部分"
|
||
options={config.sizes.map(opt => ({
|
||
label: `${opt.name} (${opt.shortName})`,
|
||
value: opt.name
|
||
}))}
|
||
/>
|
||
|
||
<ProFormSelect
|
||
name="quantities"
|
||
label="数量列表"
|
||
mode="tags"
|
||
placeholder="请输入数量,按回车确认"
|
||
tooltip="数量信息会作为bundle SKU的组成部分"
|
||
options={config.quantities.map(opt => ({
|
||
label: `${opt.name} (${opt.shortName})`,
|
||
value: opt.name
|
||
}))}
|
||
fieldProps={{ allowClear: true }}
|
||
/>
|
||
|
||
<ProForm.Item
|
||
name="generateBundleSkuForSingle"
|
||
label="为type=single生成bundle产品数据行"
|
||
tooltip="为类型为single的记录生成包含quantity的bundle SKU"
|
||
valuePropName="checked"
|
||
initialValue={true}
|
||
>
|
||
<Checkbox onChange={setGenerateBundleSkuForSingle}>
|
||
启用为single类型生成bundle SKU
|
||
</Checkbox>
|
||
</ProForm.Item>
|
||
</ProForm>
|
||
</Card>
|
||
|
||
{/* 显示所有站点及其shortname */}
|
||
<Card title="3. 所有站点信息" style={{ marginTop: '16px' }}>
|
||
<div style={{ maxHeight: '200px', overflowY: 'auto' }}>
|
||
{sites.length > 0 ? (
|
||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||
<thead>
|
||
<tr style={{ backgroundColor: '#fafafa' }}>
|
||
<th style={{ padding: '8px', textAlign: 'left', borderBottom: '1px solid #e8e8e8' }}>站点名称</th>
|
||
<th style={{ padding: '8px', textAlign: 'left', borderBottom: '1px solid #e8e8e8' }}>ShortName</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{sites.map(site => (
|
||
<tr key={site.id}>
|
||
<td style={{ padding: '8px', borderBottom: '1px solid #e8e8e8' }}>{site.name}</td>
|
||
<td style={{ padding: '8px', borderBottom: '1px solid #e8e8e8', fontWeight: 'bold' }}>{site.skuPrefix}</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
) : (
|
||
<p style={{ textAlign: 'center', color: '#999' }}>暂无站点信息</p>
|
||
)}
|
||
</div>
|
||
<div style={{ marginTop: '10px', fontSize: '12px', color: '#666' }}>
|
||
<p>说明:所有站点的shortName将作为前缀添加到生成的SKU中,以分号分隔。</p>
|
||
</div>
|
||
</Card>
|
||
</Col>
|
||
|
||
{/* 右侧:文件上传与操作 */}
|
||
<Col xs={24} md={14}>
|
||
<Card title="2. 上传文件并操作">
|
||
<Upload
|
||
beforeUpload={handleFileUpload}
|
||
maxCount={1}
|
||
showUploadList={!!file}
|
||
onRemove={() => {
|
||
setFile(null);
|
||
setCsvData([]);
|
||
setProcessedData([]);
|
||
}}
|
||
>
|
||
<Button icon={<UploadOutlined />}>选择 CSV 文件</Button>
|
||
</Upload>
|
||
<div style={{ marginTop: 16 }}>
|
||
<label style={{ display: 'block', marginBottom: 8 }}>
|
||
已上传文件
|
||
</label>
|
||
<Input value={file ? file.name : '暂未选择文件'} readOnly />
|
||
</div>
|
||
<Button
|
||
type="primary"
|
||
onClick={handleProcessData}
|
||
disabled={csvData.length === 0 || isProcessing || selectedSites.length === 0}
|
||
loading={isProcessing}
|
||
style={{ marginTop: '20px' }}
|
||
>
|
||
生成并下载SKU
|
||
</Button>
|
||
|
||
{/* 显示处理结果摘要 */}
|
||
{processedData.length > 0 && (
|
||
<div style={{ marginTop: '20px', padding: '10px', backgroundColor: '#f0f9eb', borderRadius: '4px' }}>
|
||
<p style={{ margin: 0, color: '#52c41a' }}>
|
||
已成功为 {processedData.length} 条产品记录生成SKU!
|
||
</p>
|
||
</div>
|
||
)}
|
||
</Card>
|
||
</Col>
|
||
</Row>
|
||
</PageContainer>
|
||
);
|
||
};
|
||
|
||
export default CsvTool; |