zksu
/
WEB
forked from yoone/WEB
1
0
Fork 0

Compare commits

...

13 Commits

Author SHA1 Message Date
tikkhun db5486ffab feat(CSV工具): 添加产品SKU和名称生成选项
新增两个复选框选项控制是否自动生成产品SKU和名称
移除旧的生成bundle SKU选项并重构相关逻辑
添加SKU重复检查功能
2026-01-27 19:24:05 +08:00
tikkhun aa5f6bcb48 fix(DictItemModal): 为表单字段添加 trim 规范化处理
防止用户输入前后空格导致数据不一致问题
2026-01-27 19:24:05 +08:00
tikkhun 7c0fa5796d feat(产品列表): 添加批量创建套装产品功能
新增批量创建套装产品功能,包含以下主要修改:
1. 添加 BatchCreateBundleModal 组件用于批量创建套装
2. 在产品列表页面添加批量创建套装按钮
3. 实现批量创建逻辑,支持选择单品和数量组合创建套装
2026-01-27 19:24:05 +08:00
zhuotianyuan fcbf0a4833 feat(订单): 添加WinToPay订单导入功能
实现订单导入功能,支持上传Excel文件并解析数据。添加相关API接口和前端上传组件,处理文件上传和格式转换逻辑。同时更新类型定义以支持新功能。
2026-01-26 15:46:21 +08:00
tikkhun 236d0a85bf refactor(product): site skus 的后端进行了分表重构,前端跟随修改。 2026-01-24 10:31:01 +00:00
tikkhun ec57d7c476 fix(Statistics/Sales): 将列标题从'产品名称'改为'产品sku' 2026-01-24 10:31:01 +00:00
tikkhun bd6a2a1509 feat(地图): 添加加拿大和澳大利亚地图组件及路由配置
添加两个新的地图组件用于展示加拿大和澳大利亚的省级地图数据
修复 ProFromSelect 拼写错误为 ProFormSelect
2026-01-24 10:31:01 +00:00
zhuotianyuan 8d3c3ff71c fix(Address): 调整邮箱输入框的宽度为lg 2026-01-23 16:48:47 +08:00
zhuotianyuan 5f1c6eeb44 fix(Order/List): 移除调试日志和无效的表单验证规则
移除console.log调试语句和无效的公司名称必填验证规则
2026-01-23 16:42:10 +08:00
zhuotianyuan 0abe06d9df feat(地址表单): 添加邮箱字段到地址表单和订单列表
在地址更新表单中新增邮箱字段,并在订单列表页面显示该字段
2026-01-23 16:36:03 +08:00
zhuotianyuan 860b7970c8 fix: 修正组件名称拼写错误,将ProFromSelect改为ProFormSelect 2026-01-22 18:02:23 +08:00
zhuotianyuan 67aa625785 feat(api类型): 添加shipmentPlatform和address_id字段到DTO类型 2026-01-22 17:42:05 +08:00
zhuotianyuan 1d9838f72e feat(订单): 添加发货平台选择功能并优化表单验证
- 在发货表单中新增发货平台选择器
- 将公司名称字段改为非必填
- 添加控制台日志用于调试
- 优化地址选择器的数据处理逻辑
2026-01-22 17:35:53 +08:00
22 changed files with 34922 additions and 227 deletions

View File

@ -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 自定义格式化
- 视觉映射配置

View File

@ -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',
}, },

26581
public/australia.json Normal file

File diff suppressed because it is too large Load Diff

7140
public/canada.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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;

View File

@ -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;

View File

@ -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>

View File

@ -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"

View File

@ -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

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

View File

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

View File

@ -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;

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,
@ -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' }]}

View File

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

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();
@ -295,8 +297,8 @@ const SiteList: React.FC = () => {
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}`

View File

View File

@ -42,7 +42,7 @@ const ListPage: React.FC = () => {
hideInTable: true, hideInTable: true,
}, },
{ {
title: '产品名称', title: '产品sku',
dataIndex: 'sku', dataIndex: 'sku',
}, },
{ {

View File

@ -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默认没有生成对象)

View File

@ -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;
/** 更新时间 */ /** 更新时间 */