feat: 产品进行了一定修改 #52

Merged
zhuotianyuan merged 3 commits from zksu/WEB:main into main 2026-01-27 11:24:40 +00:00
14 changed files with 33490 additions and 26248 deletions
Showing only changes of commit db5486ffab - Show all commits

View File

@ -1,15 +1,18 @@
## 实现计划 ## 实现计划
### 1. 地图数据准备 ### 1. 地图数据准备
- 获取加拿大和澳大利亚的省级地图数据JSON格式
- 获取加拿大和澳大利亚的省级地图数据JSON 格式)
- 将地图数据文件添加到项目的`public`目录下,命名为`canada.json`和`australia.json` - 将地图数据文件添加到项目的`public`目录下,命名为`canada.json`和`australia.json`
### 2. 组件实现 ### 2. 组件实现
- 为加拿大创建地图组件:`/Users/zksu/Developer/work/workcode/WEB/src/pages/Area/Canada/index.tsx` - 为加拿大创建地图组件:`/Users/zksu/Developer/work/workcode/WEB/src/pages/Area/Canada/index.tsx`
- 为澳大利亚创建地图组件:`/Users/zksu/Developer/work/workcode/WEB/src/pages/Area/Australia/index.tsx` - 为澳大利亚创建地图组件:`/Users/zksu/Developer/work/workcode/WEB/src/pages/Area/Australia/index.tsx`
- 组件将基于现有的`Map/index.tsx`实现,修改地图数据加载和配置 - 组件将基于现有的`Map/index.tsx`实现,修改地图数据加载和配置
### 3. 数据结构设计 ### 3. 数据结构设计
- 定义省级数据接口,包含: - 定义省级数据接口,包含:
- 英文名称(用于匹配地图数据) - 英文名称(用于匹配地图数据)
- 中文名称 - 中文名称
@ -17,20 +20,24 @@
- 人口数据 - 人口数据
### 4. 地图渲染配置 ### 4. 地图渲染配置
- 使用ECharts的Map组件渲染省级地图
- 配置tooltip显示中文名称、简称和人口数据 - 使用 ECharts 的 Map 组件渲染省级地图
- 配置 tooltip 显示中文名称、简称和人口数据
- 添加视觉映射组件,根据人口数据显示不同颜色 - 添加视觉映射组件,根据人口数据显示不同颜色
### 5. 数据获取与处理 ### 5. 数据获取与处理
- 定义本地数据结构,包含各个省的中文名称、简称和人口 - 定义本地数据结构,包含各个省的中文名称、简称和人口
- 将数据转换为ECharts需要的格式 - 将数据转换为 ECharts 需要的格式
### 6. 组件优化 ### 6. 组件优化
- 添加加载状态 - 添加加载状态
- 处理地图数据加载失败的情况 - 处理地图数据加载失败的情况
- 优化地图交互体验,如缩放、拖拽等 - 优化地图交互体验,如缩放、拖拽等
### 7. 实现步骤 ### 7. 实现步骤
1. 准备地图数据文件 1. 准备地图数据文件
2. 创建加拿大地图组件 2. 创建加拿大地图组件
3. 创建澳大利亚地图组件 3. 创建澳大利亚地图组件
@ -39,8 +46,9 @@
6. 测试地图渲染和交互 6. 测试地图渲染和交互
### 技术要点 ### 技术要点
- 使用ECharts的MapChart组件
- 动态加载地图JSON数据 - 使用 ECharts 的 MapChart 组件
- 动态加载地图 JSON 数据
- 数据映射和转换 - 数据映射和转换
- Tooltip自定义格式化 - Tooltip 自定义格式化
- 视觉映射配置 - 视觉映射配置

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -22,14 +22,54 @@ const AustraliaMap: React.FC = () => {
// 澳大利亚省级数据 // 澳大利亚省级数据
const provinceData: ProvinceData[] = [ const provinceData: ProvinceData[] = [
{ name: 'New South Wales', chineseName: '新南威尔士州', shortName: 'NSW', population: 8166369 }, {
{ name: 'Victoria', chineseName: '维多利亚州', shortName: 'VIC', population: 6704352 }, name: 'New South Wales',
{ name: 'Queensland', chineseName: '昆士兰州', shortName: 'QLD', population: 5265049 }, chineseName: '新南威尔士州',
{ name: 'Western Australia', chineseName: '西澳大利亚州', shortName: 'WA', population: 2885548 }, shortName: 'NSW',
{ name: 'South Australia', chineseName: '南澳大利亚州', shortName: 'SA', population: 1806604 }, population: 8166369,
{ name: 'Tasmania', chineseName: '塔斯马尼亚州', shortName: 'TAS', population: 569825 }, },
{ name: 'Australian Capital Territory', chineseName: '澳大利亚首都领地', shortName: 'ACT', population: 453349 }, {
{ name: 'Northern Territory', chineseName: '北领地', shortName: 'NT', population: 249072 }, name: 'Victoria',
chineseName: '维多利亚州',
shortName: 'VIC',
population: 6704352,
},
{
name: 'Queensland',
chineseName: '昆士兰州',
shortName: 'QLD',
population: 5265049,
},
{
name: 'Western Australia',
chineseName: '西澳大利亚州',
shortName: 'WA',
population: 2885548,
},
{
name: 'South Australia',
chineseName: '南澳大利亚州',
shortName: 'SA',
population: 1806604,
},
{
name: 'Tasmania',
chineseName: '塔斯马尼亚州',
shortName: 'TAS',
population: 569825,
},
{
name: 'Australian Capital Territory',
chineseName: '澳大利亚首都领地',
shortName: 'ACT',
population: 453349,
},
{
name: 'Northern Territory',
chineseName: '北领地',
shortName: 'NT',
population: 249072,
},
]; ];
useEffect(() => { useEffect(() => {
@ -57,7 +97,9 @@ const AustraliaMap: React.FC = () => {
if (params.data) { if (params.data) {
return ` return `
<div> <div>
<div><strong>${params.data.chineseName}</strong> (${params.data.shortName})</div> <div><strong>${params.data.chineseName}</strong> (${
params.data.shortName
})</div>
<div>人口: ${params.data.population.toLocaleString()}</div> <div>人口: ${params.data.population.toLocaleString()}</div>
</div> </div>
`; `;
@ -67,8 +109,8 @@ const AustraliaMap: React.FC = () => {
}, },
visualMap: { visualMap: {
left: 'left', left: 'left',
min: Math.min(...provinceData.map(p => p.population)), min: Math.min(...provinceData.map((p) => p.population)),
max: Math.max(...provinceData.map(p => p.population)), max: Math.max(...provinceData.map((p) => p.population)),
inRange: { inRange: {
color: ['#f0f0f0', '#52c41a', '#389e0d'], color: ['#f0f0f0', '#52c41a', '#389e0d'],
}, },
@ -89,7 +131,9 @@ const AustraliaMap: React.FC = () => {
show: true, show: true,
formatter: (params: any) => { formatter: (params: any) => {
if (params.data) { if (params.data) {
return `${params.data.shortName}\n${params.data.chineseName}\n${params.data.population.toLocaleString()}`; return `${params.data.shortName}\n${
params.data.chineseName
}\n${params.data.population.toLocaleString()}`;
} }
return params.name; return params.name;
}, },
@ -146,4 +190,4 @@ const AustraliaMap: React.FC = () => {
); );
}; };
export default AustraliaMap; export default AustraliaMap;

View File

@ -22,19 +22,84 @@ const CanadaMap: React.FC = () => {
// 加拿大省级数据 // 加拿大省级数据
const provinceData: ProvinceData[] = [ const provinceData: ProvinceData[] = [
{ name: 'British Columbia', chineseName: '不列颠哥伦比亚省', shortName: 'BC', population: 5147712 }, {
{ name: 'Alberta', chineseName: '阿尔伯塔省', shortName: 'AB', population: 4413146 }, name: 'British Columbia',
{ name: 'Saskatchewan', chineseName: '萨斯喀彻温省', shortName: 'SK', population: 1181666 }, chineseName: '不列颠哥伦比亚省',
{ name: 'Manitoba', chineseName: '曼尼托巴省', shortName: 'MB', population: 1383765 }, shortName: 'BC',
{ name: 'Ontario', chineseName: '安大略省', shortName: 'ON', population: 14711827 }, population: 5147712,
{ name: 'Quebec', chineseName: '魁北克省', shortName: 'QC', population: 8501833 }, },
{ name: 'New Brunswick', chineseName: '新不伦瑞克省', shortName: 'NB', population: 789225 }, {
{ name: 'Nova Scotia', chineseName: '新斯科舍省', shortName: 'NS', population: 971395 }, name: 'Alberta',
{ name: 'Prince Edward Island', chineseName: '爱德华王子岛省', shortName: 'PE', population: 160302 }, chineseName: '阿尔伯塔省',
{ name: 'Newfoundland and Labrador', chineseName: '纽芬兰与拉布拉多省', shortName: 'NL', population: 521365 }, shortName: 'AB',
{ name: 'Yukon', chineseName: '育空地区', shortName: 'YT', population: 43985 }, population: 4413146,
{ name: 'Northwest Territories', chineseName: '西北地区', shortName: 'NT', population: 45515 }, },
{ name: 'Nunavut', chineseName: '努纳武特地区', shortName: 'NU', population: 39430 }, {
name: 'Saskatchewan',
chineseName: '萨斯喀彻温省',
shortName: 'SK',
population: 1181666,
},
{
name: 'Manitoba',
chineseName: '曼尼托巴省',
shortName: 'MB',
population: 1383765,
},
{
name: 'Ontario',
chineseName: '安大略省',
shortName: 'ON',
population: 14711827,
},
{
name: 'Quebec',
chineseName: '魁北克省',
shortName: 'QC',
population: 8501833,
},
{
name: 'New Brunswick',
chineseName: '新不伦瑞克省',
shortName: 'NB',
population: 789225,
},
{
name: 'Nova Scotia',
chineseName: '新斯科舍省',
shortName: 'NS',
population: 971395,
},
{
name: 'Prince Edward Island',
chineseName: '爱德华王子岛省',
shortName: 'PE',
population: 160302,
},
{
name: 'Newfoundland and Labrador',
chineseName: '纽芬兰与拉布拉多省',
shortName: 'NL',
population: 521365,
},
{
name: 'Yukon',
chineseName: '育空地区',
shortName: 'YT',
population: 43985,
},
{
name: 'Northwest Territories',
chineseName: '西北地区',
shortName: 'NT',
population: 45515,
},
{
name: 'Nunavut',
chineseName: '努纳武特地区',
shortName: 'NU',
population: 39430,
},
]; ];
useEffect(() => { useEffect(() => {
@ -62,7 +127,9 @@ const CanadaMap: React.FC = () => {
if (params.data) { if (params.data) {
return ` return `
<div> <div>
<div><strong>${params.data.chineseName}</strong> (${params.data.shortName})</div> <div><strong>${params.data.chineseName}</strong> (${
params.data.shortName
})</div>
<div>人口: ${params.data.population.toLocaleString()}</div> <div>人口: ${params.data.population.toLocaleString()}</div>
</div> </div>
`; `;
@ -72,8 +139,8 @@ const CanadaMap: React.FC = () => {
}, },
visualMap: { visualMap: {
left: 'left', left: 'left',
min: Math.min(...provinceData.map(p => p.population)), min: Math.min(...provinceData.map((p) => p.population)),
max: Math.max(...provinceData.map(p => p.population)), max: Math.max(...provinceData.map((p) => p.population)),
inRange: { inRange: {
color: ['#f0f0f0', '#1890ff', '#096dd9'], color: ['#f0f0f0', '#1890ff', '#096dd9'],
}, },
@ -93,7 +160,9 @@ const CanadaMap: React.FC = () => {
show: true, show: true,
formatter: (params: any) => { formatter: (params: any) => {
if (params.data) { if (params.data) {
return `${params.data.shortName}\n${params.data.chineseName}\n${params.data.population.toLocaleString()}`; return `${params.data.shortName}\n${
params.data.chineseName
}\n${params.data.population.toLocaleString()}`;
} }
return params.name; return params.name;
}, },
@ -150,4 +219,4 @@ const CanadaMap: React.FC = () => {
); );
}; };
export default CanadaMap; export default CanadaMap;

View File

@ -4,4 +4,4 @@ export default function Statistic() {
<h1></h1> <h1></h1>
</div> </div>
); );
} }

View File

@ -67,6 +67,8 @@ const CsvTool: React.FC = () => {
const [selectedSites, setSelectedSites] = useState<Site[]>([]); // 现在使用多选 const [selectedSites, setSelectedSites] = useState<Site[]>([]); // 现在使用多选
const [generateBundleSkuForSingle, setGenerateBundleSkuForSingle] = const [generateBundleSkuForSingle, setGenerateBundleSkuForSingle] =
useState(true); // 是否为type为single的记录生成包含quantity的bundle SKU useState(true); // 是否为type为single的记录生成包含quantity的bundle SKU
const [generateSkuForProducts, setGenerateSkuForProducts] = useState(true); // 是否为产品生成SKU
const [generateNameForProducts, setGenerateNameForProducts] = useState(true); // 是否为产品生成名称
const [config, setConfig] = useState<SkuConfig>({ const [config, setConfig] = useState<SkuConfig>({
brands: [], brands: [],
categories: [], categories: [],
@ -528,39 +530,43 @@ const CsvTool: React.FC = () => {
// 获取产品类型 // 获取产品类型
const type = row.type || ''; const type = row.type || '';
// 生成基础SKU不包含站点前缀 // 根据选项决定是否生成SKU
const baseSku = generateSku( let baseSku = row.sku; // 默认为原有SKU
brand, if (generateSkuForProducts) {
version, baseSku = generateSku(
category, brand,
flavor, version,
strength, category,
humidity, flavor,
size, strength,
quantity, humidity,
type, size,
); quantity,
const name = generateName( type,
brand, );
version, }
category,
flavor,
strength,
humidity,
size,
quantity,
type,
);
// 为所有站点生成带前缀的siteSkus
const siteSkus = generateSiteSkus(baseSku);
// 返回包含新SKU和siteSkus的行数据将SKU直接保存到sku栏 // 根据选项决定是否生成名称
let generatedName = row.name; // 默认为原有名称
if (generateNameForProducts) {
generatedName = generateName(
brand,
version,
category,
flavor,
strength,
humidity,
size,
quantity,
type,
);
}
// 返回包含新SKU的行数据将SKU直接保存到sku栏
return { return {
...row, ...row,
sku: baseSku, // 直接生成在sku栏 sku: baseSku, // 根据选项决定是否更新SKU
generatedName: name, name: generateNameForProducts ? generatedName : row.name, // 根据选项决定是否更新名称
// name: name, // 生成的产品名称
siteSkus,
attribute_quantity: quantity, // 确保quantity保存到attribute_quantity attribute_quantity: quantity, // 确保quantity保存到attribute_quantity
}; };
}); });
@ -576,63 +582,94 @@ const CsvTool: React.FC = () => {
); );
// Get quantity values from the config (same source as other attributes like brand) // Get quantity values from the config (same source as other attributes like brand)
const quantityValues = config.quantities.map( const quantityValues = config.quantities
(quantity) => quantity.name, .sort((a, b) => Number(a.name) - Number(b.name))
.map((quantity) => quantity.name);
// 获取源文件中已有的所有 SKU 列表(包括 single 和 bundle用于检查重复
const existingSkus = new Set(
csvData.map((row) => row.sku).filter((sku) => sku),
);
// 获取源文件中已有的 bundle 类型记录的 SKU用于进一步检查
const existingBundleSkus = new Set(
csvData
.filter((row) => row.type === 'bundle')
.map((row) => row.sku)
.filter((sku) => sku),
); );
// Generate bundle products for each single record and quantity // Generate bundle products for each single record and quantity
const generatedBundleRecords = singleRecords.flatMap((singleRecord) => { const generatedBundleRecords = singleRecords
return quantityValues.map((quantity) => { .flatMap((singleRecord) => {
// Extract all necessary attributes from the single record return quantityValues.map((quantity) => {
const brand = singleRecord.attribute_brand || ''; // Extract all necessary attributes from the single record
const version = singleRecord.attribute_version || ''; const brand = singleRecord.attribute_brand || '';
const category = singleRecord.category || ''; const version = singleRecord.attribute_version || '';
const flavor = singleRecord.attribute_flavor || ''; const category = singleRecord.category || '';
const strength = singleRecord.attribute_strength || ''; const flavor = singleRecord.attribute_flavor || '';
const humidity = singleRecord.attribute_humidity || ''; const strength = singleRecord.attribute_strength || '';
const size = singleRecord.attribute_size || singleRecord.size || ''; const humidity = singleRecord.attribute_humidity || '';
// Generate bundle SKU with the quantity const size =
const bundleSku = generateSku( singleRecord.attribute_size || singleRecord.size || '';
brand, // 根据选项决定是否生成bundle SKU
version, let bundleSku;
category, if (generateSkuForProducts) {
flavor, bundleSku = generateSku(
strength, brand,
humidity, version,
size, category,
quantity, flavor,
'bundle', strength,
); humidity,
size,
quantity,
'bundle',
);
} else {
// 如果不生成SKU则使用基于原有SKU和数量的组合
bundleSku = `${singleRecord.sku}-bundle-${quantity}`;
}
// Generate bundle name with the quantity // 检查 bundle SKU 是否已存在于源文件中(包括所有类型的记录)
const bundleName = generateName( if (
brand, existingSkus.has(bundleSku) ||
version, existingBundleSkus.has(bundleSku)
category, ) {
flavor, return null; // 跳过已存在的 SKU
strength, }
humidity,
size,
quantity,
'bundle',
);
// Generate siteSkus for the bundle // 根据选项决定是否生成bundle名称
const bundleSiteSkus = generateSiteSkus(bundleSku); let bundleName;
if (generateNameForProducts) {
// Create the bundle record bundleName = generateName(
return { brand,
...singleRecord, version,
type: 'bundle', // Change type to bundle category,
sku: bundleSku, // Use the new bundle SKU flavor,
name: bundleName, // Use the new bundle name strength,
siteSkus: bundleSiteSkus, humidity,
attribute_quantity: quantity, // Set the attribute_quantity size,
component_1_sku: singleRecord.sku, // Set component_1_sku to the single product's sku quantity,
component_1_quantity: Number(quantity), // Set component_1_quantity to the same as attribute_quantity 'bundle',
}; );
}); } else {
}); // 如果不生成名称,则使用基于原有名称和数量的组合
bundleName = `${singleRecord.name} Bundle x ${quantity}`;
}
// 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
};
});
})
.filter(Boolean); // 过滤掉 null 值
// Combine original dataWithSku with generated bundle records // Combine original dataWithSku with generated bundle records
finalData = [...dataWithSku, ...generatedBundleRecords]; finalData = [...dataWithSku, ...generatedBundleRecords];
@ -764,22 +801,6 @@ const CsvTool: React.FC = () => {
}))} }))}
fieldProps={{ allowClear: true }} fieldProps={{ allowClear: true }}
/> />
<ProForm.Item
name="generateBundleSkuForSingle"
label="为type=single生成bundle产品数据行"
tooltip="为类型为single的记录生成包含quantity的bundle SKU"
valuePropName="checked"
initialValue={true}
>
<Checkbox
onChange={(e) =>
setGenerateBundleSkuForSingle(e.target.checked)
}
>
single类型生成bundle SKU
</Checkbox>
</ProForm.Item>
</ProForm> </ProForm>
</Card> </Card>
@ -869,6 +890,50 @@ const CsvTool: React.FC = () => {
</label> </label>
<Input value={file ? file.name : '暂未选择文件'} readOnly /> <Input value={file ? file.name : '暂未选择文件'} readOnly />
</div> </div>
<ProForm.Item
name="generateSkuForProducts"
label="为产品生成SKU"
tooltip="为所有产品记录生成SKU"
valuePropName="checked"
initialValue={true}
>
<Checkbox
onChange={(e) => setGenerateSkuForProducts(e.target.checked)}
>
SKU
</Checkbox>
</ProForm.Item>
<ProForm.Item
name="generateNameForProducts"
label="为产品生成名称"
tooltip="为所有产品记录生成名称"
valuePropName="checked"
initialValue={true}
>
<Checkbox
onChange={(e) => setGenerateNameForProducts(e.target.checked)}
>
</Checkbox>
</ProForm.Item>
<ProForm.Item
name="generateBundleSkuForSingle"
label="为type=single生成bundle产品数据行"
tooltip="为类型为single的记录生成包含quantity的bundle SKU"
valuePropName="checked"
initialValue={true}
>
<Checkbox
onChange={(e) =>
setGenerateBundleSkuForSingle(e.target.checked)
}
>
single类型生成bundle SKU
</Checkbox>
</ProForm.Item>
<Button <Button
type="primary" type="primary"
onClick={handleProcessData} onClick={handleProcessData}

View File

@ -1,9 +1,18 @@
import React, { useEffect, useState, useMemo } from 'react';
import { PageContainer, ProFormSelect } from '@ant-design/pro-components';
import { Card, Collapse, Divider, Image, Select, Space, Typography, message } from 'antd';
import { categorycontrollerGetall } from '@/servers/api/category'; import { categorycontrollerGetall } from '@/servers/api/category';
import { productcontrollerGetproductlistgrouped } from '@/servers/api/product';
import { dictcontrollerGetdictitems } from '@/servers/api/dict'; import { dictcontrollerGetdictitems } from '@/servers/api/dict';
import { productcontrollerGetproductlistgrouped } from '@/servers/api/product';
import { PageContainer, ProFormSelect } from '@ant-design/pro-components';
import {
Card,
Collapse,
Divider,
Image,
Select,
Space,
Typography,
message,
} from 'antd';
import React, { useEffect, useMemo, useState } from 'react';
// Define interfaces // Define interfaces
interface Category { interface Category {
@ -55,13 +64,22 @@ const ProductCard: React.FC<{ product: Product }> = ({ product }) => {
/> />
</div> */} </div> */}
<div> <div>
<Typography.Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: '4px' }}> <Typography.Text
type="secondary"
style={{ fontSize: 12, display: 'block', marginBottom: '4px' }}
>
{product.sku} {product.sku}
</Typography.Text> </Typography.Text>
<Typography.Text ellipsis style={{ width: '100%', display: 'block', marginBottom: '8px' }}> <Typography.Text
ellipsis
style={{ width: '100%', display: 'block', marginBottom: '8px' }}
>
{product.name} {product.name}
</Typography.Text> </Typography.Text>
<Typography.Text strong style={{ fontSize: 16, color: '#ff4d4f', display: 'block' }}> <Typography.Text
strong
style={{ fontSize: 16, color: '#ff4d4f', display: 'block' }}
>
¥{product.price || '--'} ¥{product.price || '--'}
</Typography.Text> </Typography.Text>
</div> </div>
@ -90,7 +108,11 @@ const ProductGroup: React.FC<{
)} )}
<Typography.Title level={5} style={{ margin: 0 }}> <Typography.Title level={5} style={{ margin: 0 }}>
<span> <span>
{attributeValue?.titleCN || attributeValue?.title || attributeValue?.name || attributeValueId||'未知'} {attributeValue?.titleCN ||
attributeValue?.title ||
attributeValue?.name ||
attributeValueId ||
'未知'}
( {groupProducts.length} ) ( {groupProducts.length} )
</span> </span>
</Typography.Title> </Typography.Title>
@ -108,7 +130,14 @@ const ProductGroup: React.FC<{
key: attributeValueId, key: attributeValueId,
label: panelHeader, label: panelHeader,
children: ( children: (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '16px', paddingTop: '8px' }}> <div
style={{
display: 'flex',
flexWrap: 'wrap',
gap: '16px',
paddingTop: '8px',
}}
>
{groupProducts.map((product) => ( {groupProducts.map((product) => (
<ProductCard key={product.id} product={product} /> <ProductCard key={product.id} product={product} />
))} ))}
@ -126,7 +155,9 @@ const ProductGroupBy: React.FC = () => {
const [categories, setCategories] = useState<Category[]>([]); const [categories, setCategories] = useState<Category[]>([]);
const [selectedCategory, setSelectedCategory] = useState<string | null>(null); const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
// Store selected values for each attribute // Store selected values for each attribute
const [attributeFilters, setAttributeFilters] = useState<{ [key: string]: number | null }>({}); const [attributeFilters, setAttributeFilters] = useState<{
[key: string]: number | null;
}>({});
// Group by attribute // Group by attribute
const [groupByAttribute, setGroupByAttribute] = useState<string | null>(null); const [groupByAttribute, setGroupByAttribute] = useState<string | null>(null);
@ -139,12 +170,16 @@ const ProductGroupBy: React.FC = () => {
// Extract all unique attributes from categories // Extract all unique attributes from categories
const categoryAttributes = useMemo(() => { const categoryAttributes = useMemo(() => {
if (!selectedCategory) return []; if (!selectedCategory) return [];
const categoryItem = categories.find((category: any) => category.name === selectedCategory); const categoryItem = categories.find(
(category: any) => category.name === selectedCategory,
);
if (!categoryItem) return []; if (!categoryItem) return [];
const attributesList: Attribute[] = categoryItem.attributes.map((attribute: any, index) => ({ const attributesList: Attribute[] = categoryItem.attributes.map(
...attribute.attributeDict, (attribute: any, index) => ({
id: index + 1, ...attribute.attributeDict,
})); id: index + 1,
}),
);
return attributesList; return attributesList;
}, [selectedCategory]); }, [selectedCategory]);
@ -152,12 +187,16 @@ const ProductGroupBy: React.FC = () => {
const fetchCategories = async () => { const fetchCategories = async () => {
try { try {
const response = await categorycontrollerGetall(); const response = await categorycontrollerGetall();
const rawCategories = Array.isArray(response) ? response : response?.data || []; const rawCategories = Array.isArray(response)
? response
: response?.data || [];
setCategories(rawCategories); setCategories(rawCategories);
// Set default category // Set default category
if (rawCategories.length > 0) { if (rawCategories.length > 0) {
const defaultCategory = rawCategories.find((category: any) => category.name === 'nicotine-pouches'); const defaultCategory = rawCategories.find(
(category: any) => category.name === 'nicotine-pouches',
);
setSelectedCategory(defaultCategory?.name || rawCategories[0].name); setSelectedCategory(defaultCategory?.name || rawCategories[0].name);
} }
} catch (error) { } catch (error) {
@ -170,16 +209,17 @@ const ProductGroupBy: React.FC = () => {
useEffect(() => { useEffect(() => {
if (!selectedCategory) return; if (!selectedCategory) return;
const category = categories.find(cat => cat.name === selectedCategory); const category = categories.find((cat) => cat.name === selectedCategory);
if (!category) return; if (!category) return;
// Get attributes for this category // Get attributes for this category
const attributesForCategory = categoryAttributes.filter(attr => const attributesForCategory = categoryAttributes.filter(
attr.name === 'brand' || category.attributes.includes(attr.name) (attr) =>
attr.name === 'brand' || category.attributes.includes(attr.name),
); );
// Reset attribute filters when category changes // Reset attribute filters when category changes
const newFilters: { [key: string]: number | null } = {}; const newFilters: { [key: string]: number | null } = {};
attributesForCategory.forEach(attr => { attributesForCategory.forEach((attr) => {
newFilters[attr.name] = null; newFilters[attr.name] = null;
}); });
setAttributeFilters(newFilters); setAttributeFilters(newFilters);
@ -191,8 +231,11 @@ const ProductGroupBy: React.FC = () => {
}, [selectedCategory, categories, categoryAttributes]); }, [selectedCategory, categories, categoryAttributes]);
// Handle attribute filter change // Handle attribute filter change
const handleAttributeFilterChange = (attributeName: string, value: number | null) => { const handleAttributeFilterChange = (
setAttributeFilters(prev => ({ ...prev, [attributeName]: value })); attributeName: string,
value: number | null,
) => {
setAttributeFilters((prev) => ({ ...prev, [attributeName]: value }));
}; };
// Fetch products based on filters // Fetch products based on filters
@ -201,16 +244,15 @@ const ProductGroupBy: React.FC = () => {
setLoading(true); setLoading(true);
try { try {
const params: any = { const params: any = {
category: selectedCategory, category: selectedCategory,
groupBy: groupByAttribute groupBy: groupByAttribute,
}; };
const response = await productcontrollerGetproductlistgrouped(params); const response = await productcontrollerGetproductlistgrouped(params);
const grouped = response?.data || {}; const grouped = response?.data || {};
setGroupedProducts(grouped); setGroupedProducts(grouped);
// Flatten grouped products to get all products // Flatten grouped products to get all products
const allProducts = Object.values(grouped).flat() as Product[]; const allProducts = Object.values(grouped).flat() as Product[];
setProducts(allProducts); setProducts(allProducts);
@ -242,7 +284,9 @@ const ProductGroupBy: React.FC = () => {
<div style={{ padding: '16px', background: '#fff' }}> <div style={{ padding: '16px', background: '#fff' }}>
{/* Filter Section */} {/* Filter Section */}
<div style={{ marginBottom: '24px' }}> <div style={{ marginBottom: '24px' }}>
<Title level={4} style={{ marginBottom: '16px' }}></Title> <Title level={4} style={{ marginBottom: '16px' }}>
</Title>
<Space direction="vertical" size="large"> <Space direction="vertical" size="large">
{/* Category Filter */} {/* Category Filter */}
<div> <div>
@ -256,7 +300,7 @@ const ProductGroupBy: React.FC = () => {
showSearch showSearch
optionFilterProp="children" optionFilterProp="children"
> >
{categories.map(category => ( {categories.map((category) => (
<Option key={category.id} value={category.name}> <Option key={category.id} value={category.name}>
{category.title} {category.title}
</Option> </Option>
@ -268,41 +312,61 @@ const ProductGroupBy: React.FC = () => {
{categoryAttributes.length > 0 && ( {categoryAttributes.length > 0 && (
<div> <div>
<Text strong></Text> <Text strong></Text>
<Space direction="vertical" style={{ marginTop: '8px', width: '100%' }}> <Space
{categoryAttributes.map(attr => ( direction="vertical"
<div key={attr.id} style={{ display: 'flex', alignItems: 'center' }}> style={{ marginTop: '8px', width: '100%' }}
<Text style={{ width: '100px' }}>{attr.title}</Text> >
<ProFormSelect {categoryAttributes.map((attr) => (
placeholder={`请选择${attr.title}`} <div
style={{ width: 300 }} key={attr.id}
value={attributeFilters[attr.name] || null} style={{ display: 'flex', alignItems: 'center' }}
onChange={value => handleAttributeFilterChange(attr.name, value)} >
allowClear <Text style={{ width: '100px' }}>{attr.title}</Text>
showSearch <ProFormSelect
optionFilterProp="children" placeholder={`请选择${attr.title}`}
request={async (params) => { style={{ width: 300 }}
try { value={attributeFilters[attr.name] || null}
console.log('params', params,attr); onChange={(value) =>
const response = await dictcontrollerGetdictitems({ dictId: attr.name }); handleAttributeFilterChange(attr.name, value)
const rawValues = Array.isArray(response) ? response : response?.data?.items || []; }
const filteredValues = rawValues.filter((value: any) => allowClear
value.dictId === attr.name || value.dict?.id === attr.name || value.dict?.name === attr.name showSearch
); optionFilterProp="children"
return { request={async (params) => {
options: filteredValues.map((value: any) => ({ try {
label: `${value.name}${value.titleCN || value.title}`, console.log('params', params, attr);
value: value.id const response = await dictcontrollerGetdictitems({
})) dictId: attr.name,
}; });
} catch (error) { const rawValues = Array.isArray(response)
console.error(`Failed to fetch ${attr.title} values:`, error); ? response
message.error(`获取${attr.title}属性值失败`); : response?.data?.items || [];
return { options: [] }; const filteredValues = rawValues.filter(
} (value: any) =>
}} value.dictId === attr.name ||
/> value.dict?.id === attr.name ||
</div> value.dict?.name === attr.name,
))} );
return {
options: filteredValues.map((value: any) => ({
label: `${value.name}${
value.titleCN || value.title
}`,
value: value.id,
})),
};
} catch (error) {
console.error(
`Failed to fetch ${attr.title} values:`,
error,
);
message.error(`获取${attr.title}属性值失败`);
return { options: [] };
}
}}
/>
</div>
))}
</Space> </Space>
</div> </div>
)} )}
@ -319,7 +383,7 @@ const ProductGroupBy: React.FC = () => {
showSearch showSearch
optionFilterProp="children" optionFilterProp="children"
> >
{categoryAttributes.map(attr => ( {categoryAttributes.map((attr) => (
<Option key={attr.id} value={attr.name}> <Option key={attr.id} value={attr.name}>
{attr.title} {attr.title}
</Option> </Option>
@ -334,28 +398,41 @@ const ProductGroupBy: React.FC = () => {
{/* Products Section */} {/* Products Section */}
<div> <div>
<Title level={4} style={{ marginBottom: '16px' }}> ({products.length} )</Title> <Title level={4} style={{ marginBottom: '16px' }}>
({products.length} )
</Title>
{loading ? ( {loading ? (
<div style={{ textAlign: 'center', padding: '64px' }}> <div style={{ textAlign: 'center', padding: '64px' }}>
<Text>...</Text> <Text>...</Text>
</div> </div>
) : groupByAttribute && Object.keys(groupedProducts).length > 0 ? ( ) : groupByAttribute && Object.keys(groupedProducts).length > 0 ? (
<div style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}> <div
{Object.entries(groupedProducts).map(([attrValueId, groupProducts]) => { style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}
return ( >
<ProductGroup {Object.entries(groupedProducts).map(
key={attrValueId} ([attrValueId, groupProducts]) => {
attributeValueId={attrValueId} return (
groupProducts={groupProducts} <ProductGroup
// attributeValue={} key={attrValueId}
attributeName={groupByAttribute!} attributeValueId={attrValueId}
/> groupProducts={groupProducts}
); // attributeValue={}
})} attributeName={groupByAttribute!}
/>
);
},
)}
</div> </div>
) : ( ) : (
<div style={{ textAlign: 'center', padding: '64px', background: '#fafafa', borderRadius: 8 }}> <div
style={{
textAlign: 'center',
padding: '64px',
background: '#fafafa',
borderRadius: 8,
}}
>
<Text type="secondary"></Text> <Text type="secondary"></Text>
</div> </div>
)} )}

View File

@ -1,8 +1,8 @@
import { productcontrollerCreateproduct, productcontrollerGetproductlist } from '@/servers/api/product';
import { import {
ModalForm, productcontrollerCreateproduct,
ProFormSelect, productcontrollerGetproductlist,
} from '@ant-design/pro-components'; } from '@/servers/api/product';
import { ModalForm, ProFormSelect } from '@ant-design/pro-components';
import { App } from 'antd'; import { App } from 'antd';
import React from 'react'; import React from 'react';
@ -31,12 +31,12 @@ const BatchCreateBundleModal: React.FC<{
modalProps={{ destroyOnClose: true }} modalProps={{ destroyOnClose: true }}
onFinish={async (values) => { onFinish={async (values) => {
const { products, quantity } = values; const { products, quantity } = values;
if (!products || products.length === 0) { if (!products || products.length === 0) {
message.error('请选择至少一个单品'); message.error('请选择至少一个单品');
return false; return false;
} }
// 生成批量创建的 Promise 数组 // 生成批量创建的 Promise 数组
const createPromises = products.flatMap((product: any) => { const createPromises = products.flatMap((product: any) => {
return quantity.map((q: number) => { return quantity.map((q: number) => {
@ -46,10 +46,10 @@ const BatchCreateBundleModal: React.FC<{
name: `套装 ${product.name} x ${q}`, name: `套装 ${product.name} x ${q}`,
type: 'bundle', type: 'bundle',
components: [ components: [
{ {
sku: product.sku, sku: product.sku,
quantity: q quantity: q,
} },
], ],
attributes: [], attributes: [],
}); });
@ -59,10 +59,10 @@ const BatchCreateBundleModal: React.FC<{
try { try {
// 并行执行批量创建 // 并行执行批量创建
const results = await Promise.all(createPromises); const results = await Promise.all(createPromises);
// 检查是否所有创建都成功 // 检查是否所有创建都成功
const allSuccess = results.every((result) => result.success); const allSuccess = results.every((result) => result.success);
if (allSuccess) { if (allSuccess) {
const totalCreated = createPromises.length; const totalCreated = createPromises.length;
message.success(`成功创建 ${totalCreated} 个套装产品`); message.success(`成功创建 ${totalCreated} 个套装产品`);
@ -88,9 +88,7 @@ const BatchCreateBundleModal: React.FC<{
const params = keyWords const params = keyWords
? { sku: keyWords, name: keyWords, type: 'single' } ? { sku: keyWords, name: keyWords, type: 'single' }
: { pageSize: 9999, type: 'single' }; : { pageSize: 9999, type: 'single' };
const { data } = await productcontrollerGetproductlist( const { data } = await productcontrollerGetproductlist(params as any);
params as any,
);
if (!data || !data.items) { if (!data || !data.items) {
return []; return [];
} }
@ -115,4 +113,4 @@ const BatchCreateBundleModal: React.FC<{
); );
}; };
export default BatchCreateBundleModal; export default BatchCreateBundleModal;

View File

@ -6,7 +6,6 @@ import {
productcontrollerGetproductlist, productcontrollerGetproductlist,
productcontrollerUpdateproduct, productcontrollerUpdateproduct,
} from '@/servers/api/product'; } from '@/servers/api/product';
import { sitecontrollerAll } from '@/servers/api/site';
import { stockcontrollerGetstocks as getStocks } from '@/servers/api/stock'; import { stockcontrollerGetstocks as getStocks } from '@/servers/api/stock';
import { import {
ActionType, ActionType,

View File

@ -18,10 +18,10 @@ import {
import { request } from '@umijs/max'; import { request } from '@umijs/max';
import { App, Button, Modal, Popconfirm, Tag, Upload } from 'antd'; import { App, Button, Modal, Popconfirm, Tag, Upload } from 'antd';
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import BatchCreateBundleModal from './BatchCreateBundleModal';
import CreateForm from './CreateForm'; import CreateForm from './CreateForm';
import EditForm from './EditForm'; import EditForm from './EditForm';
import SyncToSiteModal from './SyncToSiteModal'; import SyncToSiteModal from './SyncToSiteModal';
import BatchCreateBundleModal from './BatchCreateBundleModal';
const NameCn: React.FC<{ const NameCn: React.FC<{
id: number; id: number;
@ -189,7 +189,8 @@ const List: React.FC = () => {
const [batchEditModalVisible, setBatchEditModalVisible] = useState(false); const [batchEditModalVisible, setBatchEditModalVisible] = useState(false);
const [syncProducts, setSyncProducts] = useState<API.Product[]>([]); const [syncProducts, setSyncProducts] = useState<API.Product[]>([]);
const [syncModalVisible, setSyncModalVisible] = useState(false); const [syncModalVisible, setSyncModalVisible] = useState(false);
const [batchCreateBundleModalVisible, setBatchCreateBundleModalVisible] = useState(false); const [batchCreateBundleModalVisible, setBatchCreateBundleModalVisible] =
useState(false);
const { message } = App.useApp(); const { message } = App.useApp();
// 导出产品 CSV(带认证请求) // 导出产品 CSV(带认证请求)
@ -466,9 +467,7 @@ const List: React.FC = () => {
</Button>, </Button>,
// 批量创建 bundle 产品按钮 // 批量创建 bundle 产品按钮
<Button <Button onClick={() => setBatchCreateBundleModalVisible(true)}>
onClick={() => setBatchCreateBundleModalVisible(true)}
>
</Button>, </Button>,
// 批量同步按钮 // 批量同步按钮

View File

@ -58,7 +58,9 @@ export interface SiteItem {
const SiteList: React.FC = () => { const SiteList: React.FC = () => {
const actionRef = useRef<ActionType>(); const actionRef = useRef<ActionType>();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [editing, setEditing] = useState<SiteItem & { areas: string[] } | null>(null); const [editing, setEditing] = useState<
(SiteItem & { areas: string[] }) | null
>(null);
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]); const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
const [batchEditOpen, setBatchEditOpen] = useState(false); const [batchEditOpen, setBatchEditOpen] = useState(false);
const [batchEditForm] = Form.useForm(); const [batchEditForm] = Form.useForm();
@ -292,11 +294,11 @@ const SiteList: React.FC = () => {
<Button <Button
size="small" size="small"
onClick={() => { onClick={() => {
function normalEditing(row:SiteItem){ function normalEditing(row: SiteItem) {
return { return {
...row, ...row,
areas: row.areas?.map(area=>area.code) || [], areas: row.areas?.map((area) => area.code) || [],
} };
} }
setEditing(normalEditing(row)); setEditing(normalEditing(row));
setOpen(true); setOpen(true);

View File

@ -15,7 +15,9 @@ const ShopLayout: React.FC = () => {
const location = useLocation(); const location = useLocation();
const [editModalOpen, setEditModalOpen] = useState(false); const [editModalOpen, setEditModalOpen] = useState(false);
const [editingSite, setEditingSite] = useState<SiteItem & { areas: string[] } | null>(null); const [editingSite, setEditingSite] = useState<
(SiteItem & { areas: string[] }) | null
>(null);
const fetchSites = async () => { const fetchSites = async () => {
try { try {
@ -95,7 +97,12 @@ const ShopLayout: React.FC = () => {
<Row gutter={16} style={{ height: 'calc(100vh - 100px)' }}> <Row gutter={16} style={{ height: 'calc(100vh - 100px)' }}>
<Col span={4} style={{ height: '100%' }}> <Col span={4} style={{ height: '100%' }}>
<Sider <Sider
style={{ background: 'white', height: '100%', overflow: 'hidden', zIndex: 1 }} style={{
background: 'white',
height: '100%',
overflow: 'hidden',
zIndex: 1,
}}
> >
<div style={{ padding: '0 10px 16px' }}> <div style={{ padding: '0 10px 16px' }}>
<div <div
@ -103,7 +110,7 @@ const ShopLayout: React.FC = () => {
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
flexWrap: 'wrap', flexWrap: 'wrap',
gap: '4px' gap: '4px',
}} }}
> >
<Select <Select
@ -128,8 +135,8 @@ const ShopLayout: React.FC = () => {
function normalizeEditing(site: SiteItem) { function normalizeEditing(site: SiteItem) {
return { return {
...site, ...site,
areas: site.areas?.map(area => area.code) || [], areas: site.areas?.map((area) => area.code) || [],
} };
} }
setEditingSite(normalizeEditing(currentSite)); setEditingSite(normalizeEditing(currentSite));
setEditModalOpen(true); setEditModalOpen(true);

View File

@ -238,11 +238,9 @@ const OrdersPage: React.FC = () => {
? `快递方式: ${item.shipping_provider}` ? `快递方式: ${item.shipping_provider}`
: ''} : ''}
</span> </span>
{ {item.shipping_method
item.shipping_method ? `发货方式: ${item.shipping_method}`
? `发货方式: ${item.shipping_method}` : ''}
: ''
}
<span> <span>
{item.tracking_number {item.tracking_number
? `物流单号: ${item.tracking_number}` ? `物流单号: ${item.tracking_number}`