feat(产品分类): 在分类页面显示短名称并支持编辑短名称字段

feat(CSV工具): 增强SKU生成功能并支持CSV文件解析

1. 在分类页面显示短名称并支持编辑短名称字段
2. 重构SKU生成逻辑,支持更多属性组合
3. 添加对CSV文件的支持,优化中文编码处理
4. 增加尺寸和数量属性支持
5. 添加为single类型生成bundle SKU的选项
6. 优化表单选项显示,同时展示名称和短名称
This commit is contained in:
tikkhun 2026-01-13 20:36:37 +08:00
parent bf32957f0a
commit 66256acee0
2 changed files with 305 additions and 75 deletions

View File

@ -246,7 +246,7 @@ const CategoryPage: React.FC = () => {
> >
<List.Item.Meta <List.Item.Meta
title={`${item.title}(${item.titleCN ?? '-'})`} title={`${item.title}(${item.titleCN ?? '-'})`}
description={item.name} description={`${item.name} | ${item.shortName ?? '-'}`}
/> />
</List.Item> </List.Item>
)} )}
@ -255,7 +255,7 @@ const CategoryPage: React.FC = () => {
<Content style={{ padding: '24px' }}> <Content style={{ padding: '24px' }}>
{selectedCategory ? ( {selectedCategory ? (
<Card <Card
title={`分类:${selectedCategory.title} (${selectedCategory.name})`} title={`分类:${selectedCategory.title} (${selectedCategory.shortName ?? selectedCategory.name})`}
extra={ extra={
<Button type="primary" onClick={handleAddAttribute}> <Button type="primary" onClick={handleAddAttribute}>
@ -319,6 +319,9 @@ const CategoryPage: React.FC = () => {
<Form.Item name="titleCN" label="中文名称"> <Form.Item name="titleCN" label="中文名称">
<Input /> <Input />
</Form.Item> </Form.Item>
<Form.Item name="shortName" label="短名称">
<Input />
</Form.Item>
<Form.Item name="name" label="标识 (Code)"> <Form.Item name="name" label="标识 (Code)">
<Input /> <Input />
</Form.Item> </Form.Item>

View File

@ -3,10 +3,9 @@ import {
PageContainer, PageContainer,
ProForm, ProForm,
ProFormSelect, ProFormSelect,
ProFormText,
} from '@ant-design/pro-components'; } from '@ant-design/pro-components';
import { request } from '@umijs/max'; 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 React, { useEffect, useState } from 'react';
import * as XLSX from 'xlsx'; import * as XLSX from 'xlsx';
import { productcontrollerGetcategoriesall } from '@/servers/api/product'; import { productcontrollerGetcategoriesall } from '@/servers/api/product';
@ -20,14 +19,22 @@ interface Site {
isDisabled?: boolean; isDisabled?: boolean;
} }
// 定义选项接口,用于下拉选择框的选项
interface Option {
name: string; // 显示名称
shortName: string; // 短名称用于生成SKU
}
// 定义配置接口 // 定义配置接口
interface SkuConfig { interface SkuConfig {
brands: string[]; brands: Option[];
categories: string[]; categories: Option[];
flavors: string[]; flavors: Option[];
strengths: string[]; strengths: Option[];
humidities: string[]; humidities: Option[];
versions: string[]; versions: Option[];
sizes: Option[];
quantities: Option[];
} }
// 定义通用属性映射接口用于存储属性名称和shortName的对应关系 // 定义通用属性映射接口用于存储属性名称和shortName的对应关系
@ -43,6 +50,8 @@ interface AttributeMappings {
strengths: AttributeMapping; strengths: AttributeMapping;
humidities: AttributeMapping; humidities: AttributeMapping;
versions: AttributeMapping; versions: AttributeMapping;
sizes: AttributeMapping;
quantities: AttributeMapping;
} }
/** /**
@ -57,6 +66,7 @@ const CsvTool: React.FC = () => {
const [isProcessing, setIsProcessing] = useState(false); const [isProcessing, setIsProcessing] = useState(false);
const [sites, setSites] = useState<Site[]>([]); const [sites, setSites] = useState<Site[]>([]);
const [selectedSites, setSelectedSites] = useState<Site[]>([]); // 现在使用多选 const [selectedSites, setSelectedSites] = useState<Site[]>([]); // 现在使用多选
const [generateBundleSkuForSingle, setGenerateBundleSkuForSingle] = useState(true); // 是否为type为single的记录生成包含quantity的bundle SKU
const [config, setConfig] = useState<SkuConfig>({ const [config, setConfig] = useState<SkuConfig>({
brands: [], brands: [],
categories: [], categories: [],
@ -64,6 +74,8 @@ const CsvTool: React.FC = () => {
strengths: [], strengths: [],
humidities: [], humidities: [],
versions: [], versions: [],
sizes: [],
quantities: [],
}); });
// 所有属性名称到shortName的映射 // 所有属性名称到shortName的映射
const [attributeMappings, setAttributeMappings] = useState<AttributeMappings>({ const [attributeMappings, setAttributeMappings] = useState<AttributeMappings>({
@ -72,7 +84,9 @@ const CsvTool: React.FC = () => {
flavors: {}, flavors: {},
strengths: {}, strengths: {},
humidities: {}, humidities: {},
versions: {} versions: {},
sizes: {},
quantities: {},
}); });
// 在组件加载时获取站点列表和字典数据 // 在组件加载时获取站点列表和字典数据
@ -92,21 +106,24 @@ const CsvTool: React.FC = () => {
const dictListResponse = await request('/dict/list'); const dictListResponse = await request('/dict/list');
const dictList = dictListResponse?.data || dictListResponse || []; const dictList = dictListResponse?.data || dictListResponse || [];
// 3. 根据字典名称获取字典项返回包含name和shortName的对象数组 // 3. 根据字典名称获取字典项返回包含name和shortName的完整对象数组
const getDictItems = async (dictName: string) => { const getDictItems = async (dictName: string) => {
try { try {
const dict = dictList.find((d: any) => d.name === dictName); const dict = dictList.find((d: any) => d.name === dictName);
if (!dict) { if (!dict) {
console.warn(`Dictionary ${dictName} not found`); console.warn(`Dictionary ${dictName} not found`);
return { names: [], mapping: {} }; return { options: [], mapping: {} };
} }
const itemsResponse = await request('/dict/items', { const itemsResponse = await request('/dict/items', {
params: { dictId: dict.id }, params: { dictId: dict.id },
}); });
const items = itemsResponse?.data || itemsResponse || []; 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的映射 // 创建name到shortName的映射
const mapping = items.reduce((acc: AttributeMapping, item: any) => { const mapping = items.reduce((acc: AttributeMapping, item: any) => {
@ -114,25 +131,30 @@ const CsvTool: React.FC = () => {
return acc; return acc;
}, {}); }, {});
return { names, mapping }; return { options, mapping };
} catch (error) { } catch (error) {
console.error(`Failed to fetch items for ${dictName}:`, error); console.error(`Failed to fetch items for ${dictName}:`, error);
return { names: [], mapping: {} }; return { options: [], mapping: {} };
} }
}; };
// 4. 获取所有字典项(品牌、口味、强度、湿度、版本 // 4. 获取所有字典项(品牌、口味、强度、湿度、版本、尺寸、数量
const [brandResult, flavorResult, strengthResult, humidityResult, versionResult] = await Promise.all([ const [brandResult, flavorResult, strengthResult, humidityResult, versionResult, sizeResult, quantityResult] = await Promise.all([
getDictItems('brand'), getDictItems('brand'),
getDictItems('flavor'), getDictItems('flavor'),
getDictItems('strength'), getDictItems('strength'),
getDictItems('humidity'), getDictItems('humidity'),
getDictItems('version'), getDictItems('version'),
getDictItems('size'),
getDictItems('quantity'),
]); ]);
// 5. 获取商品分类列表 // 5. 获取商品分类列表
const categoriesResponse = await productcontrollerGetcategoriesall(); 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的话 // 商品分类的映射如果分类有shortName的话
const categoryMapping = categoriesResponse?.data?.reduce((acc: AttributeMapping, category: any) => { const categoryMapping = categoriesResponse?.data?.reduce((acc: AttributeMapping, category: any) => {
@ -140,27 +162,42 @@ const CsvTool: React.FC = () => {
return acc; return acc;
}, {}) || {}; }, {}) || {};
// 7. 设置所有属性映射 // 6. 设置所有属性映射
setAttributeMappings({ setAttributeMappings({
brands: brandResult.mapping, brands: brandResult.mapping,
categories: categoryMapping, categories: categoryMapping,
flavors: flavorResult.mapping, flavors: flavorResult.mapping,
strengths: strengthResult.mapping, strengths: strengthResult.mapping,
humidities: humidityResult.mapping, humidities: humidityResult.mapping,
versions: versionResult.mapping versions: versionResult.mapping,
sizes: sizeResult.mapping,
quantities: quantityResult.mapping
}); });
// 更新配置状态 // 更新配置状态
const newConfig = { const newConfig = {
brands: brandResult.names, brands: brandResult.options,
categories, categories: categoryOptions,
flavors: flavorResult.names, flavors: flavorResult.options,
strengths: strengthResult.names, strengths: strengthResult.options,
humidities: humidityResult.names, humidities: humidityResult.options,
versions: versionResult.names, versions: versionResult.options,
sizes: sizeResult.options,
quantities: quantityResult.options,
}; };
setConfig(newConfig); 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' }); message.success({ content: '数据加载成功', key: 'loading' });
} catch (error) { } catch (error) {
@ -184,46 +221,95 @@ const CsvTool: React.FC = () => {
setFile(uploadedFile); setFile(uploadedFile);
const reader = new FileReader(); const reader = new FileReader();
reader.onload = (e) => {
try { // 检查是否为CSV文件
const data = e.target?.result; const isCsvFile = uploadedFile.name.match(/\.csv$/i);
// 如果是ArrayBuffer使用type: 'array'来处理
const workbook = XLSX.read(data, { type: 'array' }); if (isCsvFile) {
const sheetName = workbook.SheetNames[0]; // 对于CSV文件使用readAsText并指定UTF-8编码以正确处理中文
const worksheet = workbook.Sheets[sheetName]; reader.onload = (e) => {
const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 }); try {
const textData = e.target?.result as string;
if (jsonData.length < 2) { // 使用XLSX.read处理CSV文本数据指定type为'csv'并设置编码
message.error('文件为空或缺少表头!'); const workbook = XLSX.read(textData, {
setCsvData([]); type: 'string',
return; codepage: 65001, // UTF-8 encoding
} cellText: true,
cellDates: true
// 将数组转换为对象数组
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; const sheetName = workbook.SheetNames[0];
}); const worksheet = workbook.Sheets[sheetName];
const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
message.success(`成功解析 ${rows.length} 条数据.`); if (jsonData.length < 2) {
setCsvData(rows); message.error('文件为空或缺少表头!');
setProcessedData([]); // 清空旧的处理结果 setCsvData([]);
} catch (error) { return;
message.error('文件解析失败,请检查文件格式!'); }
console.error('File Parse Error:', error);
setCsvData([]); // 将数组转换为对象数组
} 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) => { reader.onerror = (error) => {
message.error('文件读取失败!'); message.error('文件读取失败!');
console.error('File Read Error:', error); console.error('File Read Error:', error);
}; };
// 使用readAsArrayBuffer替代已弃用的readAsBinaryString
reader.readAsArrayBuffer(uploadedFile);
return false; // 阻止antd Upload组件的默认上传行为 return false; // 阻止antd Upload组件的默认上传行为
}; };
@ -252,15 +338,19 @@ const CsvTool: React.FC = () => {
* @param {string} strength - * @param {string} strength -
* @param {string} humidity - 湿 * @param {string} humidity - 湿
* @param {string} version - * @param {string} version -
* @param {string} type -
* @returns {string} SKU * @returns {string} SKU
*/ */
const generateSku = ( const generateSku = (
brand: string, brand: string,
version: string,
category: string, category: string,
flavor: string, flavor: string,
strength: string, strength: string,
humidity: string, humidity: string,
version: string size: string,
quantity?: any,
type?: string
): string => { ): string => {
// 构建SKU组件不包含站点前缀 // 构建SKU组件不包含站点前缀
const skuComponents: string[] = []; const skuComponents: string[] = [];
@ -271,6 +361,11 @@ const CsvTool: React.FC = () => {
const brandShortName = attributeMappings.brands[brand] || brand; const brandShortName = attributeMappings.brands[brand] || brand;
skuComponents.push(brandShortName); skuComponents.push(brandShortName);
} }
if (version) {
// 使用版本的shortName如果没有则使用版本名称
const versionShortName = attributeMappings.versions[version] || version;
skuComponents.push(versionShortName);
}
if (category) { if (category) {
// 使用分类的shortName如果没有则使用分类名称 // 使用分类的shortName如果没有则使用分类名称
const categoryShortName = attributeMappings.categories[category] || category; const categoryShortName = attributeMappings.categories[category] || category;
@ -291,10 +386,22 @@ const CsvTool: React.FC = () => {
const humidityShortName = attributeMappings.humidities[humidity] || humidity; const humidityShortName = attributeMappings.humidities[humidity] || humidity;
skuComponents.push(humidityShortName); skuComponents.push(humidityShortName);
} }
if (version) {
// 使用版本的shortName如果没有则使用版本名称 if (size) {
const versionShortName = attributeMappings.versions[version] || version; // 使用尺寸的shortName如果没有则使用尺寸名称
skuComponents.push(versionShortName); 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 strength = row.attribute_strength || '';
const humidity = row.attribute_humidity || ''; const humidity = row.attribute_humidity || '';
const version = row.attribute_version || ''; 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不包含站点前缀 // 生成基础SKU不包含站点前缀
const baseSku = generateSku(brand, category, flavor, strength, humidity, version); const baseSku = generateSku(brand, version, category, flavor, strength, humidity, size, quantity, type);
// 为所有站点生成带前缀的siteSkus // 为所有站点生成带前缀的siteSkus
const siteSkus = generateSiteSkus(baseSku); const siteSkus = generateSiteSkus(baseSku);
// 返回包含新SKU和siteSkus的行数据 // 返回包含新SKU和siteSkus的行数据将SKU直接保存到sku字段
return { ...row, GeneratedSKU: baseSku, siteSkus }; 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' }); message.success({ content: 'SKU生成成功!正在自动下载...', key: 'processing' });
// 自动下载 // 自动下载 the final data (with or without generated bundle products)
downloadData(dataWithSku); downloadData(finalData);
} catch (error) { } catch (error) {
message.error({ content: '处理失败,请检查配置或文件.', key: 'processing' }); message.error({ content: '处理失败,请检查配置或文件.', key: 'processing' });
console.error('Processing Error:', error); console.error('Processing Error:', error);
@ -397,6 +564,10 @@ const CsvTool: React.FC = () => {
placeholder="请输入品牌,按回车确认" placeholder="请输入品牌,按回车确认"
rules={[{ required: true, message: '至少需要一个品牌' }]} rules={[{ required: true, message: '至少需要一个品牌' }]}
tooltip="品牌名称会作为SKU的第一个组成部分" tooltip="品牌名称会作为SKU的第一个组成部分"
options={config.brands.map(opt => ({
label: `${opt.name} (${opt.shortName})`,
value: opt.name
}))}
/> />
<ProFormSelect <ProFormSelect
name="categories" name="categories"
@ -405,6 +576,10 @@ const CsvTool: React.FC = () => {
placeholder="请输入分类,按回车确认" placeholder="请输入分类,按回车确认"
rules={[{ required: true, message: '至少需要一个分类' }]} rules={[{ required: true, message: '至少需要一个分类' }]}
tooltip="分类名称会作为SKU的第二个组成部分" tooltip="分类名称会作为SKU的第二个组成部分"
options={config.categories.map(opt => ({
label: `${opt.name} (${opt.shortName})`,
value: opt.name
}))}
/> />
<ProFormSelect <ProFormSelect
name="flavors" name="flavors"
@ -413,6 +588,10 @@ const CsvTool: React.FC = () => {
placeholder="请输入口味,按回车确认" placeholder="请输入口味,按回车确认"
rules={[{ required: true, message: '至少需要一个口味' }]} rules={[{ required: true, message: '至少需要一个口味' }]}
tooltip="口味名称会作为SKU的第三个组成部分" tooltip="口味名称会作为SKU的第三个组成部分"
options={config.flavors.map(opt => ({
label: `${opt.name} (${opt.shortName})`,
value: opt.name
}))}
/> />
<ProFormSelect <ProFormSelect
name="strengths" name="strengths"
@ -420,6 +599,10 @@ const CsvTool: React.FC = () => {
mode="tags" mode="tags"
placeholder="请输入强度,按回车确认" placeholder="请输入强度,按回车确认"
tooltip="强度信息会作为SKU的第四个组成部分" tooltip="强度信息会作为SKU的第四个组成部分"
options={config.strengths.map(opt => ({
label: `${opt.name} (${opt.shortName})`,
value: opt.name
}))}
/> />
<ProFormSelect <ProFormSelect
name="humidities" name="humidities"
@ -427,6 +610,10 @@ const CsvTool: React.FC = () => {
mode="tags" mode="tags"
placeholder="请输入湿度,按回车确认" placeholder="请输入湿度,按回车确认"
tooltip="湿度信息会作为SKU的第五个组成部分" tooltip="湿度信息会作为SKU的第五个组成部分"
options={config.humidities.map(opt => ({
label: `${opt.name} (${opt.shortName})`,
value: opt.name
}))}
/> />
<ProFormSelect <ProFormSelect
name="versions" name="versions"
@ -434,7 +621,47 @@ const CsvTool: React.FC = () => {
mode="tags" mode="tags"
placeholder="请输入版本,按回车确认" placeholder="请输入版本,按回车确认"
tooltip="版本信息会作为SKU的第六个组成部分" 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> </ProForm>
</Card> </Card>
@ -453,7 +680,7 @@ const CsvTool: React.FC = () => {
{sites.map(site => ( {sites.map(site => (
<tr key={site.id}> <tr key={site.id}>
<td style={{ padding: '8px', borderBottom: '1px solid #e8e8e8' }}>{site.name}</td> <td style={{ padding: '8px', borderBottom: '1px solid #e8e8e8' }}>{site.name}</td>
<td style={{ padding: '8px', borderBottom: '1px solid #e8e8e8', fontWeight: 'bold' }}>{site.shortName}</td> <td style={{ padding: '8px', borderBottom: '1px solid #e8e8e8', fontWeight: 'bold' }}>{site.skuPrefix}</td>
</tr> </tr>
))} ))}
</tbody> </tbody>