Compare commits
13 Commits
c8064f20e3
...
db5486ffab
| Author | SHA1 | Date |
|---|---|---|
|
|
db5486ffab | |
|
|
aa5f6bcb48 | |
|
|
7c0fa5796d | |
|
|
fcbf0a4833 | |
|
|
236d0a85bf | |
|
|
ec57d7c476 | |
|
|
bd6a2a1509 | |
|
|
8d3c3ff71c | |
|
|
5f1c6eeb44 | |
|
|
0abe06d9df | |
|
|
860b7970c8 | |
|
|
67aa625785 | |
|
|
1d9838f72e |
|
|
@ -0,0 +1,54 @@
|
||||||
|
## 实现计划
|
||||||
|
|
||||||
|
### 1. 地图数据准备
|
||||||
|
|
||||||
|
- 获取加拿大和澳大利亚的省级地图数据(JSON 格式)
|
||||||
|
- 将地图数据文件添加到项目的`public`目录下,命名为`canada.json`和`australia.json`
|
||||||
|
|
||||||
|
### 2. 组件实现
|
||||||
|
|
||||||
|
- 为加拿大创建地图组件:`/Users/zksu/Developer/work/workcode/WEB/src/pages/Area/Canada/index.tsx`
|
||||||
|
- 为澳大利亚创建地图组件:`/Users/zksu/Developer/work/workcode/WEB/src/pages/Area/Australia/index.tsx`
|
||||||
|
- 组件将基于现有的`Map/index.tsx`实现,修改地图数据加载和配置
|
||||||
|
|
||||||
|
### 3. 数据结构设计
|
||||||
|
|
||||||
|
- 定义省级数据接口,包含:
|
||||||
|
- 英文名称(用于匹配地图数据)
|
||||||
|
- 中文名称
|
||||||
|
- 简称
|
||||||
|
- 人口数据
|
||||||
|
|
||||||
|
### 4. 地图渲染配置
|
||||||
|
|
||||||
|
- 使用 ECharts 的 Map 组件渲染省级地图
|
||||||
|
- 配置 tooltip 显示中文名称、简称和人口数据
|
||||||
|
- 添加视觉映射组件,根据人口数据显示不同颜色
|
||||||
|
|
||||||
|
### 5. 数据获取与处理
|
||||||
|
|
||||||
|
- 定义本地数据结构,包含各个省的中文名称、简称和人口
|
||||||
|
- 将数据转换为 ECharts 需要的格式
|
||||||
|
|
||||||
|
### 6. 组件优化
|
||||||
|
|
||||||
|
- 添加加载状态
|
||||||
|
- 处理地图数据加载失败的情况
|
||||||
|
- 优化地图交互体验,如缩放、拖拽等
|
||||||
|
|
||||||
|
### 7. 实现步骤
|
||||||
|
|
||||||
|
1. 准备地图数据文件
|
||||||
|
2. 创建加拿大地图组件
|
||||||
|
3. 创建澳大利亚地图组件
|
||||||
|
4. 配置地图数据和样式
|
||||||
|
5. 添加省级数据
|
||||||
|
6. 测试地图渲染和交互
|
||||||
|
|
||||||
|
### 技术要点
|
||||||
|
|
||||||
|
- 使用 ECharts 的 MapChart 组件
|
||||||
|
- 动态加载地图 JSON 数据
|
||||||
|
- 数据映射和转换
|
||||||
|
- Tooltip 自定义格式化
|
||||||
|
- 视觉映射配置
|
||||||
12
.umirc.ts
12
.umirc.ts
|
|
@ -60,6 +60,16 @@ export default defineConfig({
|
||||||
path: '/area/map',
|
path: '/area/map',
|
||||||
component: './Area/Map',
|
component: './Area/Map',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: '加拿大地图',
|
||||||
|
path: '/area/canada',
|
||||||
|
component: './Area/Canada',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '澳大利亚地图',
|
||||||
|
path: '/area/australia',
|
||||||
|
component: './Area/Australia',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -170,7 +180,7 @@ export default defineConfig({
|
||||||
component: './Product/Permutation',
|
component: './Product/Permutation',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '产品品牌空间',
|
name: '产品聚合空间',
|
||||||
path: '/product/groupBy',
|
path: '/product/groupBy',
|
||||||
component: './Product/GroupBy',
|
component: './Product/GroupBy',
|
||||||
},
|
},
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,193 @@
|
||||||
|
import { Spin, message } from 'antd';
|
||||||
|
import ReactECharts from 'echarts-for-react';
|
||||||
|
import { MapChart } from 'echarts/charts';
|
||||||
|
import { TooltipComponent, VisualMapComponent } from 'echarts/components';
|
||||||
|
import * as echarts from 'echarts/core';
|
||||||
|
import { CanvasRenderer } from 'echarts/renderers';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
// 注册 ECharts 组件
|
||||||
|
echarts.use([TooltipComponent, VisualMapComponent, MapChart, CanvasRenderer]);
|
||||||
|
|
||||||
|
interface ProvinceData {
|
||||||
|
name: string; // 英文名称(用于匹配地图)
|
||||||
|
chineseName: string; // 中文名称
|
||||||
|
shortName: string; // 简称
|
||||||
|
population: number; // 人口数据
|
||||||
|
}
|
||||||
|
|
||||||
|
const AustraliaMap: React.FC = () => {
|
||||||
|
const [option, setOption] = useState({});
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
// 澳大利亚省级数据
|
||||||
|
const provinceData: ProvinceData[] = [
|
||||||
|
{
|
||||||
|
name: 'New South Wales',
|
||||||
|
chineseName: '新南威尔士州',
|
||||||
|
shortName: 'NSW',
|
||||||
|
population: 8166369,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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(() => {
|
||||||
|
const fetchAndSetMapData = async () => {
|
||||||
|
try {
|
||||||
|
// 加载澳大利亚地图数据
|
||||||
|
const australiaMapResponse = await fetch('/australia.json');
|
||||||
|
const australiaMap = await australiaMapResponse.json();
|
||||||
|
echarts.registerMap('australia', australiaMap);
|
||||||
|
|
||||||
|
// 将省级数据转换为 ECharts 需要的格式
|
||||||
|
const mapData = provinceData.map((province) => ({
|
||||||
|
name: province.name,
|
||||||
|
value: province.population,
|
||||||
|
chineseName: province.chineseName,
|
||||||
|
shortName: province.shortName,
|
||||||
|
population: province.population,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 配置 ECharts 地图选项
|
||||||
|
const mapOption = {
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'item',
|
||||||
|
formatter: (params: any) => {
|
||||||
|
if (params.data) {
|
||||||
|
return `
|
||||||
|
<div>
|
||||||
|
<div><strong>${params.data.chineseName}</strong> (${
|
||||||
|
params.data.shortName
|
||||||
|
})</div>
|
||||||
|
<div>人口: ${params.data.population.toLocaleString()}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
return `${params.name}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
visualMap: {
|
||||||
|
left: 'left',
|
||||||
|
min: Math.min(...provinceData.map((p) => p.population)),
|
||||||
|
max: Math.max(...provinceData.map((p) => p.population)),
|
||||||
|
inRange: {
|
||||||
|
color: ['#f0f0f0', '#52c41a', '#389e0d'],
|
||||||
|
},
|
||||||
|
calculable: true,
|
||||||
|
orient: 'vertical',
|
||||||
|
left: 'right',
|
||||||
|
top: 'center',
|
||||||
|
text: ['人口多', '人口少'],
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: 'Australia States',
|
||||||
|
type: 'map',
|
||||||
|
map: 'australia',
|
||||||
|
roam: true,
|
||||||
|
nameProperty: 'STATE_NAME', // 指定地图数据中用于匹配的字段
|
||||||
|
label: {
|
||||||
|
show: true,
|
||||||
|
formatter: (params: any) => {
|
||||||
|
if (params.data) {
|
||||||
|
return `${params.data.shortName}\n${
|
||||||
|
params.data.chineseName
|
||||||
|
}\n${params.data.population.toLocaleString()}`;
|
||||||
|
}
|
||||||
|
return params.name;
|
||||||
|
},
|
||||||
|
fontSize: 12,
|
||||||
|
color: '#333',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
|
emphasis: {
|
||||||
|
label: {
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
|
itemStyle: {
|
||||||
|
areaColor: '#ffc107',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data: mapData,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
setOption(mapOption);
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(`加载地图数据失败: ${error.message}`);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchAndSetMapData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<Spin
|
||||||
|
size="large"
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
height: '80vh',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ReactECharts
|
||||||
|
echarts={echarts}
|
||||||
|
option={option}
|
||||||
|
style={{ height: '80vh', width: '100%' }}
|
||||||
|
notMerge={true}
|
||||||
|
lazyUpdate={true}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AustraliaMap;
|
||||||
|
|
@ -0,0 +1,222 @@
|
||||||
|
import { Spin, message } from 'antd';
|
||||||
|
import ReactECharts from 'echarts-for-react';
|
||||||
|
import { MapChart } from 'echarts/charts';
|
||||||
|
import { TooltipComponent, VisualMapComponent } from 'echarts/components';
|
||||||
|
import * as echarts from 'echarts/core';
|
||||||
|
import { CanvasRenderer } from 'echarts/renderers';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
// 注册 ECharts 组件
|
||||||
|
echarts.use([TooltipComponent, VisualMapComponent, MapChart, CanvasRenderer]);
|
||||||
|
|
||||||
|
interface ProvinceData {
|
||||||
|
name: string; // 英文名称(用于匹配地图)
|
||||||
|
chineseName: string; // 中文名称
|
||||||
|
shortName: string; // 简称
|
||||||
|
population: number; // 人口数据
|
||||||
|
}
|
||||||
|
|
||||||
|
const CanadaMap: React.FC = () => {
|
||||||
|
const [option, setOption] = useState({});
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
// 加拿大省级数据
|
||||||
|
const provinceData: ProvinceData[] = [
|
||||||
|
{
|
||||||
|
name: 'British Columbia',
|
||||||
|
chineseName: '不列颠哥伦比亚省',
|
||||||
|
shortName: 'BC',
|
||||||
|
population: 5147712,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Alberta',
|
||||||
|
chineseName: '阿尔伯塔省',
|
||||||
|
shortName: 'AB',
|
||||||
|
population: 4413146,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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(() => {
|
||||||
|
const fetchAndSetMapData = async () => {
|
||||||
|
try {
|
||||||
|
// 加载加拿大地图数据
|
||||||
|
const canadaMapResponse = await fetch('/canada.json');
|
||||||
|
const canadaMap = await canadaMapResponse.json();
|
||||||
|
echarts.registerMap('canada', canadaMap);
|
||||||
|
|
||||||
|
// 将省级数据转换为 ECharts 需要的格式
|
||||||
|
const mapData = provinceData.map((province) => ({
|
||||||
|
name: province.name,
|
||||||
|
value: province.population,
|
||||||
|
chineseName: province.chineseName,
|
||||||
|
shortName: province.shortName,
|
||||||
|
population: province.population,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 配置 ECharts 地图选项
|
||||||
|
const mapOption = {
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'item',
|
||||||
|
formatter: (params: any) => {
|
||||||
|
if (params.data) {
|
||||||
|
return `
|
||||||
|
<div>
|
||||||
|
<div><strong>${params.data.chineseName}</strong> (${
|
||||||
|
params.data.shortName
|
||||||
|
})</div>
|
||||||
|
<div>人口: ${params.data.population.toLocaleString()}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
return `${params.name}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
visualMap: {
|
||||||
|
left: 'left',
|
||||||
|
min: Math.min(...provinceData.map((p) => p.population)),
|
||||||
|
max: Math.max(...provinceData.map((p) => p.population)),
|
||||||
|
inRange: {
|
||||||
|
color: ['#f0f0f0', '#1890ff', '#096dd9'],
|
||||||
|
},
|
||||||
|
calculable: true,
|
||||||
|
orient: 'vertical',
|
||||||
|
left: 'right',
|
||||||
|
top: 'center',
|
||||||
|
text: ['人口多', '人口少'],
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: 'Canada Provinces',
|
||||||
|
type: 'map',
|
||||||
|
map: 'canada',
|
||||||
|
roam: true,
|
||||||
|
label: {
|
||||||
|
show: true,
|
||||||
|
formatter: (params: any) => {
|
||||||
|
if (params.data) {
|
||||||
|
return `${params.data.shortName}\n${
|
||||||
|
params.data.chineseName
|
||||||
|
}\n${params.data.population.toLocaleString()}`;
|
||||||
|
}
|
||||||
|
return params.name;
|
||||||
|
},
|
||||||
|
fontSize: 12,
|
||||||
|
color: '#333',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
|
emphasis: {
|
||||||
|
label: {
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
|
itemStyle: {
|
||||||
|
areaColor: '#ffc107',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data: mapData,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
setOption(mapOption);
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(`加载地图数据失败: ${error.message}`);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchAndSetMapData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<Spin
|
||||||
|
size="large"
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
height: '80vh',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ReactECharts
|
||||||
|
echarts={echarts}
|
||||||
|
option={option}
|
||||||
|
style={{ height: '80vh', width: '100%' }}
|
||||||
|
notMerge={true}
|
||||||
|
lazyUpdate={true}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CanadaMap;
|
||||||
|
|
@ -66,6 +66,7 @@ const DictItemModal: React.FC<DictItemModalProps> = ({
|
||||||
label="名称"
|
label="名称"
|
||||||
name="name"
|
name="name"
|
||||||
rules={[{ required: true, message: '请输入名称' }]}
|
rules={[{ required: true, message: '请输入名称' }]}
|
||||||
|
normalize={(value) => (value || '').trim()}
|
||||||
>
|
>
|
||||||
<Input placeholder="名称 (e.g., zyn)" />
|
<Input placeholder="名称 (e.g., zyn)" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
@ -73,19 +74,36 @@ const DictItemModal: React.FC<DictItemModalProps> = ({
|
||||||
label="标题"
|
label="标题"
|
||||||
name="title"
|
name="title"
|
||||||
rules={[{ required: true, message: '请输入标题' }]}
|
rules={[{ required: true, message: '请输入标题' }]}
|
||||||
|
normalize={(value) => (value || '').trim()}
|
||||||
>
|
>
|
||||||
<Input placeholder="标题 (e.g., ZYN)" />
|
<Input placeholder="标题 (e.g., ZYN)" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label="中文标题" name="titleCN">
|
<Form.Item
|
||||||
|
label="中文标题"
|
||||||
|
name="titleCN"
|
||||||
|
normalize={(value) => (value || '').trim()}
|
||||||
|
>
|
||||||
<Input placeholder="中文标题 (e.g., 品牌)" />
|
<Input placeholder="中文标题 (e.g., 品牌)" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label="简称 (可选)" name="shortName">
|
<Form.Item
|
||||||
|
label="简称 (可选)"
|
||||||
|
name="shortName"
|
||||||
|
normalize={(value) => (value || '').trim()}
|
||||||
|
>
|
||||||
<Input placeholder="简称 (可选)" />
|
<Input placeholder="简称 (可选)" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label="图片 (可选)" name="image">
|
<Form.Item
|
||||||
|
label="图片 (可选)"
|
||||||
|
name="image"
|
||||||
|
normalize={(value) => (value || '').trim()}
|
||||||
|
>
|
||||||
<Input placeholder="图片链接 (可选)" />
|
<Input placeholder="图片链接 (可选)" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label="值 (可选)" name="value">
|
<Form.Item
|
||||||
|
label="值 (可选)"
|
||||||
|
name="value"
|
||||||
|
normalize={(value) => (value || '').trim()}
|
||||||
|
>
|
||||||
<Input placeholder="值 (可选)" />
|
<Input placeholder="值 (可选)" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
|
||||||
|
|
@ -393,6 +393,14 @@ const UpdateForm: React.FC<{
|
||||||
}));
|
}));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<ProFormText
|
||||||
|
name={['email']}
|
||||||
|
label="邮箱"
|
||||||
|
width="lg"
|
||||||
|
placeholder="请输入邮箱"
|
||||||
|
required
|
||||||
|
rules={[{ required: true, message: '请输入邮箱' }]}
|
||||||
|
/>
|
||||||
<ProForm.Group title="地址">
|
<ProForm.Group title="地址">
|
||||||
<ProFormText
|
<ProFormText
|
||||||
name={['address', 'country']}
|
name={['address', 'country']}
|
||||||
|
|
@ -431,6 +439,8 @@ const UpdateForm: React.FC<{
|
||||||
required
|
required
|
||||||
rules={[{ required: true, message: '请输入详细地址' }]}
|
rules={[{ required: true, message: '请输入详细地址' }]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
||||||
</ProForm.Group>
|
</ProForm.Group>
|
||||||
<ProFormItem
|
<ProFormItem
|
||||||
name="contact"
|
name="contact"
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import styles from '../../../style/order-list.css';
|
||||||
import InternationalPhoneInput from '@/components/InternationalPhoneInput';
|
import InternationalPhoneInput from '@/components/InternationalPhoneInput';
|
||||||
import SyncForm from '@/components/SyncForm';
|
import SyncForm from '@/components/SyncForm';
|
||||||
import { showSyncResult, SyncResultData } from '@/components/SyncResultMessage';
|
import { showSyncResult, SyncResultData } from '@/components/SyncResultMessage';
|
||||||
|
import { UploadOutlined } from '@ant-design/icons';
|
||||||
import { ORDER_STATUS_ENUM } from '@/constants';
|
import { ORDER_STATUS_ENUM } from '@/constants';
|
||||||
import { HistoryOrder } from '@/pages/Statistics/Order';
|
import { HistoryOrder } from '@/pages/Statistics/Order';
|
||||||
import {
|
import {
|
||||||
|
|
@ -24,6 +25,7 @@ import {
|
||||||
ordercontrollerSyncorderbyid,
|
ordercontrollerSyncorderbyid,
|
||||||
ordercontrollerSyncorders,
|
ordercontrollerSyncorders,
|
||||||
ordercontrollerUpdateorderitems,
|
ordercontrollerUpdateorderitems,
|
||||||
|
ordercontrollerImportwintopay,
|
||||||
} from '@/servers/api/order';
|
} from '@/servers/api/order';
|
||||||
import { productcontrollerSearchproducts } from '@/servers/api/product';
|
import { productcontrollerSearchproducts } from '@/servers/api/product';
|
||||||
import { sitecontrollerAll } from '@/servers/api/site';
|
import { sitecontrollerAll } from '@/servers/api/site';
|
||||||
|
|
@ -74,11 +76,16 @@ import {
|
||||||
Tabs,
|
Tabs,
|
||||||
TabsProps,
|
TabsProps,
|
||||||
Tag,
|
Tag,
|
||||||
|
Upload,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import React, { useMemo, useRef, useState } from 'react';
|
import React, { useMemo, useRef, useState } from 'react';
|
||||||
import RelatedOrders from '../../Subscription/Orders/RelatedOrders';
|
import RelatedOrders from '../../Subscription/Orders/RelatedOrders';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import * as XLSX from 'xlsx';
|
||||||
const ListPage: React.FC = () => {
|
const ListPage: React.FC = () => {
|
||||||
|
const [file, setFile] = useState<File | null>(null);
|
||||||
|
const [csvData, setCsvData] = useState<any[]>([]);
|
||||||
|
const [processedData, setProcessedData] = useState<any[]>([]);
|
||||||
const actionRef = useRef<ActionType>();
|
const actionRef = useRef<ActionType>();
|
||||||
const [activeKey, setActiveKey] = useState<string>('all');
|
const [activeKey, setActiveKey] = useState<string>('all');
|
||||||
const [count, setCount] = useState<any[]>([]);
|
const [count, setCount] = useState<any[]>([]);
|
||||||
|
|
@ -468,6 +475,64 @@ const ListPage: React.FC = () => {
|
||||||
];
|
];
|
||||||
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
|
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 处理文件上传
|
||||||
|
*/
|
||||||
|
const handleFileUpload = (uploadedFile: File) => {
|
||||||
|
// 检查文件类型
|
||||||
|
if (!uploadedFile.name.match(/\.(xlsx)$/)) {
|
||||||
|
message.error('请上传 xlsx 格式的文件!');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
setFile(uploadedFile);
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
// 对于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组件的默认上传行为
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer ghost>
|
<PageContainer ghost>
|
||||||
<Tabs items={tabs} activeKey={activeKey} onChange={setActiveKey} />
|
<Tabs items={tabs} activeKey={activeKey} onChange={setActiveKey} />
|
||||||
|
|
@ -495,6 +560,55 @@ const ListPage: React.FC = () => {
|
||||||
}}
|
}}
|
||||||
toolBarRender={() => [
|
toolBarRender={() => [
|
||||||
// <CreateOrder tableRef={actionRef} />,
|
// <CreateOrder tableRef={actionRef} />,
|
||||||
|
<Upload
|
||||||
|
// beforeUpload={handleFileUpload}
|
||||||
|
name="file"
|
||||||
|
accept=".xlsx"
|
||||||
|
showUploadList={false}
|
||||||
|
maxCount={1}
|
||||||
|
customRequest={async (options) => {
|
||||||
|
const { file, onSuccess, onError } = options;
|
||||||
|
console.log(file);
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
try {
|
||||||
|
const res = await request('/order/import', {
|
||||||
|
method: 'POST',
|
||||||
|
data: formData,
|
||||||
|
requestType: 'form',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res?.success && res.data) {
|
||||||
|
// 使用xlsx将JSON数据转换为Excel
|
||||||
|
const XLSX = require('xlsx');
|
||||||
|
const worksheet = XLSX.utils.json_to_sheet(res.data);
|
||||||
|
const workbook = XLSX.utils.book_new();
|
||||||
|
XLSX.utils.book_append_sheet(workbook, worksheet, 'Orders');
|
||||||
|
const excelBuffer = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' });
|
||||||
|
// 否则按原逻辑处理二进制数据
|
||||||
|
const blob = new Blob([excelBuffer], {
|
||||||
|
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
});
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = 'orders.xlsx';
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
message.error(res.message || '导出失败');
|
||||||
|
}
|
||||||
|
actionRef.current?.reload();
|
||||||
|
setSelectedRowKeys([]);
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error('导入失败: ' + (error.message || '未知错误'));
|
||||||
|
onError?.(error);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button icon={<UploadOutlined />}>选择文件</Button>
|
||||||
|
</Upload>,
|
||||||
<SyncForm
|
<SyncForm
|
||||||
onFinish={async (values: any) => {
|
onFinish={async (values: any) => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -1267,7 +1381,10 @@ const Shipping: React.FC<{
|
||||||
const [rates, setRates] = useState<API.RateDTO[]>([]);
|
const [rates, setRates] = useState<API.RateDTO[]>([]);
|
||||||
const [ratesLoading, setRatesLoading] = useState(false);
|
const [ratesLoading, setRatesLoading] = useState(false);
|
||||||
const { message } = App.useApp();
|
const { message } = App.useApp();
|
||||||
|
const [shipmentPlatforms, setShipmentPlatforms] = useState([
|
||||||
|
{ label: 'uniuni', value: 'uniuni' },
|
||||||
|
{ label: 'tms.freightwaves', value: 'freightwaves' },
|
||||||
|
]);
|
||||||
return (
|
return (
|
||||||
<ModalForm
|
<ModalForm
|
||||||
formRef={formRef}
|
formRef={formRef}
|
||||||
|
|
@ -1296,6 +1413,7 @@ const Shipping: React.FC<{
|
||||||
await ordercontrollerGetorderdetail({
|
await ordercontrollerGetorderdetail({
|
||||||
orderId: id,
|
orderId: id,
|
||||||
});
|
});
|
||||||
|
console.log('success data',success,data)
|
||||||
if (!success || !data) return {};
|
if (!success || !data) return {};
|
||||||
data.sales = data.sales?.reduce(
|
data.sales = data.sales?.reduce(
|
||||||
(acc: API.OrderSale[], cur: API.OrderSale) => {
|
(acc: API.OrderSale[], cur: API.OrderSale) => {
|
||||||
|
|
@ -1318,7 +1436,8 @@ const Shipping: React.FC<{
|
||||||
if (reShipping) data.sales = [{}];
|
if (reShipping) data.sales = [{}];
|
||||||
let shipmentInfo = localStorage.getItem('shipmentInfo');
|
let shipmentInfo = localStorage.getItem('shipmentInfo');
|
||||||
if (shipmentInfo) shipmentInfo = JSON.parse(shipmentInfo);
|
if (shipmentInfo) shipmentInfo = JSON.parse(shipmentInfo);
|
||||||
return {
|
const a = {
|
||||||
|
shipmentPlatform: 'uniuni',
|
||||||
...data,
|
...data,
|
||||||
// payment_method_id: shipmentInfo?.payment_method_id,
|
// payment_method_id: shipmentInfo?.payment_method_id,
|
||||||
stockPointId: shipmentInfo?.stockPointId,
|
stockPointId: shipmentInfo?.stockPointId,
|
||||||
|
|
@ -1378,6 +1497,7 @@ const Shipping: React.FC<{
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
return a
|
||||||
}}
|
}}
|
||||||
onFinish={async ({
|
onFinish={async ({
|
||||||
customer_note,
|
customer_note,
|
||||||
|
|
@ -1441,7 +1561,18 @@ const Shipping: React.FC<{
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ProFormText label="订单号" readonly name={'externalOrderId'} />
|
<Row gutter={16}>
|
||||||
|
<Col span={8}>
|
||||||
|
<ProFormSelect
|
||||||
|
name="shipmentPlatform"
|
||||||
|
label="发货平台"
|
||||||
|
options={shipmentPlatforms}
|
||||||
|
placeholder="请选择发货平台"
|
||||||
|
rules={[{ required: true, message: '请选择一个选项' }]}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<ProFormText label="订单号" readonly name='externalOrderId' />
|
||||||
<ProFormText label="客户备注" readonly name="customer_note" />
|
<ProFormText label="客户备注" readonly name="customer_note" />
|
||||||
<ProFormList
|
<ProFormList
|
||||||
label="后台备注"
|
label="后台备注"
|
||||||
|
|
@ -1573,16 +1704,21 @@ const Shipping: React.FC<{
|
||||||
title="发货信息"
|
title="发货信息"
|
||||||
extra={
|
extra={
|
||||||
<AddressPicker
|
<AddressPicker
|
||||||
onChange={({
|
onChange={(row) => {
|
||||||
|
console.log(row);
|
||||||
|
const {
|
||||||
address,
|
address,
|
||||||
phone_number,
|
phone_number,
|
||||||
phone_number_extension,
|
phone_number_extension,
|
||||||
stockPointId,
|
stockPointId,
|
||||||
}) => {
|
email,
|
||||||
|
} = row;
|
||||||
formRef?.current?.setFieldsValue({
|
formRef?.current?.setFieldsValue({
|
||||||
stockPointId,
|
stockPointId,
|
||||||
|
// address_id: row.id,
|
||||||
details: {
|
details: {
|
||||||
origin: {
|
origin: {
|
||||||
|
email_addresses:email,
|
||||||
address,
|
address,
|
||||||
phone_number: {
|
phone_number: {
|
||||||
phone: phone_number,
|
phone: phone_number,
|
||||||
|
|
@ -1595,6 +1731,11 @@ const Shipping: React.FC<{
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
{/* <ProFormText
|
||||||
|
label="address_id"
|
||||||
|
name={'address_id'}
|
||||||
|
rules={[{ required: true, message: '请输入ID' }]}
|
||||||
|
/> */}
|
||||||
<ProFormSelect
|
<ProFormSelect
|
||||||
name="stockPointId"
|
name="stockPointId"
|
||||||
width="md"
|
width="md"
|
||||||
|
|
@ -1687,7 +1828,6 @@ const Shipping: React.FC<{
|
||||||
<ProFormText
|
<ProFormText
|
||||||
label="公司名称"
|
label="公司名称"
|
||||||
name={['details', 'destination', 'name']}
|
name={['details', 'destination', 'name']}
|
||||||
rules={[{ required: true, message: '请输入公司名称' }]}
|
|
||||||
/>
|
/>
|
||||||
<ProFormItem
|
<ProFormItem
|
||||||
name={['details', 'destination', 'address', 'country']}
|
name={['details', 'destination', 'address', 'country']}
|
||||||
|
|
@ -2017,6 +2157,7 @@ const Shipping: React.FC<{
|
||||||
details.origin.phone_number.number =
|
details.origin.phone_number.number =
|
||||||
details.origin.phone_number.phone;
|
details.origin.phone_number.phone;
|
||||||
const res = await logisticscontrollerGetshipmentfee({
|
const res = await logisticscontrollerGetshipmentfee({
|
||||||
|
shipmentPlatform: data.shipmentPlatform,
|
||||||
stockPointId: data.stockPointId,
|
stockPointId: data.stockPointId,
|
||||||
|
|
||||||
sender: details.origin.contact_name,
|
sender: details.origin.contact_name,
|
||||||
|
|
@ -2343,7 +2484,7 @@ const CreateOrder: React.FC<{
|
||||||
<ProFormText
|
<ProFormText
|
||||||
label="公司名称"
|
label="公司名称"
|
||||||
name={['billing', 'company']}
|
name={['billing', 'company']}
|
||||||
rules={[{ required: true, message: '请输入公司名称' }]}
|
rules={[{ message: '请输入公司名称' }]}
|
||||||
/>
|
/>
|
||||||
<ProFormItem
|
<ProFormItem
|
||||||
name={['billing', 'country']}
|
name={['billing', 'country']}
|
||||||
|
|
@ -2429,6 +2570,11 @@ const AddressPicker: React.FC<{
|
||||||
value: item.id,
|
value: item.id,
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'id',
|
||||||
|
dataIndex: ['id'],
|
||||||
|
hideInSearch: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '地区',
|
title: '地区',
|
||||||
|
|
@ -2456,6 +2602,11 @@ const AddressPicker: React.FC<{
|
||||||
`+${record.phone_number_extension} ${record.phone_number}`,
|
`+${record.phone_number_extension} ${record.phone_number}`,
|
||||||
hideInSearch: true,
|
hideInSearch: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: '邮箱',
|
||||||
|
dataIndex: [ 'email'],
|
||||||
|
hideInSearch: true,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
return (
|
return (
|
||||||
<ModalForm
|
<ModalForm
|
||||||
|
|
|
||||||
|
|
@ -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,8 +530,10 @@ const CsvTool: React.FC = () => {
|
||||||
// 获取产品类型
|
// 获取产品类型
|
||||||
const type = row.type || '';
|
const type = row.type || '';
|
||||||
|
|
||||||
// 生成基础SKU(不包含站点前缀)
|
// 根据选项决定是否生成SKU
|
||||||
const baseSku = generateSku(
|
let baseSku = row.sku; // 默认为原有SKU
|
||||||
|
if (generateSkuForProducts) {
|
||||||
|
baseSku = generateSku(
|
||||||
brand,
|
brand,
|
||||||
version,
|
version,
|
||||||
category,
|
category,
|
||||||
|
|
@ -540,27 +544,29 @@ const CsvTool: React.FC = () => {
|
||||||
quantity,
|
quantity,
|
||||||
type,
|
type,
|
||||||
);
|
);
|
||||||
const name = generateName(
|
}
|
||||||
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,12 +582,25 @@ 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
|
||||||
|
.flatMap((singleRecord) => {
|
||||||
return quantityValues.map((quantity) => {
|
return quantityValues.map((quantity) => {
|
||||||
// Extract all necessary attributes from the single record
|
// Extract all necessary attributes from the single record
|
||||||
const brand = singleRecord.attribute_brand || '';
|
const brand = singleRecord.attribute_brand || '';
|
||||||
|
|
@ -590,9 +609,12 @@ const CsvTool: React.FC = () => {
|
||||||
const flavor = singleRecord.attribute_flavor || '';
|
const flavor = singleRecord.attribute_flavor || '';
|
||||||
const strength = singleRecord.attribute_strength || '';
|
const strength = singleRecord.attribute_strength || '';
|
||||||
const humidity = singleRecord.attribute_humidity || '';
|
const humidity = singleRecord.attribute_humidity || '';
|
||||||
const size = singleRecord.attribute_size || singleRecord.size || '';
|
const size =
|
||||||
// Generate bundle SKU with the quantity
|
singleRecord.attribute_size || singleRecord.size || '';
|
||||||
const bundleSku = generateSku(
|
// 根据选项决定是否生成bundle SKU
|
||||||
|
let bundleSku;
|
||||||
|
if (generateSkuForProducts) {
|
||||||
|
bundleSku = generateSku(
|
||||||
brand,
|
brand,
|
||||||
version,
|
version,
|
||||||
category,
|
category,
|
||||||
|
|
@ -603,9 +625,23 @@ const CsvTool: React.FC = () => {
|
||||||
quantity,
|
quantity,
|
||||||
'bundle',
|
'bundle',
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
// 如果不生成SKU,则使用基于原有SKU和数量的组合
|
||||||
|
bundleSku = `${singleRecord.sku}-bundle-${quantity}`;
|
||||||
|
}
|
||||||
|
|
||||||
// Generate bundle name with the quantity
|
// 检查 bundle SKU 是否已存在于源文件中(包括所有类型的记录)
|
||||||
const bundleName = generateName(
|
if (
|
||||||
|
existingSkus.has(bundleSku) ||
|
||||||
|
existingBundleSkus.has(bundleSku)
|
||||||
|
) {
|
||||||
|
return null; // 跳过已存在的 SKU
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据选项决定是否生成bundle名称
|
||||||
|
let bundleName;
|
||||||
|
if (generateNameForProducts) {
|
||||||
|
bundleName = generateName(
|
||||||
brand,
|
brand,
|
||||||
version,
|
version,
|
||||||
category,
|
category,
|
||||||
|
|
@ -616,23 +652,24 @@ const CsvTool: React.FC = () => {
|
||||||
quantity,
|
quantity,
|
||||||
'bundle',
|
'bundle',
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
// Generate siteSkus for the bundle
|
// 如果不生成名称,则使用基于原有名称和数量的组合
|
||||||
const bundleSiteSkus = generateSiteSkus(bundleSku);
|
bundleName = `${singleRecord.name} Bundle x ${quantity}`;
|
||||||
|
}
|
||||||
// Create the bundle record
|
// Create the bundle record
|
||||||
return {
|
return {
|
||||||
...singleRecord,
|
...singleRecord,
|
||||||
type: 'bundle', // Change type to bundle
|
type: 'bundle', // Change type to bundle
|
||||||
sku: bundleSku, // Use the new bundle SKU
|
sku: bundleSku, // Use the new bundle SKU
|
||||||
name: bundleName, // Use the new bundle name
|
name: bundleName, // Use the new bundle name
|
||||||
siteSkus: bundleSiteSkus,
|
// siteSkus: bundleSiteSkus,
|
||||||
attribute_quantity: quantity, // Set the attribute_quantity
|
attribute_quantity: quantity, // Set the attribute_quantity
|
||||||
component_1_sku: singleRecord.sku, // Set component_1_sku to the single product's sku
|
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
|
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}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,18 @@
|
||||||
import React, { useEffect, useState, useMemo } from 'react';
|
|
||||||
import { PageContainer, ProFromSelect } 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: any, index) => ({
|
||||||
...attribute.attributeDict,
|
...attribute.attributeDict,
|
||||||
id: index + 1,
|
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
|
||||||
|
|
@ -203,10 +246,9 @@ const ProductGroupBy: React.FC = () => {
|
||||||
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);
|
||||||
|
|
@ -238,11 +280,13 @@ const ProductGroupBy: React.FC = () => {
|
||||||
const { Title, Text } = Typography;
|
const { Title, Text } = Typography;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer title="品牌空间">
|
<PageContainer title="聚合空间">
|
||||||
<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,34 +312,54 @@ 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%' }}
|
||||||
|
>
|
||||||
|
{categoryAttributes.map((attr) => (
|
||||||
|
<div
|
||||||
|
key={attr.id}
|
||||||
|
style={{ display: 'flex', alignItems: 'center' }}
|
||||||
|
>
|
||||||
<Text style={{ width: '100px' }}>{attr.title}:</Text>
|
<Text style={{ width: '100px' }}>{attr.title}:</Text>
|
||||||
<ProFromSelect
|
<ProFormSelect
|
||||||
placeholder={`请选择${attr.title}`}
|
placeholder={`请选择${attr.title}`}
|
||||||
style={{ width: 300 }}
|
style={{ width: 300 }}
|
||||||
value={attributeFilters[attr.name] || null}
|
value={attributeFilters[attr.name] || null}
|
||||||
onChange={value => handleAttributeFilterChange(attr.name, value)}
|
onChange={(value) =>
|
||||||
|
handleAttributeFilterChange(attr.name, value)
|
||||||
|
}
|
||||||
allowClear
|
allowClear
|
||||||
showSearch
|
showSearch
|
||||||
optionFilterProp="children"
|
optionFilterProp="children"
|
||||||
request={async (params) => {
|
request={async (params) => {
|
||||||
try {
|
try {
|
||||||
console.log('params', params,attr);
|
console.log('params', params, attr);
|
||||||
const response = await dictcontrollerGetdictitems({ dictId: attr.name });
|
const response = await dictcontrollerGetdictitems({
|
||||||
const rawValues = Array.isArray(response) ? response : response?.data?.items || [];
|
dictId: attr.name,
|
||||||
const filteredValues = rawValues.filter((value: any) =>
|
});
|
||||||
value.dictId === attr.name || value.dict?.id === attr.name || value.dict?.name === attr.name
|
const rawValues = Array.isArray(response)
|
||||||
|
? response
|
||||||
|
: response?.data?.items || [];
|
||||||
|
const filteredValues = rawValues.filter(
|
||||||
|
(value: any) =>
|
||||||
|
value.dictId === attr.name ||
|
||||||
|
value.dict?.id === attr.name ||
|
||||||
|
value.dict?.name === attr.name,
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
options: filteredValues.map((value: any) => ({
|
options: filteredValues.map((value: any) => ({
|
||||||
label: `${value.name}${value.titleCN || value.title}`,
|
label: `${value.name}${
|
||||||
value: value.id
|
value.titleCN || value.title
|
||||||
}))
|
}`,
|
||||||
|
value: value.id,
|
||||||
|
})),
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to fetch ${attr.title} values:`, error);
|
console.error(
|
||||||
|
`Failed to fetch ${attr.title} values:`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
message.error(`获取${attr.title}属性值失败`);
|
message.error(`获取${attr.title}属性值失败`);
|
||||||
return { options: [] };
|
return { options: [] };
|
||||||
}
|
}
|
||||||
|
|
@ -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,15 +398,20 @@ 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' }}
|
||||||
|
>
|
||||||
|
{Object.entries(groupedProducts).map(
|
||||||
|
([attrValueId, groupProducts]) => {
|
||||||
return (
|
return (
|
||||||
<ProductGroup
|
<ProductGroup
|
||||||
key={attrValueId}
|
key={attrValueId}
|
||||||
|
|
@ -352,10 +421,18 @@ const ProductGroupBy: React.FC = () => {
|
||||||
attributeName={groupByAttribute!}
|
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>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,116 @@
|
||||||
|
import {
|
||||||
|
productcontrollerCreateproduct,
|
||||||
|
productcontrollerGetproductlist,
|
||||||
|
} from '@/servers/api/product';
|
||||||
|
import { ModalForm, ProFormSelect } from '@ant-design/pro-components';
|
||||||
|
import { App } from 'antd';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const BatchCreateBundleModal: React.FC<{
|
||||||
|
visible: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onSuccess: () => void;
|
||||||
|
}> = ({ visible, onClose, onSuccess }) => {
|
||||||
|
const { message } = App.useApp();
|
||||||
|
|
||||||
|
// 批量创建数量选项
|
||||||
|
const quantityOptions = [
|
||||||
|
{ label: '1', value: 1 },
|
||||||
|
{ label: '5', value: 5 },
|
||||||
|
{ label: '10', value: 10 },
|
||||||
|
{ label: '20', value: 20 },
|
||||||
|
{ label: '50', value: 50 },
|
||||||
|
{ label: '100', value: 100 },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalForm
|
||||||
|
title="批量创建套装产品"
|
||||||
|
open={visible}
|
||||||
|
onOpenChange={(open) => !open && onClose()}
|
||||||
|
modalProps={{ destroyOnClose: true }}
|
||||||
|
onFinish={async (values) => {
|
||||||
|
const { products, quantity } = values;
|
||||||
|
|
||||||
|
if (!products || products.length === 0) {
|
||||||
|
message.error('请选择至少一个单品');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成批量创建的 Promise 数组
|
||||||
|
const createPromises = products.flatMap((product: any) => {
|
||||||
|
return quantity.map((q: number) => {
|
||||||
|
const bundleSku = `bundle-${product.sku}-${q}`;
|
||||||
|
return productcontrollerCreateproduct({
|
||||||
|
sku: bundleSku,
|
||||||
|
name: `套装 ${product.name} x ${q}`,
|
||||||
|
type: 'bundle',
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
sku: product.sku,
|
||||||
|
quantity: q,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
attributes: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 并行执行批量创建
|
||||||
|
const results = await Promise.all(createPromises);
|
||||||
|
|
||||||
|
// 检查是否所有创建都成功
|
||||||
|
const allSuccess = results.every((result) => result.success);
|
||||||
|
|
||||||
|
if (allSuccess) {
|
||||||
|
const totalCreated = createPromises.length;
|
||||||
|
message.success(`成功创建 ${totalCreated} 个套装产品`);
|
||||||
|
onSuccess();
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
message.error('部分产品创建失败,请检查');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error('创建失败,请重试');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ProFormSelect
|
||||||
|
name="products"
|
||||||
|
label="选择单品"
|
||||||
|
mode="multiple"
|
||||||
|
placeholder="请选择要创建套装的单品"
|
||||||
|
rules={[{ required: true, message: '请选择至少一个单品' }]}
|
||||||
|
request={async ({ keyWords }) => {
|
||||||
|
const params = keyWords
|
||||||
|
? { sku: keyWords, name: keyWords, type: 'single' }
|
||||||
|
: { pageSize: 9999, type: 'single' };
|
||||||
|
const { data } = await productcontrollerGetproductlist(params as any);
|
||||||
|
if (!data || !data.items) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
// 只返回类型为单品的产品
|
||||||
|
return data.items
|
||||||
|
.filter((item: any) => item.type === 'single' && item.sku)
|
||||||
|
.map((item: any) => ({
|
||||||
|
label: `${item.sku} - ${item.name}`,
|
||||||
|
value: item,
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ProFormSelect
|
||||||
|
name="quantity"
|
||||||
|
label="选择数量"
|
||||||
|
mode="multiple"
|
||||||
|
options={quantityOptions}
|
||||||
|
placeholder="请选择套装包含的单品数量"
|
||||||
|
rules={[{ required: true, message: '请选择至少一个数量' }]}
|
||||||
|
/>
|
||||||
|
</ModalForm>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BatchCreateBundleModal;
|
||||||
|
|
@ -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,
|
||||||
|
|
@ -35,7 +34,6 @@ const EditForm: React.FC<{
|
||||||
const [stockStatus, setStockStatus] = useState<
|
const [stockStatus, setStockStatus] = useState<
|
||||||
'in-stock' | 'out-of-stock' | null
|
'in-stock' | 'out-of-stock' | null
|
||||||
>(null);
|
>(null);
|
||||||
const [sites, setSites] = useState<any[]>([]);
|
|
||||||
|
|
||||||
const [categories, setCategories] = useState<any[]>([]);
|
const [categories, setCategories] = useState<any[]>([]);
|
||||||
const [activeAttributes, setActiveAttributes] = useState<any[]>([]);
|
const [activeAttributes, setActiveAttributes] = useState<any[]>([]);
|
||||||
|
|
@ -44,10 +42,6 @@ const EditForm: React.FC<{
|
||||||
productcontrollerGetcategoriesall().then((res: any) => {
|
productcontrollerGetcategoriesall().then((res: any) => {
|
||||||
setCategories(res?.data || []);
|
setCategories(res?.data || []);
|
||||||
});
|
});
|
||||||
// 获取站点列表用于站点SKU选择
|
|
||||||
sitecontrollerAll().then((res: any) => {
|
|
||||||
setSites(res?.data || []);
|
|
||||||
});
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -118,9 +112,6 @@ const EditForm: React.FC<{
|
||||||
components: components,
|
components: components,
|
||||||
type: type,
|
type: type,
|
||||||
categoryId: (record as any).categoryId || (record as any).category?.id,
|
categoryId: (record as any).categoryId || (record as any).category?.id,
|
||||||
// 初始化站点SKU为字符串数组
|
|
||||||
// 修改后代码:
|
|
||||||
siteSkus: (record.siteSkus || []).map((code) => ({ code })),
|
|
||||||
};
|
};
|
||||||
}, [record, components, type]);
|
}, [record, components, type]);
|
||||||
return (
|
return (
|
||||||
|
|
@ -187,7 +178,7 @@ const EditForm: React.FC<{
|
||||||
attributes,
|
attributes,
|
||||||
type: values.type, // 直接使用 type
|
type: values.type, // 直接使用 type
|
||||||
categoryId: values.categoryId,
|
categoryId: values.categoryId,
|
||||||
siteSkus: values.siteSkus.map((v: { code: string }) => v.code) || [], // 直接传递字符串数组
|
siteSkus: values.siteSkus.map((v: { sku: string }) => v.sku) || [], // 直接传递字符串数组
|
||||||
// 连带更新 components
|
// 连带更新 components
|
||||||
components:
|
components:
|
||||||
values.type === 'bundle'
|
values.type === 'bundle'
|
||||||
|
|
@ -251,7 +242,7 @@ const EditForm: React.FC<{
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<ProFormText
|
<ProFormText
|
||||||
name="code"
|
name="sku"
|
||||||
width="md"
|
width="md"
|
||||||
placeholder="请输入站点SKU"
|
placeholder="请输入站点SKU"
|
||||||
rules={[{ required: true, message: '请输入站点SKU' }]}
|
rules={[{ required: true, message: '请输入站点SKU' }]}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ 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';
|
||||||
|
|
@ -188,6 +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 { message } = App.useApp();
|
const { message } = App.useApp();
|
||||||
// 导出产品 CSV(带认证请求)
|
// 导出产品 CSV(带认证请求)
|
||||||
|
|
@ -228,7 +231,7 @@ const List: React.FC = () => {
|
||||||
<>
|
<>
|
||||||
{record.siteSkus?.map((siteSku, index) => (
|
{record.siteSkus?.map((siteSku, index) => (
|
||||||
<Tag key={index} color="cyan">
|
<Tag key={index} color="cyan">
|
||||||
{siteSku}
|
{siteSku.sku}
|
||||||
</Tag>
|
</Tag>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
|
|
@ -463,6 +466,10 @@ const List: React.FC = () => {
|
||||||
>
|
>
|
||||||
批量修改
|
批量修改
|
||||||
</Button>,
|
</Button>,
|
||||||
|
// 批量创建 bundle 产品按钮
|
||||||
|
<Button onClick={() => setBatchCreateBundleModalVisible(true)}>
|
||||||
|
批量创建套装
|
||||||
|
</Button>,
|
||||||
// 批量同步按钮
|
// 批量同步按钮
|
||||||
<Button
|
<Button
|
||||||
disabled={selectedRows.length <= 0}
|
disabled={selectedRows.length <= 0}
|
||||||
|
|
@ -578,6 +585,14 @@ const List: React.FC = () => {
|
||||||
actionRef.current?.reload();
|
actionRef.current?.reload();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<BatchCreateBundleModal
|
||||||
|
visible={batchCreateBundleModalVisible}
|
||||||
|
onClose={() => setBatchCreateBundleModalVisible(false)}
|
||||||
|
onSuccess={() => {
|
||||||
|
setBatchCreateBundleModalVisible(false);
|
||||||
|
actionRef.current?.reload();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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}`
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ const ListPage: React.FC = () => {
|
||||||
hideInTable: true,
|
hideInTable: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '产品名称',
|
title: '产品sku',
|
||||||
dataIndex: 'sku',
|
dataIndex: 'sku',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -149,6 +149,45 @@ export async function ordercontrollerGetordersales(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 此处后端没有提供注释 POST /order/import */
|
||||||
|
export async function ordercontrollerImportwintopay(
|
||||||
|
body: {},
|
||||||
|
files?: File[],
|
||||||
|
options?: { [key: string]: any },
|
||||||
|
) {
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
if (files) {
|
||||||
|
files.forEach((f) => formData.append('files', f || ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.keys(body).forEach((ele) => {
|
||||||
|
const item = (body as any)[ele];
|
||||||
|
|
||||||
|
if (item !== undefined && item !== null) {
|
||||||
|
if (typeof item === 'object' && !(item instanceof File)) {
|
||||||
|
if (item instanceof Array) {
|
||||||
|
item.forEach((f) => formData.append(ele, f || ''));
|
||||||
|
} else {
|
||||||
|
formData.append(
|
||||||
|
ele,
|
||||||
|
new Blob([JSON.stringify(item)], { type: 'application/json' }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
formData.append(ele, item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return request<any>('/order/import', {
|
||||||
|
method: 'POST',
|
||||||
|
data: formData,
|
||||||
|
requestType: 'form',
|
||||||
|
...(options || {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/** 此处后端没有提供注释 POST /order/order/cancel/${param0} */
|
/** 此处后端没有提供注释 POST /order/order/cancel/${param0} */
|
||||||
export async function ordercontrollerCancelorder(
|
export async function ordercontrollerCancelorder(
|
||||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
||||||
|
|
|
||||||
|
|
@ -460,6 +460,8 @@ declare namespace API {
|
||||||
tracking_id?: string;
|
tracking_id?: string;
|
||||||
/** 物流单号 */
|
/** 物流单号 */
|
||||||
tracking_number?: string;
|
tracking_number?: string;
|
||||||
|
/** 物流产品代码 */
|
||||||
|
tracking_product_code?: string;
|
||||||
/** 物流公司 */
|
/** 物流公司 */
|
||||||
shipping_provider?: string;
|
shipping_provider?: string;
|
||||||
/** 发货方式 */
|
/** 发货方式 */
|
||||||
|
|
@ -621,7 +623,7 @@ declare namespace API {
|
||||||
shipping_total?: number;
|
shipping_total?: number;
|
||||||
shipping_tax?: number;
|
shipping_tax?: number;
|
||||||
cart_tax?: number;
|
cart_tax?: number;
|
||||||
total?: number;
|
total?: any;
|
||||||
total_tax?: number;
|
total_tax?: number;
|
||||||
customer_id?: number;
|
customer_id?: number;
|
||||||
customer_email?: string;
|
customer_email?: string;
|
||||||
|
|
@ -820,7 +822,7 @@ declare namespace API {
|
||||||
shipping_total?: number;
|
shipping_total?: number;
|
||||||
shipping_tax?: number;
|
shipping_tax?: number;
|
||||||
cart_tax?: number;
|
cart_tax?: number;
|
||||||
total?: number;
|
total?: any;
|
||||||
total_tax?: number;
|
total_tax?: number;
|
||||||
customer_id?: number;
|
customer_id?: number;
|
||||||
customer_email?: string;
|
customer_email?: string;
|
||||||
|
|
@ -1627,12 +1629,14 @@ declare namespace API {
|
||||||
details?: ShippingDetailsDTO;
|
details?: ShippingDetailsDTO;
|
||||||
stockPointId?: number;
|
stockPointId?: number;
|
||||||
orderIds?: number[];
|
orderIds?: number[];
|
||||||
|
shipmentPlatform?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ShipmentFeeBookDTO = {
|
type ShipmentFeeBookDTO = {
|
||||||
|
shipmentPlatform?: string;
|
||||||
stockPointId?: number;
|
stockPointId?: number;
|
||||||
sender?: string;
|
sender?: string;
|
||||||
startPhone?: string;
|
startPhone?: Record<string, any>;
|
||||||
startPostalCode?: string;
|
startPostalCode?: string;
|
||||||
pickupAddress?: string;
|
pickupAddress?: string;
|
||||||
shipperCountryCode?: string;
|
shipperCountryCode?: string;
|
||||||
|
|
@ -1650,6 +1654,7 @@ declare namespace API {
|
||||||
dimensionUom?: string;
|
dimensionUom?: string;
|
||||||
weight?: number;
|
weight?: number;
|
||||||
weightUom?: string;
|
weightUom?: string;
|
||||||
|
address_id?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ShippingAddress = {
|
type ShippingAddress = {
|
||||||
|
|
@ -1660,6 +1665,7 @@ declare namespace API {
|
||||||
phone_number?: string;
|
phone_number?: string;
|
||||||
phone_number_extension?: string;
|
phone_number_extension?: string;
|
||||||
phone_number_country?: string;
|
phone_number_country?: string;
|
||||||
|
email?: string;
|
||||||
/** 创建时间 */
|
/** 创建时间 */
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
/** 更新时间 */
|
/** 更新时间 */
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue