feat(Product): 新增品牌空间页面
refactor: 优化订单列表和统计页面的代码格式和逻辑 style: 调整多个页面的代码格式和导入顺序 fix: 修复订单列表中的物流信息显示问题 chore: 更新路由配置添加品牌空间页面
This commit is contained in:
parent
66256acee0
commit
d0097aec38
|
|
@ -164,6 +164,11 @@ export default defineConfig({
|
||||||
path: '/product/attribute',
|
path: '/product/attribute',
|
||||||
component: './Product/Attribute',
|
component: './Product/Attribute',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: '产品品牌空间',
|
||||||
|
path: '/product/brandspace',
|
||||||
|
component: './Product/BrandSpace',
|
||||||
|
},
|
||||||
// sync
|
// sync
|
||||||
{
|
{
|
||||||
name: '同步产品',
|
name: '同步产品',
|
||||||
|
|
@ -174,7 +179,7 @@ export default defineConfig({
|
||||||
name: '产品CSV 工具',
|
name: '产品CSV 工具',
|
||||||
path: '/product/csvtool',
|
path: '/product/csvtool',
|
||||||
component: './Product/CsvTool',
|
component: './Product/CsvTool',
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useState, useEffect } from 'react';
|
|
||||||
import { sitecontrollerAll } from '@/servers/api/site';
|
import { sitecontrollerAll } from '@/servers/api/site';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
// 站点数据的类型定义
|
// 站点数据的类型定义
|
||||||
interface Site {
|
interface Site {
|
||||||
|
|
|
||||||
|
|
@ -116,7 +116,6 @@ const CustomerList: React.FC = () => {
|
||||||
const { message } = App.useApp();
|
const { message } = App.useApp();
|
||||||
const [syncModalVisible, setSyncModalVisible] = useState(false);
|
const [syncModalVisible, setSyncModalVisible] = useState(false);
|
||||||
|
|
||||||
|
|
||||||
const columns: ProColumns<API.GetCustomerDTO>[] = [
|
const columns: ProColumns<API.GetCustomerDTO>[] = [
|
||||||
{
|
{
|
||||||
title: 'ID',
|
title: 'ID',
|
||||||
|
|
@ -318,25 +317,27 @@ const CustomerList: React.FC = () => {
|
||||||
actionRef={actionRef}
|
actionRef={actionRef}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
request={async (params, sorter,filter) => {
|
request={async (params, sorter, filter) => {
|
||||||
console.log('custoemr request',params, sorter,filter)
|
console.log('custoemr request', params, sorter, filter);
|
||||||
const { current, pageSize, ...restParams } = params;
|
const { current, pageSize, ...restParams } = params;
|
||||||
const orderBy:any = {}
|
const orderBy: any = {};
|
||||||
Object.entries(sorter).forEach(([key, value]) => {
|
Object.entries(sorter).forEach(([key, value]) => {
|
||||||
orderBy[key] = value === 'ascend' ? 'asc' : 'desc';
|
orderBy[key] = value === 'ascend' ? 'asc' : 'desc';
|
||||||
})
|
});
|
||||||
// 构建查询参数
|
// 构建查询参数
|
||||||
const queryParams: any = {
|
const queryParams: any = {
|
||||||
page: current || 1,
|
page: current || 1,
|
||||||
per_page: pageSize || 20,
|
per_page: pageSize || 20,
|
||||||
where: {
|
where: {
|
||||||
...filter,
|
...filter,
|
||||||
...restParams
|
...restParams,
|
||||||
},
|
},
|
||||||
orderBy
|
orderBy,
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await customercontrollerGetcustomerlist({params: queryParams});
|
const result = await customercontrollerGetcustomerlist({
|
||||||
|
params: queryParams,
|
||||||
|
});
|
||||||
console.log(queryParams, result);
|
console.log(queryParams, result);
|
||||||
return {
|
return {
|
||||||
total: result?.data?.total || 0,
|
total: result?.data?.total || 0,
|
||||||
|
|
@ -344,7 +345,6 @@ const CustomerList: React.FC = () => {
|
||||||
success: true,
|
success: true,
|
||||||
};
|
};
|
||||||
}}
|
}}
|
||||||
|
|
||||||
search={{
|
search={{
|
||||||
labelWidth: 'auto',
|
labelWidth: 'auto',
|
||||||
span: 6,
|
span: 6,
|
||||||
|
|
|
||||||
|
|
@ -137,7 +137,8 @@ const ListPage: React.FC = () => {
|
||||||
{
|
{
|
||||||
title: '账单地址',
|
title: '账单地址',
|
||||||
dataIndex: 'billing',
|
dataIndex: 'billing',
|
||||||
render: (_, record) => JSON.stringify(record?.billing || record?.shipping),
|
render: (_, record) =>
|
||||||
|
JSON.stringify(record?.billing || record?.shipping),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '标签',
|
title: '标签',
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@ import {
|
||||||
ProFormTextArea,
|
ProFormTextArea,
|
||||||
ProTable,
|
ProTable,
|
||||||
} from '@ant-design/pro-components';
|
} from '@ant-design/pro-components';
|
||||||
|
import { request } from '@umijs/max';
|
||||||
import {
|
import {
|
||||||
App,
|
App,
|
||||||
Button,
|
Button,
|
||||||
|
|
@ -76,7 +77,6 @@ import {
|
||||||
} 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 { request } from '@umijs/max';
|
|
||||||
|
|
||||||
const ListPage: React.FC = () => {
|
const ListPage: React.FC = () => {
|
||||||
const actionRef = useRef<ActionType>();
|
const actionRef = useRef<ActionType>();
|
||||||
|
|
@ -191,7 +191,7 @@ const ListPage: React.FC = () => {
|
||||||
request: async () => {
|
request: async () => {
|
||||||
try {
|
try {
|
||||||
const result = await sitecontrollerAll();
|
const result = await sitecontrollerAll();
|
||||||
const {success, data}= result
|
const { success, data } = result;
|
||||||
if (success && data) {
|
if (success && data) {
|
||||||
return data.map((site: any) => ({
|
return data.map((site: any) => ({
|
||||||
label: site.name,
|
label: site.name,
|
||||||
|
|
@ -281,9 +281,15 @@ const ListPage: React.FC = () => {
|
||||||
{(record as any)?.fulfillments?.map((item: any) => {
|
{(record as any)?.fulfillments?.map((item: any) => {
|
||||||
if (!item) return;
|
if (!item) return;
|
||||||
return (
|
return (
|
||||||
<div style={{ display:"flex", alignItems:"center",'flexDirection':'column' }}>
|
<div
|
||||||
<span>物流供应商: {item.shipping_provider}</span>
|
style={{
|
||||||
<span>物流单号: {item.tracking_number}</span>
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
flexDirection: 'column',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>物流供应商: {item.shipping_provider}</span>
|
||||||
|
<span>物流单号: {item.tracking_number}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
@ -519,10 +525,12 @@ const ListPage: React.FC = () => {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: {
|
data: {
|
||||||
ids: selectedRowKeys,
|
ids: selectedRowKeys,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
if (res?.success && res.data) {
|
if (res?.success && res.data) {
|
||||||
const blob = new Blob([res.data], { type: 'text/csv;charset=utf-8;' });
|
const blob = new Blob([res.data], {
|
||||||
|
type: 'text/csv;charset=utf-8;',
|
||||||
|
});
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = url;
|
a.href = url;
|
||||||
|
|
@ -539,10 +547,7 @@ const ListPage: React.FC = () => {
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Button type="primary" ghost>
|
||||||
type="primary"
|
|
||||||
ghost
|
|
||||||
>
|
|
||||||
批量导出
|
批量导出
|
||||||
</Button>
|
</Button>
|
||||||
</Popconfirm>,
|
</Popconfirm>,
|
||||||
|
|
@ -634,36 +639,36 @@ const Detail: React.FC<{
|
||||||
)
|
)
|
||||||
? []
|
? []
|
||||||
: [
|
: [
|
||||||
<Divider type="vertical" />,
|
<Divider type="vertical" />,
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
try {
|
try {
|
||||||
if (!record.siteId || !record.externalOrderId) {
|
if (!record.siteId || !record.externalOrderId) {
|
||||||
message.error('站点ID或外部订单ID不存在');
|
message.error('站点ID或外部订单ID不存在');
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
const {
|
||||||
|
success,
|
||||||
|
message: errMsg,
|
||||||
|
data,
|
||||||
|
} = await ordercontrollerSyncorderbyid({
|
||||||
|
siteId: record.siteId,
|
||||||
|
orderId: record.externalOrderId,
|
||||||
|
});
|
||||||
|
if (!success) {
|
||||||
|
throw new Error(errMsg);
|
||||||
|
}
|
||||||
|
showSyncResult(data as SyncResultData, '订单');
|
||||||
|
tableRef.current?.reload();
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(error?.message || '同步失败');
|
||||||
}
|
}
|
||||||
const {
|
}}
|
||||||
success,
|
>
|
||||||
message: errMsg,
|
同步订单
|
||||||
data,
|
</Button>,
|
||||||
} = await ordercontrollerSyncorderbyid({
|
]),
|
||||||
siteId: record.siteId,
|
|
||||||
orderId: record.externalOrderId,
|
|
||||||
});
|
|
||||||
if (!success) {
|
|
||||||
throw new Error(errMsg);
|
|
||||||
}
|
|
||||||
showSyncResult(data as SyncResultData, '订单');
|
|
||||||
tableRef.current?.reload();
|
|
||||||
} catch (error: any) {
|
|
||||||
message.error(error?.message || '同步失败');
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
同步订单
|
|
||||||
</Button>,
|
|
||||||
]),
|
|
||||||
// ...(['processing', 'pending_reshipment'].includes(record.orderStatus)
|
// ...(['processing', 'pending_reshipment'].includes(record.orderStatus)
|
||||||
// ? [
|
// ? [
|
||||||
// <Divider type="vertical" />,
|
// <Divider type="vertical" />,
|
||||||
|
|
@ -682,152 +687,152 @@ const Detail: React.FC<{
|
||||||
'pending_refund',
|
'pending_refund',
|
||||||
].includes(record.orderStatus)
|
].includes(record.orderStatus)
|
||||||
? [
|
? [
|
||||||
<Divider type="vertical" />,
|
<Divider type="vertical" />,
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
title="转至售后"
|
title="转至售后"
|
||||||
description="确认转至售后?"
|
description="确认转至售后?"
|
||||||
onConfirm={async () => {
|
onConfirm={async () => {
|
||||||
try {
|
try {
|
||||||
if (!record.id) {
|
if (!record.id) {
|
||||||
message.error('订单ID不存在');
|
message.error('订单ID不存在');
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
const { success, message: errMsg } =
|
||||||
|
await ordercontrollerChangestatus(
|
||||||
|
{
|
||||||
|
id: record.id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 'after_sale_pending',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (!success) {
|
||||||
|
throw new Error(errMsg);
|
||||||
|
}
|
||||||
|
tableRef.current?.reload();
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(error.message);
|
||||||
}
|
}
|
||||||
const { success, message: errMsg } =
|
}}
|
||||||
await ordercontrollerChangestatus(
|
>
|
||||||
{
|
<Button type="primary" ghost>
|
||||||
id: record.id,
|
转至售后
|
||||||
},
|
</Button>
|
||||||
{
|
</Popconfirm>,
|
||||||
status: 'after_sale_pending',
|
]
|
||||||
},
|
|
||||||
);
|
|
||||||
if (!success) {
|
|
||||||
throw new Error(errMsg);
|
|
||||||
}
|
|
||||||
tableRef.current?.reload();
|
|
||||||
} catch (error: any) {
|
|
||||||
message.error(error.message);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button type="primary" ghost>
|
|
||||||
转至售后
|
|
||||||
</Button>
|
|
||||||
</Popconfirm>,
|
|
||||||
]
|
|
||||||
: []),
|
: []),
|
||||||
...(record.orderStatus === 'after_sale_pending'
|
...(record.orderStatus === 'after_sale_pending'
|
||||||
? [
|
? [
|
||||||
<Divider type="vertical" />,
|
<Divider type="vertical" />,
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
title="转至取消"
|
title="转至取消"
|
||||||
description="确认转至取消?"
|
description="确认转至取消?"
|
||||||
onConfirm={async () => {
|
onConfirm={async () => {
|
||||||
try {
|
try {
|
||||||
if (!record.id) {
|
if (!record.id) {
|
||||||
message.error('订单ID不存在');
|
message.error('订单ID不存在');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { success, message: errMsg } =
|
const { success, message: errMsg } =
|
||||||
await ordercontrollerCancelorder({
|
await ordercontrollerCancelorder({
|
||||||
id: record.id,
|
|
||||||
});
|
|
||||||
if (!success) {
|
|
||||||
throw new Error(errMsg);
|
|
||||||
}
|
|
||||||
tableRef.current?.reload();
|
|
||||||
} catch (error: any) {
|
|
||||||
message.error(error.message);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button type="primary" ghost>
|
|
||||||
转至取消
|
|
||||||
</Button>
|
|
||||||
</Popconfirm>,
|
|
||||||
<Divider type="vertical" />,
|
|
||||||
<Popconfirm
|
|
||||||
title="转至退款"
|
|
||||||
description="确认转至退款?"
|
|
||||||
onConfirm={async () => {
|
|
||||||
try {
|
|
||||||
if (!record.id) {
|
|
||||||
message.error('订单ID不存在');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const { success, message: errMsg } =
|
|
||||||
await ordercontrollerRefundorder({
|
|
||||||
id: record.id,
|
|
||||||
});
|
|
||||||
if (!success) {
|
|
||||||
throw new Error(errMsg);
|
|
||||||
}
|
|
||||||
tableRef.current?.reload();
|
|
||||||
} catch (error: any) {
|
|
||||||
message.error(error.message);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button type="primary" ghost>
|
|
||||||
转至退款
|
|
||||||
</Button>
|
|
||||||
</Popconfirm>,
|
|
||||||
<Divider type="vertical" />,
|
|
||||||
<Popconfirm
|
|
||||||
title="转至完成"
|
|
||||||
description="确认转至完成?"
|
|
||||||
onConfirm={async () => {
|
|
||||||
try {
|
|
||||||
if (!record.id) {
|
|
||||||
message.error('订单ID不存在');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const { success, message: errMsg } =
|
|
||||||
await ordercontrollerCompletedorder({
|
|
||||||
id: record.id,
|
|
||||||
});
|
|
||||||
if (!success) {
|
|
||||||
throw new Error(errMsg);
|
|
||||||
}
|
|
||||||
tableRef.current?.reload();
|
|
||||||
} catch (error: any) {
|
|
||||||
message.error(error.message);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button type="primary" ghost>
|
|
||||||
转至完成
|
|
||||||
</Button>
|
|
||||||
</Popconfirm>,
|
|
||||||
<Divider type="vertical" />,
|
|
||||||
<Popconfirm
|
|
||||||
title="转至待补发"
|
|
||||||
description="确认转至待补发?"
|
|
||||||
onConfirm={async () => {
|
|
||||||
try {
|
|
||||||
const { success, message: errMsg } =
|
|
||||||
await ordercontrollerChangestatus(
|
|
||||||
{
|
|
||||||
id: record.id,
|
id: record.id,
|
||||||
},
|
});
|
||||||
{
|
if (!success) {
|
||||||
status: 'pending_reshipment',
|
throw new Error(errMsg);
|
||||||
},
|
}
|
||||||
);
|
tableRef.current?.reload();
|
||||||
if (!success) {
|
} catch (error: any) {
|
||||||
throw new Error(errMsg);
|
message.error(error.message);
|
||||||
}
|
}
|
||||||
tableRef.current?.reload();
|
}}
|
||||||
} catch (error: any) {
|
>
|
||||||
message.error(error.message);
|
<Button type="primary" ghost>
|
||||||
}
|
转至取消
|
||||||
}}
|
</Button>
|
||||||
>
|
</Popconfirm>,
|
||||||
<Button type="primary" ghost>
|
<Divider type="vertical" />,
|
||||||
转至待补发
|
<Popconfirm
|
||||||
</Button>
|
title="转至退款"
|
||||||
</Popconfirm>,
|
description="确认转至退款?"
|
||||||
]
|
onConfirm={async () => {
|
||||||
|
try {
|
||||||
|
if (!record.id) {
|
||||||
|
message.error('订单ID不存在');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { success, message: errMsg } =
|
||||||
|
await ordercontrollerRefundorder({
|
||||||
|
id: record.id,
|
||||||
|
});
|
||||||
|
if (!success) {
|
||||||
|
throw new Error(errMsg);
|
||||||
|
}
|
||||||
|
tableRef.current?.reload();
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(error.message);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button type="primary" ghost>
|
||||||
|
转至退款
|
||||||
|
</Button>
|
||||||
|
</Popconfirm>,
|
||||||
|
<Divider type="vertical" />,
|
||||||
|
<Popconfirm
|
||||||
|
title="转至完成"
|
||||||
|
description="确认转至完成?"
|
||||||
|
onConfirm={async () => {
|
||||||
|
try {
|
||||||
|
if (!record.id) {
|
||||||
|
message.error('订单ID不存在');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { success, message: errMsg } =
|
||||||
|
await ordercontrollerCompletedorder({
|
||||||
|
id: record.id,
|
||||||
|
});
|
||||||
|
if (!success) {
|
||||||
|
throw new Error(errMsg);
|
||||||
|
}
|
||||||
|
tableRef.current?.reload();
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(error.message);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button type="primary" ghost>
|
||||||
|
转至完成
|
||||||
|
</Button>
|
||||||
|
</Popconfirm>,
|
||||||
|
<Divider type="vertical" />,
|
||||||
|
<Popconfirm
|
||||||
|
title="转至待补发"
|
||||||
|
description="确认转至待补发?"
|
||||||
|
onConfirm={async () => {
|
||||||
|
try {
|
||||||
|
const { success, message: errMsg } =
|
||||||
|
await ordercontrollerChangestatus(
|
||||||
|
{
|
||||||
|
id: record.id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 'pending_reshipment',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (!success) {
|
||||||
|
throw new Error(errMsg);
|
||||||
|
}
|
||||||
|
tableRef.current?.reload();
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(error.message);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button type="primary" ghost>
|
||||||
|
转至待补发
|
||||||
|
</Button>
|
||||||
|
</Popconfirm>,
|
||||||
|
]
|
||||||
: []),
|
: []),
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
|
|
@ -1089,31 +1094,31 @@ const Detail: React.FC<{
|
||||||
}
|
}
|
||||||
actions={
|
actions={
|
||||||
v.state === 'waiting-for-scheduling' ||
|
v.state === 'waiting-for-scheduling' ||
|
||||||
v.state === 'waiting-for-transit'
|
v.state === 'waiting-for-transit'
|
||||||
? [
|
? [
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
title="取消运单"
|
title="取消运单"
|
||||||
description="确认取消运单?"
|
description="确认取消运单?"
|
||||||
onConfirm={async () => {
|
onConfirm={async () => {
|
||||||
try {
|
try {
|
||||||
const { success, message: errMsg } =
|
const { success, message: errMsg } =
|
||||||
await logisticscontrollerDelshipment({
|
await logisticscontrollerDelshipment({
|
||||||
id: v.id,
|
id: v.id,
|
||||||
});
|
});
|
||||||
if (!success) {
|
if (!success) {
|
||||||
throw new Error(errMsg);
|
throw new Error(errMsg);
|
||||||
|
}
|
||||||
|
tableRef.current?.reload();
|
||||||
|
initRequest();
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(error.message);
|
||||||
}
|
}
|
||||||
tableRef.current?.reload();
|
}}
|
||||||
initRequest();
|
>
|
||||||
} catch (error: any) {
|
<DeleteFilled />
|
||||||
message.error(error.message);
|
取消运单
|
||||||
}
|
</Popconfirm>,
|
||||||
}}
|
]
|
||||||
>
|
|
||||||
<DeleteFilled />
|
|
||||||
取消运单
|
|
||||||
</Popconfirm>,
|
|
||||||
]
|
|
||||||
: []
|
: []
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
@ -1501,16 +1506,16 @@ const Shipping: React.FC<{
|
||||||
<ProFormList
|
<ProFormList
|
||||||
label="发货产品"
|
label="发货产品"
|
||||||
name="sales"
|
name="sales"
|
||||||
// rules={[
|
// rules={[
|
||||||
// {
|
// {
|
||||||
// required: true,
|
// required: true,
|
||||||
// message: '至少需要一个商品',
|
// message: '至少需要一个商品',
|
||||||
// validator: (_, value) =>
|
// validator: (_, value) =>
|
||||||
// value && value.length > 0
|
// value && value.length > 0
|
||||||
// ? Promise.resolve()
|
// ? Promise.resolve()
|
||||||
// : Promise.reject('至少需要一个商品'),
|
// : Promise.reject('至少需要一个商品'),
|
||||||
// },
|
// },
|
||||||
// ]}
|
// ]}
|
||||||
>
|
>
|
||||||
<ProForm.Group>
|
<ProForm.Group>
|
||||||
<ProFormSelect
|
<ProFormSelect
|
||||||
|
|
@ -1935,7 +1940,7 @@ const Shipping: React.FC<{
|
||||||
name="description"
|
name="description"
|
||||||
placeholder="请输入描述"
|
placeholder="请输入描述"
|
||||||
width="lg"
|
width="lg"
|
||||||
// rules={[{ required: true, message: '请输入描述' }]}
|
// rules={[{ required: true, message: '请输入描述' }]}
|
||||||
/>
|
/>
|
||||||
</ProForm.Group>
|
</ProForm.Group>
|
||||||
</ProFormList>
|
</ProFormList>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import * as dictApi from '@/servers/api/dict';
|
||||||
import {
|
import {
|
||||||
ActionType,
|
ActionType,
|
||||||
PageContainer,
|
PageContainer,
|
||||||
|
ProColumns,
|
||||||
ProTable,
|
ProTable,
|
||||||
} from '@ant-design/pro-components';
|
} from '@ant-design/pro-components';
|
||||||
import { request } from '@umijs/max';
|
import { request } from '@umijs/max';
|
||||||
|
|
@ -219,11 +220,35 @@ const AttributePage: React.FC = () => {
|
||||||
];
|
];
|
||||||
|
|
||||||
// 右侧字典项列表列定义(紧凑样式)
|
// 右侧字典项列表列定义(紧凑样式)
|
||||||
const dictItemColumns: any[] = [
|
const dictItemColumns: ProColumns<any>[] = [
|
||||||
{ title: '名称', dataIndex: 'name', key: 'name', copyable: true },
|
{
|
||||||
{ title: '标题', dataIndex: 'title', key: 'title', copyable: true },
|
title: '名称',
|
||||||
{ title: '中文标题', dataIndex: 'titleCN', key: 'titleCN', copyable: true },
|
dataIndex: 'name',
|
||||||
{ title: '简称', dataIndex: 'shortName', key: 'shortName', copyable: true },
|
key: 'name',
|
||||||
|
copyable: true,
|
||||||
|
sorter: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '标题',
|
||||||
|
dataIndex: 'title',
|
||||||
|
key: 'title',
|
||||||
|
copyable: true,
|
||||||
|
sorter: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '中文标题',
|
||||||
|
dataIndex: 'titleCN',
|
||||||
|
key: 'titleCN',
|
||||||
|
copyable: true,
|
||||||
|
sorter: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '简称',
|
||||||
|
dataIndex: 'shortName',
|
||||||
|
key: 'shortName',
|
||||||
|
copyable: true,
|
||||||
|
sorter: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: '图片',
|
title: '图片',
|
||||||
dataIndex: 'image',
|
dataIndex: 'image',
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,452 @@
|
||||||
|
import { PageContainer } from '@ant-design/pro-components';
|
||||||
|
import { request } from '@umijs/max';
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
Col,
|
||||||
|
Image,
|
||||||
|
Layout,
|
||||||
|
Row,
|
||||||
|
Select,
|
||||||
|
Space,
|
||||||
|
Typography,
|
||||||
|
message,
|
||||||
|
} from 'antd';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
const { Sider, Content } = Layout;
|
||||||
|
const { Title, Text } = Typography;
|
||||||
|
const { Option } = Select;
|
||||||
|
|
||||||
|
// Define interfaces
|
||||||
|
interface Brand {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
shortName?: string;
|
||||||
|
image?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Attribute {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AttributeValue {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
title: string;
|
||||||
|
titleCN?: string;
|
||||||
|
value?: string;
|
||||||
|
image?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Product {
|
||||||
|
id: number;
|
||||||
|
sku: string;
|
||||||
|
name: string;
|
||||||
|
image?: string;
|
||||||
|
brandId: number;
|
||||||
|
brandName: string;
|
||||||
|
attributes: { [key: string]: any };
|
||||||
|
}
|
||||||
|
|
||||||
|
const BrandSpace: React.FC = () => {
|
||||||
|
// State management
|
||||||
|
const [brands, setBrands] = useState<Brand[]>([]);
|
||||||
|
const [selectedBrand, setSelectedBrand] = useState<number | null>(null);
|
||||||
|
|
||||||
|
const [attributes, setAttributes] = useState<Attribute[]>([]);
|
||||||
|
const [selectedAttribute, setSelectedAttribute] = useState<string | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
const [attributeValues, setAttributeValues] = useState<AttributeValue[]>([]);
|
||||||
|
const [selectedAttributeValue, setSelectedAttributeValue] = useState<
|
||||||
|
number | null
|
||||||
|
>(null);
|
||||||
|
|
||||||
|
const [products, setProducts] = useState<Product[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
// Fetch brands list
|
||||||
|
const fetchBrands = async () => {
|
||||||
|
try {
|
||||||
|
const response = await request('/dict/items', {
|
||||||
|
params: { dictId: 'brand' }, // Assuming brand is a dict
|
||||||
|
});
|
||||||
|
const brandList = Array.isArray(response)
|
||||||
|
? response
|
||||||
|
: response?.data || [];
|
||||||
|
setBrands(brandList);
|
||||||
|
|
||||||
|
// Set default brand to "yoone" if exists
|
||||||
|
const defaultBrand = brandList.find((brand) => brand.name === 'yoone');
|
||||||
|
if (defaultBrand) {
|
||||||
|
setSelectedBrand(defaultBrand.id);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch brands:', error);
|
||||||
|
message.error('获取品牌列表失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fetch attributes list
|
||||||
|
const fetchAttributes = async () => {
|
||||||
|
try {
|
||||||
|
// Get all dicts that are attributes (excluding non-attribute dicts)
|
||||||
|
const response = await request('/dict/list');
|
||||||
|
const dictList = Array.isArray(response)
|
||||||
|
? response
|
||||||
|
: response?.data || [];
|
||||||
|
|
||||||
|
// Filter out non-attribute dicts (assuming attributes are specific dicts)
|
||||||
|
const attributeDicts = dictList.filter((dict: any) =>
|
||||||
|
['strength', 'flavor', 'humidity', 'size', 'version'].includes(
|
||||||
|
dict.name,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
setAttributes(attributeDicts);
|
||||||
|
|
||||||
|
// Set default attribute to strength if exists
|
||||||
|
const defaultAttribute = attributeDicts.find(
|
||||||
|
(attr) => attr.name === 'strength',
|
||||||
|
);
|
||||||
|
if (defaultAttribute) {
|
||||||
|
setSelectedAttribute(defaultAttribute.name);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch attributes:', error);
|
||||||
|
message.error('获取属性列表失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fetch attribute values based on selected attribute
|
||||||
|
const fetchAttributeValues = async (attributeName: string) => {
|
||||||
|
try {
|
||||||
|
const response = await request('/dict/items', {
|
||||||
|
params: { dictId: attributeName },
|
||||||
|
});
|
||||||
|
const values = Array.isArray(response) ? response : response?.data || [];
|
||||||
|
setAttributeValues(values);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch attribute values:', error);
|
||||||
|
message.error('获取属性值列表失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fetch products based on filters
|
||||||
|
const fetchProducts = async () => {
|
||||||
|
if (!selectedBrand) return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const params: any = {
|
||||||
|
brandId: selectedBrand,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add attribute filter if selected
|
||||||
|
if (selectedAttribute) {
|
||||||
|
// If attribute value is selected, filter by both attribute and value
|
||||||
|
if (selectedAttributeValue) {
|
||||||
|
params[selectedAttribute] = selectedAttributeValue;
|
||||||
|
} else {
|
||||||
|
// If only attribute is selected, filter by attribute presence
|
||||||
|
params[selectedAttribute] = 'hasValue';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await request('/product/list', {
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
|
||||||
|
const productList = Array.isArray(response)
|
||||||
|
? response
|
||||||
|
: response?.data || [];
|
||||||
|
setProducts(productList);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch products:', error);
|
||||||
|
message.error('获取产品列表失败');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initial data fetch
|
||||||
|
useEffect(() => {
|
||||||
|
fetchBrands();
|
||||||
|
fetchAttributes();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Fetch attribute values when attribute changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedAttribute) {
|
||||||
|
fetchAttributeValues(selectedAttribute);
|
||||||
|
setSelectedAttributeValue(null); // Reset selected value when attribute changes
|
||||||
|
}
|
||||||
|
}, [selectedAttribute]);
|
||||||
|
|
||||||
|
// Fetch products when filters change
|
||||||
|
useEffect(() => {
|
||||||
|
fetchProducts();
|
||||||
|
}, [selectedBrand, selectedAttribute, selectedAttributeValue]);
|
||||||
|
|
||||||
|
// Handle brand selection change
|
||||||
|
const handleBrandChange = (value: number) => {
|
||||||
|
setSelectedBrand(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle attribute selection change
|
||||||
|
const handleAttributeChange = (value: string) => {
|
||||||
|
setSelectedAttribute(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle attribute value selection change
|
||||||
|
const handleAttributeValueChange = (value: number) => {
|
||||||
|
setSelectedAttributeValue(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer title="品牌空间">
|
||||||
|
<Layout style={{ minHeight: 'calc(100vh - 64px)', background: '#fff' }}>
|
||||||
|
{/* Top Brand Selection */}
|
||||||
|
<div style={{ padding: '16px', borderBottom: '1px solid #f0f0f0' }}>
|
||||||
|
<Space direction="vertical" style={{ width: '100%' }}>
|
||||||
|
<Title level={4} style={{ margin: 0 }}>
|
||||||
|
选择品牌
|
||||||
|
</Title>
|
||||||
|
<Select
|
||||||
|
placeholder="请选择品牌"
|
||||||
|
style={{ width: 300 }}
|
||||||
|
value={selectedBrand}
|
||||||
|
onChange={handleBrandChange}
|
||||||
|
allowClear
|
||||||
|
>
|
||||||
|
{brands.map((brand) => (
|
||||||
|
<Option key={brand.id} value={brand.id}>
|
||||||
|
{brand.name}
|
||||||
|
</Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Layout>
|
||||||
|
{/* Left Attribute Selection */}
|
||||||
|
<Sider
|
||||||
|
width={240}
|
||||||
|
style={{ background: '#fafafa', borderRight: '1px solid #f0f0f0' }}
|
||||||
|
>
|
||||||
|
<div style={{ padding: '16px' }}>
|
||||||
|
<Space direction="vertical" style={{ width: '100%' }}>
|
||||||
|
<Title level={5} style={{ margin: 0 }}>
|
||||||
|
选择属性
|
||||||
|
</Title>
|
||||||
|
<Select
|
||||||
|
placeholder="请选择属性类型"
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
value={selectedAttribute}
|
||||||
|
onChange={handleAttributeChange}
|
||||||
|
allowClear
|
||||||
|
>
|
||||||
|
{attributes.map((attr) => (
|
||||||
|
<Option key={attr.id} value={attr.name}>
|
||||||
|
{attr.title}
|
||||||
|
</Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
{selectedAttribute && (
|
||||||
|
<>
|
||||||
|
<Title level={5} style={{ margin: '16px 0 8px 0' }}>
|
||||||
|
属性值
|
||||||
|
</Title>
|
||||||
|
<Select
|
||||||
|
placeholder={`请选择${
|
||||||
|
attributes.find((a) => a.name === selectedAttribute)
|
||||||
|
?.title
|
||||||
|
}`}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
value={selectedAttributeValue}
|
||||||
|
onChange={handleAttributeValueChange}
|
||||||
|
allowClear
|
||||||
|
>
|
||||||
|
{attributeValues.map((value) => (
|
||||||
|
<Option key={value.id} value={value.id}>
|
||||||
|
<Space>
|
||||||
|
{value.image && (
|
||||||
|
<Image
|
||||||
|
src={value.image}
|
||||||
|
style={{
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
objectFit: 'cover',
|
||||||
|
borderRadius: 4,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<span>
|
||||||
|
{value.titleCN || value.title || value.name}
|
||||||
|
</span>
|
||||||
|
</Space>
|
||||||
|
</Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Filter Summary */}
|
||||||
|
{selectedBrand && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
marginTop: 24,
|
||||||
|
padding: 12,
|
||||||
|
background: '#fff',
|
||||||
|
borderRadius: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text strong>当前筛选条件:</Text>
|
||||||
|
<div style={{ marginTop: 8 }}>
|
||||||
|
<Text type="secondary">品牌: </Text>
|
||||||
|
<Text>
|
||||||
|
{brands.find((b) => b.id === selectedBrand)?.name}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
{selectedAttribute && (
|
||||||
|
<div style={{ marginTop: 4 }}>
|
||||||
|
<Text type="secondary">
|
||||||
|
{
|
||||||
|
attributes.find((a) => a.name === selectedAttribute)
|
||||||
|
?.title
|
||||||
|
}
|
||||||
|
:
|
||||||
|
</Text>
|
||||||
|
<Text>
|
||||||
|
{selectedAttributeValue
|
||||||
|
? attributeValues.find(
|
||||||
|
(v) => v.id === selectedAttributeValue,
|
||||||
|
)?.titleCN ||
|
||||||
|
attributeValues.find(
|
||||||
|
(v) => v.id === selectedAttributeValue,
|
||||||
|
)?.title
|
||||||
|
: '所有值'}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
</Sider>
|
||||||
|
|
||||||
|
{/* Main Content - Product List */}
|
||||||
|
<Content style={{ padding: '16px' }}>
|
||||||
|
<div style={{ marginBottom: 16 }}>
|
||||||
|
<Title level={4} style={{ margin: 0 }}>
|
||||||
|
产品列表
|
||||||
|
<Text type="secondary" style={{ fontSize: 16, marginLeft: 8 }}>
|
||||||
|
({products.length} 个产品)
|
||||||
|
</Text>
|
||||||
|
</Title>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<div style={{ textAlign: 'center', padding: '64px' }}>
|
||||||
|
<Text>加载中...</Text>
|
||||||
|
</div>
|
||||||
|
) : products.length > 0 ? (
|
||||||
|
<Row gutter={[16, 16]}>
|
||||||
|
{products.map((product) => (
|
||||||
|
<Col xs={24} sm={12} md={8} lg={6} key={product.id}>
|
||||||
|
<Card
|
||||||
|
hoverable
|
||||||
|
style={{
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: 200,
|
||||||
|
overflow: 'hidden',
|
||||||
|
marginBottom: 12,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={
|
||||||
|
product.image ||
|
||||||
|
'https://via.placeholder.com/200x200?text=No+Image'
|
||||||
|
}
|
||||||
|
alt={product.name}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
objectFit: 'cover',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
type="secondary"
|
||||||
|
style={{ fontSize: 12, marginBottom: 4 }}
|
||||||
|
>
|
||||||
|
{product.sku}
|
||||||
|
</Text>
|
||||||
|
<Title
|
||||||
|
level={5}
|
||||||
|
style={{
|
||||||
|
margin: '4px 0',
|
||||||
|
fontSize: 16,
|
||||||
|
height: 48,
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{product.name}
|
||||||
|
</Title>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
marginTop: 'auto',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
strong
|
||||||
|
style={{ fontSize: 18, color: '#ff4d4f' }}
|
||||||
|
>
|
||||||
|
¥{product.price || '--'}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
))}
|
||||||
|
</Row>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
textAlign: 'center',
|
||||||
|
padding: '64px',
|
||||||
|
background: '#fafafa',
|
||||||
|
borderRadius: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text type="secondary">暂无符合条件的产品</Text>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Content>
|
||||||
|
</Layout>
|
||||||
|
</Layout>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BrandSpace;
|
||||||
|
|
@ -255,7 +255,9 @@ const CategoryPage: React.FC = () => {
|
||||||
<Content style={{ padding: '24px' }}>
|
<Content style={{ padding: '24px' }}>
|
||||||
{selectedCategory ? (
|
{selectedCategory ? (
|
||||||
<Card
|
<Card
|
||||||
title={`分类:${selectedCategory.title} (${selectedCategory.shortName ?? selectedCategory.name})`}
|
title={`分类:${selectedCategory.title} (${
|
||||||
|
selectedCategory.shortName ?? selectedCategory.name
|
||||||
|
})`}
|
||||||
extra={
|
extra={
|
||||||
<Button type="primary" onClick={handleAddAttribute}>
|
<Button type="primary" onClick={handleAddAttribute}>
|
||||||
添加关联属性
|
添加关联属性
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { productcontrollerGetcategoriesall } from '@/servers/api/product';
|
||||||
import { UploadOutlined } from '@ant-design/icons';
|
import { UploadOutlined } from '@ant-design/icons';
|
||||||
import {
|
import {
|
||||||
PageContainer,
|
PageContainer,
|
||||||
|
|
@ -8,8 +9,6 @@ import { request } from '@umijs/max';
|
||||||
import { Button, Card, Checkbox, Col, Input, message, Row, Upload } from 'antd';
|
import { Button, Card, Checkbox, Col, Input, message, Row, Upload } from 'antd';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import * as XLSX from 'xlsx';
|
import * as XLSX from 'xlsx';
|
||||||
import { productcontrollerGetcategoriesall } from '@/servers/api/product';
|
|
||||||
|
|
||||||
|
|
||||||
// 定义站点接口
|
// 定义站点接口
|
||||||
interface Site {
|
interface Site {
|
||||||
|
|
@ -21,7 +20,7 @@ interface Site {
|
||||||
|
|
||||||
// 定义选项接口,用于下拉选择框的选项
|
// 定义选项接口,用于下拉选择框的选项
|
||||||
interface Option {
|
interface Option {
|
||||||
name: string; // 显示名称
|
name: string; // 显示名称
|
||||||
shortName: string; // 短名称,用于生成SKU
|
shortName: string; // 短名称,用于生成SKU
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -66,7 +65,8 @@ const CsvTool: React.FC = () => {
|
||||||
const [isProcessing, setIsProcessing] = useState(false);
|
const [isProcessing, setIsProcessing] = useState(false);
|
||||||
const [sites, setSites] = useState<Site[]>([]);
|
const [sites, setSites] = useState<Site[]>([]);
|
||||||
const [selectedSites, setSelectedSites] = useState<Site[]>([]); // 现在使用多选
|
const [selectedSites, setSelectedSites] = useState<Site[]>([]); // 现在使用多选
|
||||||
const [generateBundleSkuForSingle, setGenerateBundleSkuForSingle] = useState(true); // 是否为type为single的记录生成包含quantity的bundle SKU
|
const [generateBundleSkuForSingle, setGenerateBundleSkuForSingle] =
|
||||||
|
useState(true); // 是否为type为single的记录生成包含quantity的bundle SKU
|
||||||
const [config, setConfig] = useState<SkuConfig>({
|
const [config, setConfig] = useState<SkuConfig>({
|
||||||
brands: [],
|
brands: [],
|
||||||
categories: [],
|
categories: [],
|
||||||
|
|
@ -78,16 +78,18 @@ const CsvTool: React.FC = () => {
|
||||||
quantities: [],
|
quantities: [],
|
||||||
});
|
});
|
||||||
// 所有属性名称到shortName的映射
|
// 所有属性名称到shortName的映射
|
||||||
const [attributeMappings, setAttributeMappings] = useState<AttributeMappings>({
|
const [attributeMappings, setAttributeMappings] = useState<AttributeMappings>(
|
||||||
brands: {},
|
{
|
||||||
categories: {},
|
brands: {},
|
||||||
flavors: {},
|
categories: {},
|
||||||
strengths: {},
|
flavors: {},
|
||||||
humidities: {},
|
strengths: {},
|
||||||
versions: {},
|
humidities: {},
|
||||||
sizes: {},
|
versions: {},
|
||||||
quantities: {},
|
sizes: {},
|
||||||
});
|
quantities: {},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// 在组件加载时获取站点列表和字典数据
|
// 在组件加载时获取站点列表和字典数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -122,7 +124,7 @@ const CsvTool: React.FC = () => {
|
||||||
// 创建完整的选项数组
|
// 创建完整的选项数组
|
||||||
const options = items.map((item: any) => ({
|
const options = items.map((item: any) => ({
|
||||||
name: item.name,
|
name: item.name,
|
||||||
shortName: item.shortName || item.name
|
shortName: item.shortName || item.name,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// 创建name到shortName的映射
|
// 创建name到shortName的映射
|
||||||
|
|
@ -139,7 +141,15 @@ const CsvTool: React.FC = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
// 4. 获取所有字典项(品牌、口味、强度、湿度、版本、尺寸、数量)
|
// 4. 获取所有字典项(品牌、口味、强度、湿度、版本、尺寸、数量)
|
||||||
const [brandResult, flavorResult, strengthResult, humidityResult, versionResult, sizeResult, quantityResult] = await Promise.all([
|
const [
|
||||||
|
brandResult,
|
||||||
|
flavorResult,
|
||||||
|
strengthResult,
|
||||||
|
humidityResult,
|
||||||
|
versionResult,
|
||||||
|
sizeResult,
|
||||||
|
quantityResult,
|
||||||
|
] = await Promise.all([
|
||||||
getDictItems('brand'),
|
getDictItems('brand'),
|
||||||
getDictItems('flavor'),
|
getDictItems('flavor'),
|
||||||
getDictItems('strength'),
|
getDictItems('strength'),
|
||||||
|
|
@ -151,16 +161,21 @@ const CsvTool: React.FC = () => {
|
||||||
|
|
||||||
// 5. 获取商品分类列表
|
// 5. 获取商品分类列表
|
||||||
const categoriesResponse = await productcontrollerGetcategoriesall();
|
const categoriesResponse = await productcontrollerGetcategoriesall();
|
||||||
const categoryOptions = categoriesResponse?.data?.map((category: any) => ({
|
const categoryOptions =
|
||||||
name: category.name,
|
categoriesResponse?.data?.map((category: any) => ({
|
||||||
shortName: category.shortName || category.name
|
name: category.name,
|
||||||
})) || [];
|
shortName: category.shortName || category.name,
|
||||||
|
})) || [];
|
||||||
|
|
||||||
// 商品分类的映射(如果分类有shortName的话)
|
// 商品分类的映射(如果分类有shortName的话)
|
||||||
const categoryMapping = categoriesResponse?.data?.reduce((acc: AttributeMapping, category: any) => {
|
const categoryMapping =
|
||||||
acc[category.name] = category.shortName || category.name;
|
categoriesResponse?.data?.reduce(
|
||||||
return acc;
|
(acc: AttributeMapping, category: any) => {
|
||||||
}, {}) || {};
|
acc[category.name] = category.shortName || category.name;
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
) || {};
|
||||||
|
|
||||||
// 6. 设置所有属性映射
|
// 6. 设置所有属性映射
|
||||||
setAttributeMappings({
|
setAttributeMappings({
|
||||||
|
|
@ -171,7 +186,7 @@ const CsvTool: React.FC = () => {
|
||||||
humidities: humidityResult.mapping,
|
humidities: humidityResult.mapping,
|
||||||
versions: versionResult.mapping,
|
versions: versionResult.mapping,
|
||||||
sizes: sizeResult.mapping,
|
sizes: sizeResult.mapping,
|
||||||
quantities: quantityResult.mapping
|
quantities: quantityResult.mapping,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 更新配置状态
|
// 更新配置状态
|
||||||
|
|
@ -188,21 +203,24 @@ const CsvTool: React.FC = () => {
|
||||||
setConfig(newConfig);
|
setConfig(newConfig);
|
||||||
// 设置表单值时只需要name数组
|
// 设置表单值时只需要name数组
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
brands: brandResult.options.map(opt => opt.name),
|
brands: brandResult.options.map((opt) => opt.name),
|
||||||
categories: categoryOptions.map(opt => opt.name),
|
categories: categoryOptions.map((opt) => opt.name),
|
||||||
flavors: flavorResult.options.map(opt => opt.name),
|
flavors: flavorResult.options.map((opt) => opt.name),
|
||||||
strengths: strengthResult.options.map(opt => opt.name),
|
strengths: strengthResult.options.map((opt) => opt.name),
|
||||||
humidities: humidityResult.options.map(opt => opt.name),
|
humidities: humidityResult.options.map((opt) => opt.name),
|
||||||
versions: versionResult.options.map(opt => opt.name),
|
versions: versionResult.options.map((opt) => opt.name),
|
||||||
sizes: sizeResult.options.map(opt => opt.name),
|
sizes: sizeResult.options.map((opt) => opt.name),
|
||||||
quantities: quantityResult.options.map(opt => opt.name),
|
quantities: quantityResult.options.map((opt) => opt.name),
|
||||||
generateBundleSkuForSingle: true,
|
generateBundleSkuForSingle: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
message.success({ content: '数据加载成功', key: 'loading' });
|
message.success({ content: '数据加载成功', key: 'loading' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch data:', error);
|
console.error('Failed to fetch data:', error);
|
||||||
message.error({ content: '数据加载失败,请刷新页面重试', key: 'loading' });
|
message.error({
|
||||||
|
content: '数据加载失败,请刷新页面重试',
|
||||||
|
key: 'loading',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -235,7 +253,7 @@ const CsvTool: React.FC = () => {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
codepage: 65001, // UTF-8 encoding
|
codepage: 65001, // UTF-8 encoding
|
||||||
cellText: true,
|
cellText: true,
|
||||||
cellDates: true
|
cellDates: true,
|
||||||
});
|
});
|
||||||
const sheetName = workbook.SheetNames[0];
|
const sheetName = workbook.SheetNames[0];
|
||||||
const worksheet = workbook.Sheets[sheetName];
|
const worksheet = workbook.Sheets[sheetName];
|
||||||
|
|
@ -350,7 +368,7 @@ const CsvTool: React.FC = () => {
|
||||||
humidity: string,
|
humidity: string,
|
||||||
size: string,
|
size: string,
|
||||||
quantity?: any,
|
quantity?: any,
|
||||||
type?: string
|
type?: string,
|
||||||
): string => {
|
): string => {
|
||||||
// 构建SKU组件,不包含站点前缀
|
// 构建SKU组件,不包含站点前缀
|
||||||
const skuComponents: string[] = [];
|
const skuComponents: string[] = [];
|
||||||
|
|
@ -368,7 +386,8 @@ const CsvTool: React.FC = () => {
|
||||||
}
|
}
|
||||||
if (category) {
|
if (category) {
|
||||||
// 使用分类的shortName,如果没有则使用分类名称
|
// 使用分类的shortName,如果没有则使用分类名称
|
||||||
const categoryShortName = attributeMappings.categories[category] || category;
|
const categoryShortName =
|
||||||
|
attributeMappings.categories[category] || category;
|
||||||
skuComponents.push(categoryShortName);
|
skuComponents.push(categoryShortName);
|
||||||
}
|
}
|
||||||
if (flavor) {
|
if (flavor) {
|
||||||
|
|
@ -378,12 +397,14 @@ const CsvTool: React.FC = () => {
|
||||||
}
|
}
|
||||||
if (strength) {
|
if (strength) {
|
||||||
// 使用强度的shortName,如果没有则使用强度名称
|
// 使用强度的shortName,如果没有则使用强度名称
|
||||||
const strengthShortName = attributeMappings.strengths[strength] || strength;
|
const strengthShortName =
|
||||||
|
attributeMappings.strengths[strength] || strength;
|
||||||
skuComponents.push(strengthShortName);
|
skuComponents.push(strengthShortName);
|
||||||
}
|
}
|
||||||
if (humidity) {
|
if (humidity) {
|
||||||
// 使用湿度的shortName,如果没有则使用湿度名称
|
// 使用湿度的shortName,如果没有则使用湿度名称
|
||||||
const humidityShortName = attributeMappings.humidities[humidity] || humidity;
|
const humidityShortName =
|
||||||
|
attributeMappings.humidities[humidity] || humidity;
|
||||||
skuComponents.push(humidityShortName);
|
skuComponents.push(humidityShortName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -394,20 +415,65 @@ const CsvTool: React.FC = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果type为single且启用了生成bundle SKU,则添加quantity
|
// 如果type为single且启用了生成bundle SKU,则添加quantity
|
||||||
if ((!type || type === 'single') && generateBundleSkuForSingle && quantity) {
|
if (
|
||||||
// 将quantity转换为数字,然后格式化为四位数(前导零)
|
quantity
|
||||||
const formattedQuantity = Number(quantity).toString().padStart(4, '0');
|
) {
|
||||||
skuComponents.push(formattedQuantity);
|
console.log(quantity, attributeMappings.quantities[quantity])
|
||||||
} else if (type === 'bundle' && quantity) {
|
// 使用quantity的shortName,如果没有则使用quantity但匹配 4 个零
|
||||||
// 对于bundle类型,始终添加quantity
|
const quantityShortName = attributeMappings.quantities[quantity] || Number(quantity).toString().padStart(4, '0');
|
||||||
const formattedQuantity = Number(quantity).toString().padStart(4, '0');
|
skuComponents.push(quantityShortName);
|
||||||
skuComponents.push(formattedQuantity);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 合并所有组件,使用短横线分隔
|
// 合并所有组件,使用短横线分隔
|
||||||
return skuComponents.join('-').toUpperCase();
|
return skuComponents.join('-').toUpperCase();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 根据配置生成产品名称(使用属性的完整名称,空格分隔)
|
||||||
|
* @param {string} brand - 品牌
|
||||||
|
* @param {string} version - 版本
|
||||||
|
* @param {string} category - 分类
|
||||||
|
* @param {string} flavor - 口味
|
||||||
|
* @param {string} strength - 强度
|
||||||
|
* @param {string} humidity - 湿度
|
||||||
|
* @param {string} size - 型号
|
||||||
|
* @param {any} quantity - 数量
|
||||||
|
* @param {string} type - 产品类型
|
||||||
|
* @returns {string} 生成的产品名称
|
||||||
|
*/
|
||||||
|
const generateName = (
|
||||||
|
brand: string,
|
||||||
|
version: string,
|
||||||
|
category: string,
|
||||||
|
flavor: string,
|
||||||
|
strength: string,
|
||||||
|
humidity: string,
|
||||||
|
size: string,
|
||||||
|
quantity?: any,
|
||||||
|
type?: string,
|
||||||
|
): string => {
|
||||||
|
// 构建产品名称组件数组
|
||||||
|
const nameComponents: string[] = [];
|
||||||
|
|
||||||
|
// 按顺序添加组件:品牌 -> 版本 -> 品类 -> 风味 -> 毫克数(强度) -> 湿度 -> 型号 -> 数量
|
||||||
|
if (brand) nameComponents.push(brand);
|
||||||
|
if (version) nameComponents.push(version);
|
||||||
|
if (category) nameComponents.push(category);
|
||||||
|
if (flavor) nameComponents.push(flavor);
|
||||||
|
if (strength) nameComponents.push(strength);
|
||||||
|
if (humidity) nameComponents.push(humidity);
|
||||||
|
if (size) nameComponents.push(size);
|
||||||
|
|
||||||
|
// 如果有数量且类型为bundle或者生成bundle的single产品,则添加数量
|
||||||
|
if (
|
||||||
|
type==='bundle' && quantity
|
||||||
|
) {
|
||||||
|
nameComponents.push(String(quantity));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用空格连接所有组件
|
||||||
|
return nameComponents.join(' ');
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description 为所有站点生成带前缀的siteSkus
|
* @description 为所有站点生成带前缀的siteSkus
|
||||||
* @param {string} baseSku - 基础SKU(不包含站点前缀)
|
* @param {string} baseSku - 基础SKU(不包含站点前缀)
|
||||||
|
|
@ -418,7 +484,7 @@ const CsvTool: React.FC = () => {
|
||||||
if (selectedSites.length === 0 || !baseSku) return '';
|
if (selectedSites.length === 0 || !baseSku) return '';
|
||||||
|
|
||||||
// 为每个站点生成siteSku
|
// 为每个站点生成siteSku
|
||||||
const siteSkus = selectedSites.map(site => {
|
const siteSkus = selectedSites.map((site) => {
|
||||||
// 如果站点有shortName,则添加前缀,否则使用基础SKU
|
// 如果站点有shortName,则添加前缀,否则使用基础SKU
|
||||||
if (site.skuPrefix) {
|
if (site.skuPrefix) {
|
||||||
return `${site.skuPrefix}-${baseSku}`;
|
return `${site.skuPrefix}-${baseSku}`;
|
||||||
|
|
@ -427,7 +493,7 @@ const CsvTool: React.FC = () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// 使用分号分隔所有站点的siteSkus
|
// 使用分号分隔所有站点的siteSkus
|
||||||
return siteSkus.join(';').toUpperCase();
|
return [baseSku, ...siteSkus].join(';').toUpperCase();
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -466,17 +532,39 @@ const CsvTool: React.FC = () => {
|
||||||
const type = row.type || '';
|
const type = row.type || '';
|
||||||
|
|
||||||
// 生成基础SKU(不包含站点前缀)
|
// 生成基础SKU(不包含站点前缀)
|
||||||
const baseSku = generateSku(brand, version, category, flavor, strength, humidity, size, quantity, type);
|
const baseSku = generateSku(
|
||||||
|
brand,
|
||||||
|
version,
|
||||||
|
category,
|
||||||
|
flavor,
|
||||||
|
strength,
|
||||||
|
humidity,
|
||||||
|
size,
|
||||||
|
quantity,
|
||||||
|
type,
|
||||||
|
);
|
||||||
|
const name = generateName(
|
||||||
|
brand,
|
||||||
|
version,
|
||||||
|
category,
|
||||||
|
flavor,
|
||||||
|
strength,
|
||||||
|
humidity,
|
||||||
|
size,
|
||||||
|
quantity,
|
||||||
|
type,
|
||||||
|
);
|
||||||
// 为所有站点生成带前缀的siteSkus
|
// 为所有站点生成带前缀的siteSkus
|
||||||
const siteSkus = generateSiteSkus(baseSku);
|
const siteSkus = generateSiteSkus(baseSku);
|
||||||
|
|
||||||
// 返回包含新SKU和siteSkus的行数据,将SKU直接保存到sku字段
|
// 返回包含新SKU和siteSkus的行数据,将SKU直接保存到sku栏
|
||||||
return {
|
return {
|
||||||
...row,
|
...row,
|
||||||
sku: baseSku, // 直接生成在sku栏
|
sku: baseSku, // 直接生成在sku栏
|
||||||
|
generatedName: name,
|
||||||
|
// name: name, // 生成的产品名称
|
||||||
siteSkus,
|
siteSkus,
|
||||||
attribute_quantity: quantity // 确保quantity保存到attribute_quantity
|
attribute_quantity: quantity, // 确保quantity保存到attribute_quantity
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -484,18 +572,18 @@ const CsvTool: React.FC = () => {
|
||||||
let finalData = dataWithSku;
|
let finalData = dataWithSku;
|
||||||
|
|
||||||
// If generateBundleSkuForSingle is enabled, generate bundle products for single products
|
// If generateBundleSkuForSingle is enabled, generate bundle products for single products
|
||||||
if(generateBundleSkuForSingle) {
|
if (generateBundleSkuForSingle) {
|
||||||
// Filter out single records
|
// Filter out single records
|
||||||
const singleRecords = dataWithSku.filter(row => row.type === 'single');
|
const singleRecords = dataWithSku.filter(
|
||||||
|
(row) => row.type === 'single',
|
||||||
|
);
|
||||||
|
|
||||||
// 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
|
const quantityValues = config.quantities.map(quantity=>quantity.name)
|
||||||
.map(quantity => Number(quantity.name)) // Extract name and convert to number
|
|
||||||
.filter(quantity => !isNaN(quantity)); // Filter out invalid numbers
|
|
||||||
|
|
||||||
// Generate bundle products for each single record and quantity
|
// 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 || '';
|
||||||
const version = singleRecord.attribute_version || '';
|
const version = singleRecord.attribute_version || '';
|
||||||
|
|
@ -504,9 +592,31 @@ const CsvTool: React.FC = () => {
|
||||||
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 = singleRecord.attribute_size || singleRecord.size || '';
|
||||||
|
|
||||||
// Generate bundle SKU with the quantity
|
// Generate bundle SKU with the quantity
|
||||||
const bundleSku = generateSku(brand, version, category, flavor, strength, humidity, size, quantity, 'bundle');
|
const bundleSku = generateSku(
|
||||||
|
brand,
|
||||||
|
version,
|
||||||
|
category,
|
||||||
|
flavor,
|
||||||
|
strength,
|
||||||
|
humidity,
|
||||||
|
size,
|
||||||
|
quantity,
|
||||||
|
'bundle',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Generate bundle name with the quantity
|
||||||
|
const bundleName = generateName(
|
||||||
|
brand,
|
||||||
|
version,
|
||||||
|
category,
|
||||||
|
flavor,
|
||||||
|
strength,
|
||||||
|
humidity,
|
||||||
|
size,
|
||||||
|
quantity,
|
||||||
|
'bundle',
|
||||||
|
);
|
||||||
|
|
||||||
// Generate siteSkus for the bundle
|
// Generate siteSkus for the bundle
|
||||||
const bundleSiteSkus = generateSiteSkus(bundleSku);
|
const bundleSiteSkus = generateSiteSkus(bundleSku);
|
||||||
|
|
@ -516,10 +626,11 @@ const CsvTool: React.FC = () => {
|
||||||
...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
|
||||||
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: 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
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -531,20 +642,24 @@ const CsvTool: React.FC = () => {
|
||||||
// Set the processed data
|
// Set the processed data
|
||||||
setProcessedData(finalData);
|
setProcessedData(finalData);
|
||||||
|
|
||||||
message.success({ content: 'SKU生成成功!正在自动下载...', key: 'processing' });
|
message.success({
|
||||||
|
content: 'SKU生成成功!正在自动下载...',
|
||||||
|
key: 'processing',
|
||||||
|
});
|
||||||
|
|
||||||
// 自动下载 the final data (with or without generated bundle products)
|
// 自动下载 the final data (with or without generated bundle products)
|
||||||
downloadData(finalData);
|
downloadData(finalData);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error({ content: '处理失败,请检查配置或文件.', key: 'processing' });
|
message.error({
|
||||||
|
content: '处理失败,请检查配置或文件.',
|
||||||
|
key: 'processing',
|
||||||
|
});
|
||||||
console.error('Processing Error:', error);
|
console.error('Processing Error:', error);
|
||||||
} finally {
|
} finally {
|
||||||
setIsProcessing(false);
|
setIsProcessing(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer title="产品SKU批量生成工具">
|
<PageContainer title="产品SKU批量生成工具">
|
||||||
<Row gutter={[16, 16]}>
|
<Row gutter={[16, 16]}>
|
||||||
|
|
@ -564,9 +679,9 @@ const CsvTool: React.FC = () => {
|
||||||
placeholder="请输入品牌,按回车确认"
|
placeholder="请输入品牌,按回车确认"
|
||||||
rules={[{ required: true, message: '至少需要一个品牌' }]}
|
rules={[{ required: true, message: '至少需要一个品牌' }]}
|
||||||
tooltip="品牌名称会作为SKU的第一个组成部分"
|
tooltip="品牌名称会作为SKU的第一个组成部分"
|
||||||
options={config.brands.map(opt => ({
|
options={config.brands.map((opt) => ({
|
||||||
label: `${opt.name} (${opt.shortName})`,
|
label: `${opt.name} (${opt.shortName})`,
|
||||||
value: opt.name
|
value: opt.name,
|
||||||
}))}
|
}))}
|
||||||
/>
|
/>
|
||||||
<ProFormSelect
|
<ProFormSelect
|
||||||
|
|
@ -576,9 +691,9 @@ const CsvTool: React.FC = () => {
|
||||||
placeholder="请输入分类,按回车确认"
|
placeholder="请输入分类,按回车确认"
|
||||||
rules={[{ required: true, message: '至少需要一个分类' }]}
|
rules={[{ required: true, message: '至少需要一个分类' }]}
|
||||||
tooltip="分类名称会作为SKU的第二个组成部分"
|
tooltip="分类名称会作为SKU的第二个组成部分"
|
||||||
options={config.categories.map(opt => ({
|
options={config.categories.map((opt) => ({
|
||||||
label: `${opt.name} (${opt.shortName})`,
|
label: `${opt.name} (${opt.shortName})`,
|
||||||
value: opt.name
|
value: opt.name,
|
||||||
}))}
|
}))}
|
||||||
/>
|
/>
|
||||||
<ProFormSelect
|
<ProFormSelect
|
||||||
|
|
@ -588,9 +703,9 @@ const CsvTool: React.FC = () => {
|
||||||
placeholder="请输入口味,按回车确认"
|
placeholder="请输入口味,按回车确认"
|
||||||
rules={[{ required: true, message: '至少需要一个口味' }]}
|
rules={[{ required: true, message: '至少需要一个口味' }]}
|
||||||
tooltip="口味名称会作为SKU的第三个组成部分"
|
tooltip="口味名称会作为SKU的第三个组成部分"
|
||||||
options={config.flavors.map(opt => ({
|
options={config.flavors.map((opt) => ({
|
||||||
label: `${opt.name} (${opt.shortName})`,
|
label: `${opt.name} (${opt.shortName})`,
|
||||||
value: opt.name
|
value: opt.name,
|
||||||
}))}
|
}))}
|
||||||
/>
|
/>
|
||||||
<ProFormSelect
|
<ProFormSelect
|
||||||
|
|
@ -599,9 +714,9 @@ const CsvTool: React.FC = () => {
|
||||||
mode="tags"
|
mode="tags"
|
||||||
placeholder="请输入强度,按回车确认"
|
placeholder="请输入强度,按回车确认"
|
||||||
tooltip="强度信息会作为SKU的第四个组成部分"
|
tooltip="强度信息会作为SKU的第四个组成部分"
|
||||||
options={config.strengths.map(opt => ({
|
options={config.strengths.map((opt) => ({
|
||||||
label: `${opt.name} (${opt.shortName})`,
|
label: `${opt.name} (${opt.shortName})`,
|
||||||
value: opt.name
|
value: opt.name,
|
||||||
}))}
|
}))}
|
||||||
/>
|
/>
|
||||||
<ProFormSelect
|
<ProFormSelect
|
||||||
|
|
@ -610,9 +725,9 @@ const CsvTool: React.FC = () => {
|
||||||
mode="tags"
|
mode="tags"
|
||||||
placeholder="请输入湿度,按回车确认"
|
placeholder="请输入湿度,按回车确认"
|
||||||
tooltip="湿度信息会作为SKU的第五个组成部分"
|
tooltip="湿度信息会作为SKU的第五个组成部分"
|
||||||
options={config.humidities.map(opt => ({
|
options={config.humidities.map((opt) => ({
|
||||||
label: `${opt.name} (${opt.shortName})`,
|
label: `${opt.name} (${opt.shortName})`,
|
||||||
value: opt.name
|
value: opt.name,
|
||||||
}))}
|
}))}
|
||||||
/>
|
/>
|
||||||
<ProFormSelect
|
<ProFormSelect
|
||||||
|
|
@ -621,9 +736,9 @@ const CsvTool: React.FC = () => {
|
||||||
mode="tags"
|
mode="tags"
|
||||||
placeholder="请输入版本,按回车确认"
|
placeholder="请输入版本,按回车确认"
|
||||||
tooltip="版本信息会作为SKU的第六个组成部分"
|
tooltip="版本信息会作为SKU的第六个组成部分"
|
||||||
options={config.versions.map(opt => ({
|
options={config.versions.map((opt) => ({
|
||||||
label: `${opt.name} (${opt.shortName})`,
|
label: `${opt.name} (${opt.shortName})`,
|
||||||
value: opt.name
|
value: opt.name,
|
||||||
}))}
|
}))}
|
||||||
/>
|
/>
|
||||||
<ProFormSelect
|
<ProFormSelect
|
||||||
|
|
@ -632,9 +747,9 @@ const CsvTool: React.FC = () => {
|
||||||
mode="tags"
|
mode="tags"
|
||||||
placeholder="请输入尺寸,按回车确认"
|
placeholder="请输入尺寸,按回车确认"
|
||||||
tooltip="尺寸信息会作为SKU的第七个组成部分"
|
tooltip="尺寸信息会作为SKU的第七个组成部分"
|
||||||
options={config.sizes.map(opt => ({
|
options={config.sizes.map((opt) => ({
|
||||||
label: `${opt.name} (${opt.shortName})`,
|
label: `${opt.name} (${opt.shortName})`,
|
||||||
value: opt.name
|
value: opt.name,
|
||||||
}))}
|
}))}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
@ -644,9 +759,9 @@ const CsvTool: React.FC = () => {
|
||||||
mode="tags"
|
mode="tags"
|
||||||
placeholder="请输入数量,按回车确认"
|
placeholder="请输入数量,按回车确认"
|
||||||
tooltip="数量信息会作为bundle SKU的组成部分"
|
tooltip="数量信息会作为bundle SKU的组成部分"
|
||||||
options={config.quantities.map(opt => ({
|
options={config.quantities.map((opt) => ({
|
||||||
label: `${opt.name} (${opt.shortName})`,
|
label: `${opt.name} (${opt.shortName})`,
|
||||||
value: opt.name
|
value: opt.name,
|
||||||
}))}
|
}))}
|
||||||
fieldProps={{ allowClear: true }}
|
fieldProps={{ allowClear: true }}
|
||||||
/>
|
/>
|
||||||
|
|
@ -672,25 +787,60 @@ const CsvTool: React.FC = () => {
|
||||||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr style={{ backgroundColor: '#fafafa' }}>
|
<tr style={{ backgroundColor: '#fafafa' }}>
|
||||||
<th style={{ padding: '8px', textAlign: 'left', borderBottom: '1px solid #e8e8e8' }}>站点名称</th>
|
<th
|
||||||
<th style={{ padding: '8px', textAlign: 'left', borderBottom: '1px solid #e8e8e8' }}>ShortName</th>
|
style={{
|
||||||
|
padding: '8px',
|
||||||
|
textAlign: 'left',
|
||||||
|
borderBottom: '1px solid #e8e8e8',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
站点名称
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
style={{
|
||||||
|
padding: '8px',
|
||||||
|
textAlign: 'left',
|
||||||
|
borderBottom: '1px solid #e8e8e8',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
ShortName
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{sites.map(site => (
|
{sites.map((site) => (
|
||||||
<tr key={site.id}>
|
<tr key={site.id}>
|
||||||
<td style={{ padding: '8px', borderBottom: '1px solid #e8e8e8' }}>{site.name}</td>
|
<td
|
||||||
<td style={{ padding: '8px', borderBottom: '1px solid #e8e8e8', fontWeight: 'bold' }}>{site.skuPrefix}</td>
|
style={{
|
||||||
|
padding: '8px',
|
||||||
|
borderBottom: '1px solid #e8e8e8',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{site.name}
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
style={{
|
||||||
|
padding: '8px',
|
||||||
|
borderBottom: '1px solid #e8e8e8',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{site.skuPrefix}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
) : (
|
) : (
|
||||||
<p style={{ textAlign: 'center', color: '#999' }}>暂无站点信息</p>
|
<p style={{ textAlign: 'center', color: '#999' }}>
|
||||||
|
暂无站点信息
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ marginTop: '10px', fontSize: '12px', color: '#666' }}>
|
<div style={{ marginTop: '10px', fontSize: '12px', color: '#666' }}>
|
||||||
<p>说明:所有站点的shortName将作为前缀添加到生成的SKU中,以分号分隔。</p>
|
<p>
|
||||||
|
说明:所有站点的shortName将作为前缀添加到生成的SKU中,以分号分隔。
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
@ -719,7 +869,11 @@ const CsvTool: React.FC = () => {
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
onClick={handleProcessData}
|
onClick={handleProcessData}
|
||||||
disabled={csvData.length === 0 || isProcessing || selectedSites.length === 0}
|
disabled={
|
||||||
|
csvData.length === 0 ||
|
||||||
|
isProcessing ||
|
||||||
|
selectedSites.length === 0
|
||||||
|
}
|
||||||
loading={isProcessing}
|
loading={isProcessing}
|
||||||
style={{ marginTop: '20px' }}
|
style={{ marginTop: '20px' }}
|
||||||
>
|
>
|
||||||
|
|
@ -728,7 +882,14 @@ const CsvTool: React.FC = () => {
|
||||||
|
|
||||||
{/* 显示处理结果摘要 */}
|
{/* 显示处理结果摘要 */}
|
||||||
{processedData.length > 0 && (
|
{processedData.length > 0 && (
|
||||||
<div style={{ marginTop: '20px', padding: '10px', backgroundColor: '#f0f9eb', borderRadius: '4px' }}>
|
<div
|
||||||
|
style={{
|
||||||
|
marginTop: '20px',
|
||||||
|
padding: '10px',
|
||||||
|
backgroundColor: '#f0f9eb',
|
||||||
|
borderRadius: '4px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<p style={{ margin: 0, color: '#52c41a' }}>
|
<p style={{ margin: 0, color: '#52c41a' }}>
|
||||||
已成功为 {processedData.length} 条产品记录生成SKU!
|
已成功为 {processedData.length} 条产品记录生成SKU!
|
||||||
</p>
|
</p>
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@ const CreateForm: React.FC<{
|
||||||
const strengthName: string = String(strengthValues?.[0] || '');
|
const strengthName: string = String(strengthValues?.[0] || '');
|
||||||
const flavorName: string = String(flavorValues?.[0] || '');
|
const flavorName: string = String(flavorValues?.[0] || '');
|
||||||
const humidityName: string = String(humidityValues?.[0] || '');
|
const humidityName: string = String(humidityValues?.[0] || '');
|
||||||
console.log(formValues)
|
console.log(formValues);
|
||||||
// 调用模板渲染API来生成SKU
|
// 调用模板渲染API来生成SKU
|
||||||
const {
|
const {
|
||||||
data: rendered,
|
data: rendered,
|
||||||
|
|
@ -86,25 +86,25 @@ const CreateForm: React.FC<{
|
||||||
} = await templatecontrollerRendertemplate(
|
} = await templatecontrollerRendertemplate(
|
||||||
{ name: 'product.sku' },
|
{ name: 'product.sku' },
|
||||||
{
|
{
|
||||||
category: formValues.category,
|
category: formValues.category,
|
||||||
attributes: [
|
attributes: [
|
||||||
{
|
{
|
||||||
dict: {name: "brand"},
|
dict: { name: 'brand' },
|
||||||
shortName: brandName || '',
|
shortName: brandName || '',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
dict: {name: "flavor"},
|
dict: { name: 'flavor' },
|
||||||
shortName: flavorName || '',
|
shortName: flavorName || '',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
dict: {name: "strength"},
|
dict: { name: 'strength' },
|
||||||
shortName: strengthName || '',
|
shortName: strengthName || '',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
dict: {name: "humidity"},
|
dict: { name: 'humidity' },
|
||||||
shortName: humidityName ? capitalize(humidityName) : '',
|
shortName: humidityName ? capitalize(humidityName) : '',
|
||||||
},
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (!success) {
|
if (!success) {
|
||||||
|
|
@ -153,8 +153,8 @@ const CreateForm: React.FC<{
|
||||||
humidityName === 'dry'
|
humidityName === 'dry'
|
||||||
? 'Dry'
|
? 'Dry'
|
||||||
: humidityName === 'moisture'
|
: humidityName === 'moisture'
|
||||||
? 'Moisture'
|
? 'Moisture'
|
||||||
: capitalize(humidityName),
|
: capitalize(humidityName),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (!success) {
|
if (!success) {
|
||||||
|
|
@ -219,20 +219,21 @@ const CreateForm: React.FC<{
|
||||||
// 根据产品类型决定是否组装 attributes
|
// 根据产品类型决定是否组装 attributes
|
||||||
// 如果产品类型为 bundle,则 attributes 为空数组
|
// 如果产品类型为 bundle,则 attributes 为空数组
|
||||||
// 如果产品类型为 single,则根据 activeAttributes 动态组装 attributes
|
// 如果产品类型为 single,则根据 activeAttributes 动态组装 attributes
|
||||||
const attributes = values.type === 'bundle'
|
const attributes =
|
||||||
? []
|
values.type === 'bundle'
|
||||||
: activeAttributes.flatMap((attr: any) => {
|
? []
|
||||||
const dictName = attr.name;
|
: activeAttributes.flatMap((attr: any) => {
|
||||||
const key = `${dictName}Values`;
|
const dictName = attr.name;
|
||||||
const vals = values[key];
|
const key = `${dictName}Values`;
|
||||||
if (vals && Array.isArray(vals)) {
|
const vals = values[key];
|
||||||
return vals.map((v: string) => ({
|
if (vals && Array.isArray(vals)) {
|
||||||
dictName: dictName,
|
return vals.map((v: string) => ({
|
||||||
name: v,
|
dictName: dictName,
|
||||||
}));
|
name: v,
|
||||||
}
|
}));
|
||||||
return [];
|
}
|
||||||
});
|
return [];
|
||||||
|
});
|
||||||
|
|
||||||
const payload: any = {
|
const payload: any = {
|
||||||
name: (values as any).name,
|
name: (values as any).name,
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import {
|
||||||
productcontrollerGetcategoryattributes,
|
productcontrollerGetcategoryattributes,
|
||||||
productcontrollerGetproductcomponents,
|
productcontrollerGetproductcomponents,
|
||||||
productcontrollerGetproductlist,
|
productcontrollerGetproductlist,
|
||||||
productcontrollerGetproductsiteskus,
|
|
||||||
productcontrollerUpdateproduct,
|
productcontrollerUpdateproduct,
|
||||||
} from '@/servers/api/product';
|
} from '@/servers/api/product';
|
||||||
import { sitecontrollerAll } from '@/servers/api/site';
|
import { sitecontrollerAll } from '@/servers/api/site';
|
||||||
|
|
@ -121,7 +120,7 @@ const EditForm: React.FC<{
|
||||||
categoryId: (record as any).categoryId || (record as any).category?.id,
|
categoryId: (record as any).categoryId || (record as any).category?.id,
|
||||||
// 初始化站点SKU为字符串数组
|
// 初始化站点SKU为字符串数组
|
||||||
// 修改后代码:
|
// 修改后代码:
|
||||||
siteSkus:(record.siteSkus||[]).map(code => ({ code })),
|
siteSkus: (record.siteSkus || []).map((code) => ({ code })),
|
||||||
};
|
};
|
||||||
}, [record, components, type]);
|
}, [record, components, type]);
|
||||||
return (
|
return (
|
||||||
|
|
@ -188,7 +187,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: { code: string }) => v.code) || [], // 直接传递字符串数组
|
||||||
// 连带更新 components
|
// 连带更新 components
|
||||||
components:
|
components:
|
||||||
values.type === 'bundle'
|
values.type === 'bundle'
|
||||||
|
|
@ -316,9 +315,17 @@ const EditForm: React.FC<{
|
||||||
rules={[{ required: true, message: '请输入单品SKU' }]}
|
rules={[{ required: true, message: '请输入单品SKU' }]}
|
||||||
request={async ({ keyWords }) => {
|
request={async ({ keyWords }) => {
|
||||||
const params = keyWords
|
const params = keyWords
|
||||||
? { where: {sku: keyWords, name: keyWords, type: 'single'} }
|
? {
|
||||||
: { 'per_page': 9999 , where: {type: 'single'} };
|
where: {
|
||||||
const { data } = await productcontrollerGetproductlist(params);
|
sku: keyWords,
|
||||||
|
name: keyWords,
|
||||||
|
type: 'single',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: { per_page: 9999, where: { type: 'single' } };
|
||||||
|
const { data } = await productcontrollerGetproductlist(
|
||||||
|
params,
|
||||||
|
);
|
||||||
if (!data || !data.items) {
|
if (!data || !data.items) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import { showBatchOperationResult } from '@/utils/showResult';
|
import { productcontrollerBatchsynctosite } from '@/servers/api/product';
|
||||||
import { sitecontrollerAll } from '@/servers/api/site';
|
import { sitecontrollerAll } from '@/servers/api/site';
|
||||||
import { templatecontrollerRendertemplate } from '@/servers/api/template';
|
import { templatecontrollerRendertemplate } from '@/servers/api/template';
|
||||||
import { productcontrollerBatchsynctosite } from '@/servers/api/product';
|
import { showBatchOperationResult } from '@/utils/showResult';
|
||||||
import {
|
import {
|
||||||
ModalForm,
|
ModalForm,
|
||||||
ProFormSelect,
|
|
||||||
ProFormDependency,
|
ProFormDependency,
|
||||||
|
ProFormSelect,
|
||||||
ProFormText,
|
ProFormText,
|
||||||
} from '@ant-design/pro-components';
|
} from '@ant-design/pro-components';
|
||||||
import { App, Button, Tag } from 'antd';
|
import { App, Button, Tag } from 'antd';
|
||||||
|
|
@ -36,12 +36,14 @@ const SyncToSiteModal: React.FC<SyncToSiteModalProps> = ({
|
||||||
product: API.Product,
|
product: API.Product,
|
||||||
): Promise<string> => {
|
): Promise<string> => {
|
||||||
try {
|
try {
|
||||||
console.log('site', currentSite)
|
console.log('site', currentSite);
|
||||||
const { data: renderedSku } = await templatecontrollerRendertemplate(
|
const { data: renderedSku } = await templatecontrollerRendertemplate(
|
||||||
{ name: 'site.product.sku' },
|
{ name: 'site.product.sku' },
|
||||||
{ site: currentSite, product },
|
{ site: currentSite, product },
|
||||||
);
|
);
|
||||||
return renderedSku || `${currentSite.skuPrefix || ''}${product.sku || ''}`;
|
return (
|
||||||
|
renderedSku || `${currentSite.skuPrefix || ''}${product.sku || ''}`
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return `${currentSite.skuPrefix || ''}${product.sku || ''}`;
|
return `${currentSite.skuPrefix || ''}${product.sku || ''}`;
|
||||||
}
|
}
|
||||||
|
|
@ -95,15 +97,16 @@ const SyncToSiteModal: React.FC<SyncToSiteModalProps> = ({
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onFinish={async (values) => {
|
onFinish={async (values) => {
|
||||||
console.log(`values`,values)
|
console.log(`values`, values);
|
||||||
if (!values.siteId) return false;
|
if (!values.siteId) return false;
|
||||||
try {
|
try {
|
||||||
const siteSkusMap = values.siteSkus || {};
|
const siteSkusMap = values.siteSkus || {};
|
||||||
const data = products.map((product) => ({
|
const data = products.map((product) => ({
|
||||||
productId: product.id,
|
productId: product.id,
|
||||||
siteSku: siteSkusMap[product.id] || `${values.siteId}-${product.sku}`,
|
siteSku:
|
||||||
|
siteSkusMap[product.id] || `${values.siteId}-${product.sku}`,
|
||||||
}));
|
}));
|
||||||
console.log(`data`,data)
|
console.log(`data`, data);
|
||||||
const result = await productcontrollerBatchsynctosite({
|
const result = await productcontrollerBatchsynctosite({
|
||||||
siteId: values.siteId,
|
siteId: values.siteId,
|
||||||
data,
|
data,
|
||||||
|
|
@ -128,7 +131,14 @@ const SyncToSiteModal: React.FC<SyncToSiteModalProps> = ({
|
||||||
<ProFormDependency key={row.id} name={['siteId']}>
|
<ProFormDependency key={row.id} name={['siteId']}>
|
||||||
{({ siteId }) => (
|
{({ siteId }) => (
|
||||||
<div style={{ marginBottom: 16 }}>
|
<div style={{ marginBottom: 16 }}>
|
||||||
<div style={{ display: 'flex', gap: 8, alignItems: 'center', marginBottom: 8 }}>
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: 8,
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div style={{ minWidth: 220 }}>原始SKU: {row.sku || '-'}</div>
|
<div style={{ minWidth: 220 }}>原始SKU: {row.sku || '-'}</div>
|
||||||
<div style={{ minWidth: 150 }}>
|
<div style={{ minWidth: 150 }}>
|
||||||
已有商品SKU:{' '}
|
已有商品SKU:{' '}
|
||||||
|
|
@ -150,9 +160,12 @@ const SyncToSiteModal: React.FC<SyncToSiteModalProps> = ({
|
||||||
fieldProps={{
|
fieldProps={{
|
||||||
onChange: (e) => {
|
onChange: (e) => {
|
||||||
// 手动输入时更新表单值
|
// 手动输入时更新表单值
|
||||||
const currentValues = formRef.current?.getFieldValue('siteSkus') || {};
|
const currentValues =
|
||||||
|
formRef.current?.getFieldValue('siteSkus') || {};
|
||||||
currentValues[row.id] = e.target.value;
|
currentValues[row.id] = e.target.value;
|
||||||
formRef.current?.setFieldsValue({ siteSkus: currentValues });
|
formRef.current?.setFieldsValue({
|
||||||
|
siteSkus: currentValues,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
@ -162,11 +175,18 @@ const SyncToSiteModal: React.FC<SyncToSiteModalProps> = ({
|
||||||
size="small"
|
size="small"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
if (siteId) {
|
if (siteId) {
|
||||||
const currentSite = sites.find((s: any) => s.id === siteId) || {};
|
const currentSite =
|
||||||
const siteSku = await generateSingleSiteSku(currentSite, row);
|
sites.find((s: any) => s.id === siteId) || {};
|
||||||
const currentValues = formRef.current?.getFieldValue('siteSkus') || {};
|
const siteSku = await generateSingleSiteSku(
|
||||||
|
currentSite,
|
||||||
|
row,
|
||||||
|
);
|
||||||
|
const currentValues =
|
||||||
|
formRef.current?.getFieldValue('siteSkus') || {};
|
||||||
currentValues[row.id] = siteSku;
|
currentValues[row.id] = siteSku;
|
||||||
formRef.current?.setFieldsValue({ siteSkus: currentValues });
|
formRef.current?.setFieldsValue({
|
||||||
|
siteSkus: currentValues,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,8 @@ import {
|
||||||
productcontrollerBatchupdateproduct,
|
productcontrollerBatchupdateproduct,
|
||||||
productcontrollerDeleteproduct,
|
productcontrollerDeleteproduct,
|
||||||
productcontrollerGetcategoriesall,
|
productcontrollerGetcategoriesall,
|
||||||
productcontrollerGetproductcomponents,
|
|
||||||
productcontrollerGetproductlist,
|
productcontrollerGetproductlist,
|
||||||
productcontrollerUpdatenamecn
|
productcontrollerUpdatenamecn,
|
||||||
} from '@/servers/api/product';
|
} from '@/servers/api/product';
|
||||||
import {
|
import {
|
||||||
ActionType,
|
ActionType,
|
||||||
|
|
@ -14,7 +13,7 @@ import {
|
||||||
ProColumns,
|
ProColumns,
|
||||||
ProFormSelect,
|
ProFormSelect,
|
||||||
ProFormText,
|
ProFormText,
|
||||||
ProTable
|
ProTable,
|
||||||
} from '@ant-design/pro-components';
|
} from '@ant-design/pro-components';
|
||||||
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';
|
||||||
|
|
@ -154,13 +153,18 @@ const BatchEditModal: React.FC<{
|
||||||
</ModalForm>
|
</ModalForm>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
const ProductList = ({ filter, columns }: { filter: { skus: string[] }, columns: any[] }) => {
|
const ProductList = ({
|
||||||
|
filter,
|
||||||
|
columns,
|
||||||
|
}: {
|
||||||
|
filter: { skus: string[] };
|
||||||
|
columns: any[];
|
||||||
|
}) => {
|
||||||
return (
|
return (
|
||||||
<ProTable
|
<ProTable
|
||||||
request={async (pag) => {
|
request={async (pag) => {
|
||||||
const { data, success } = await productcontrollerGetproductlist({
|
const { data, success } = await productcontrollerGetproductlist({
|
||||||
where: filter
|
where: filter,
|
||||||
});
|
});
|
||||||
if (!success) return [];
|
if (!success) return [];
|
||||||
return data || [];
|
return data || [];
|
||||||
|
|
@ -170,7 +174,7 @@ const ProductList = ({ filter, columns }: { filter: { skus: string[] }, columns:
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
bordered
|
bordered
|
||||||
size="small"
|
size="small"
|
||||||
scroll={{ x: "max-content" }}
|
scroll={{ x: 'max-content' }}
|
||||||
headerTitle={null}
|
headerTitle={null}
|
||||||
toolBarRender={false}
|
toolBarRender={false}
|
||||||
/>
|
/>
|
||||||
|
|
@ -219,6 +223,7 @@ const List: React.FC = () => {
|
||||||
{
|
{
|
||||||
title: '关联商品',
|
title: '关联商品',
|
||||||
dataIndex: 'siteSkus',
|
dataIndex: 'siteSkus',
|
||||||
|
width: 200,
|
||||||
render: (_, record) => (
|
render: (_, record) => (
|
||||||
<>
|
<>
|
||||||
{record.siteSkus?.map((siteSku, index) => (
|
{record.siteSkus?.map((siteSku, index) => (
|
||||||
|
|
@ -244,7 +249,6 @@ const List: React.FC = () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
{
|
{
|
||||||
title: '价格',
|
title: '价格',
|
||||||
dataIndex: 'price',
|
dataIndex: 'price',
|
||||||
|
|
@ -257,7 +261,7 @@ const List: React.FC = () => {
|
||||||
hideInSearch: true,
|
hideInSearch: true,
|
||||||
sorter: true,
|
sorter: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '商品类型',
|
title: '商品类型',
|
||||||
dataIndex: 'category',
|
dataIndex: 'category',
|
||||||
render: (_, record: any) => {
|
render: (_, record: any) => {
|
||||||
|
|
@ -443,7 +447,6 @@ const List: React.FC = () => {
|
||||||
onError?.(error);
|
onError?.(error);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|
||||||
>
|
>
|
||||||
<Button>批量导入</Button>
|
<Button>批量导入</Button>
|
||||||
</Upload>,
|
</Upload>,
|
||||||
|
|
@ -506,8 +509,8 @@ const List: React.FC = () => {
|
||||||
sortField = field;
|
sortField = field;
|
||||||
sortOrder = sort[field];
|
sortOrder = sort[field];
|
||||||
}
|
}
|
||||||
const { current, pageSize, ...where } = params
|
const { current, pageSize, ...where } = params;
|
||||||
console.log(`params`, params)
|
console.log(`params`, params);
|
||||||
const { data, success } = await productcontrollerGetproductlist({
|
const { data, success } = await productcontrollerGetproductlist({
|
||||||
where,
|
where,
|
||||||
page: current || 1,
|
page: current || 1,
|
||||||
|
|
@ -573,5 +576,4 @@ const List: React.FC = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export default List;
|
export default List;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import { showBatchOperationResult } from '@/utils/showResult';
|
|
||||||
import {
|
import {
|
||||||
productcontrollerBatchsynctosite,
|
productcontrollerBatchsynctosite,
|
||||||
productcontrollerGetproductlist,
|
productcontrollerGetproductlist,
|
||||||
|
|
|
||||||
|
|
@ -5,14 +5,29 @@ import {
|
||||||
sitecontrollerList,
|
sitecontrollerList,
|
||||||
sitecontrollerUpdate,
|
sitecontrollerUpdate,
|
||||||
} from '@/servers/api/site';
|
} from '@/servers/api/site';
|
||||||
import { subscriptioncontrollerSync } from '@/servers/api/subscription';
|
|
||||||
import { stockcontrollerGetallstockpoints } from '@/servers/api/stock';
|
import { stockcontrollerGetallstockpoints } from '@/servers/api/stock';
|
||||||
import { ActionType, ProColumns, ProTable, DrawerForm, ProFormSelect, ProFormSwitch } from '@ant-design/pro-components';
|
import { subscriptioncontrollerSync } from '@/servers/api/subscription';
|
||||||
import { Button, message, notification, Popconfirm, Space, Tag, Form } from 'antd';
|
import {
|
||||||
import React, { useRef, useState, useEffect } from 'react';
|
ActionType,
|
||||||
import EditSiteForm from '../Shop/EditSiteForm'; // 引入重构后的表单组件
|
DrawerForm,
|
||||||
|
ProColumns,
|
||||||
|
ProFormSelect,
|
||||||
|
ProFormSwitch,
|
||||||
|
ProTable,
|
||||||
|
} from '@ant-design/pro-components';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Form,
|
||||||
|
message,
|
||||||
|
notification,
|
||||||
|
Popconfirm,
|
||||||
|
Space,
|
||||||
|
Tag,
|
||||||
|
} from 'antd';
|
||||||
import * as countries from 'i18n-iso-countries';
|
import * as countries from 'i18n-iso-countries';
|
||||||
import zhCN from 'i18n-iso-countries/langs/zh';
|
import zhCN from 'i18n-iso-countries/langs/zh';
|
||||||
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
import EditSiteForm from '../Shop/EditSiteForm'; // 引入重构后的表单组件
|
||||||
|
|
||||||
// 区域数据项类型
|
// 区域数据项类型
|
||||||
interface AreaItem {
|
interface AreaItem {
|
||||||
|
|
@ -49,7 +64,6 @@ const SiteList: React.FC = () => {
|
||||||
const [batchEditForm] = Form.useForm();
|
const [batchEditForm] = Form.useForm();
|
||||||
countries.registerLocale(zhCN);
|
countries.registerLocale(zhCN);
|
||||||
|
|
||||||
|
|
||||||
const handleSync = async (ids: number[]) => {
|
const handleSync = async (ids: number[]) => {
|
||||||
if (!ids.length) return;
|
if (!ids.length) return;
|
||||||
const hide = message.loading('正在同步...', 0);
|
const hide = message.loading('正在同步...', 0);
|
||||||
|
|
@ -206,8 +220,8 @@ const SiteList: React.FC = () => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// 地区列配置
|
// 地区列配置
|
||||||
title: "地区",
|
title: '地区',
|
||||||
dataIndex: "areas",
|
dataIndex: 'areas',
|
||||||
hideInSearch: true,
|
hideInSearch: true,
|
||||||
render: (_, row) => {
|
render: (_, row) => {
|
||||||
// 如果没有关联地区,显示"全局"标签
|
// 如果没有关联地区,显示"全局"标签
|
||||||
|
|
@ -310,10 +324,10 @@ const SiteList: React.FC = () => {
|
||||||
try {
|
try {
|
||||||
const { current, pageSize, name, type } = params;
|
const { current, pageSize, name, type } = params;
|
||||||
const resp = await sitecontrollerList({
|
const resp = await sitecontrollerList({
|
||||||
current,
|
current,
|
||||||
pageSize,
|
pageSize,
|
||||||
keyword: name || undefined,
|
keyword: name || undefined,
|
||||||
type: type || undefined,
|
type: type || undefined,
|
||||||
});
|
});
|
||||||
// 假设 resp 直接就是后端返回的结构,包含 items 和 total
|
// 假设 resp 直接就是后端返回的结构,包含 items 和 total
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -465,7 +465,11 @@ const CustomerPage: React.FC = () => {
|
||||||
<ProTable
|
<ProTable
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
search={false}
|
search={false}
|
||||||
pagination={{ pageSize: 20 ,showSizeChanger: true, showQuickJumper: true,}}
|
pagination={{
|
||||||
|
pageSize: 20,
|
||||||
|
showSizeChanger: true,
|
||||||
|
showQuickJumper: true,
|
||||||
|
}}
|
||||||
columns={[
|
columns={[
|
||||||
{ title: '订单号', dataIndex: 'number', copyable: true },
|
{ title: '订单号', dataIndex: 'number', copyable: true },
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import { areacontrollerGetarealist } from '@/servers/api/area';
|
|
||||||
import { stockcontrollerGetallstockpoints } from '@/servers/api/stock';
|
import { stockcontrollerGetallstockpoints } from '@/servers/api/stock';
|
||||||
import {
|
import {
|
||||||
DrawerForm,
|
DrawerForm,
|
||||||
|
|
@ -9,9 +8,9 @@ import {
|
||||||
ProFormTextArea,
|
ProFormTextArea,
|
||||||
} from '@ant-design/pro-components';
|
} from '@ant-design/pro-components';
|
||||||
import { Form } from 'antd';
|
import { Form } from 'antd';
|
||||||
import React, { useEffect } from 'react';
|
|
||||||
import * as countries from 'i18n-iso-countries';
|
import * as countries from 'i18n-iso-countries';
|
||||||
import zhCN from 'i18n-iso-countries/langs/zh';
|
import zhCN from 'i18n-iso-countries/langs/zh';
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
|
||||||
// 定义组件的 props 类型
|
// 定义组件的 props 类型
|
||||||
interface EditSiteFormProps {
|
interface EditSiteFormProps {
|
||||||
|
|
@ -41,7 +40,8 @@ const EditSiteForm: React.FC<EditSiteFormProps> = ({
|
||||||
// 如果是编辑模式并且有初始值
|
// 如果是编辑模式并且有初始值
|
||||||
if (isEdit && initialValues) {
|
if (isEdit && initialValues) {
|
||||||
// 编辑模式下, 设置表单值为初始值
|
// 编辑模式下, 设置表单值为初始值
|
||||||
const { token, consumerKey, consumerSecret, ...safeInitialValues } = initialValues;
|
const { token, consumerKey, consumerSecret, ...safeInitialValues } =
|
||||||
|
initialValues;
|
||||||
// 清空敏感字段, 让用户输入最新的数据
|
// 清空敏感字段, 让用户输入最新的数据
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
...safeInitialValues,
|
...safeInitialValues,
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ const OrdersPage: React.FC = () => {
|
||||||
dataIndex: 'id',
|
dataIndex: 'id',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title:'订单号',
|
title: '订单号',
|
||||||
dataIndex: 'number',
|
dataIndex: 'number',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -127,7 +127,7 @@ const OrdersPage: React.FC = () => {
|
||||||
title: '客户姓名',
|
title: '客户姓名',
|
||||||
dataIndex: 'customer_name',
|
dataIndex: 'customer_name',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '客户IP',
|
title: '客户IP',
|
||||||
dataIndex: 'customer_ip_address',
|
dataIndex: 'customer_ip_address',
|
||||||
},
|
},
|
||||||
|
|
@ -190,7 +190,7 @@ const OrdersPage: React.FC = () => {
|
||||||
ellipsis: true,
|
ellipsis: true,
|
||||||
copyable: true,
|
copyable: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '发货状态',
|
title: '发货状态',
|
||||||
dataIndex: 'fulfillment_status',
|
dataIndex: 'fulfillment_status',
|
||||||
// hideInSearch: true,
|
// hideInSearch: true,
|
||||||
|
|
@ -384,8 +384,7 @@ const OrdersPage: React.FC = () => {
|
||||||
setSelectedRowKeys={setSelectedRowKeys}
|
setSelectedRowKeys={setSelectedRowKeys}
|
||||||
siteId={siteId}
|
siteId={siteId}
|
||||||
/>,
|
/>,
|
||||||
<Button disabled>批量发货</Button>
|
<Button disabled>批量发货</Button>,
|
||||||
,
|
|
||||||
<Button
|
<Button
|
||||||
title="批量删除"
|
title="批量删除"
|
||||||
danger
|
danger
|
||||||
|
|
@ -541,9 +540,12 @@ const OrdersPage: React.FC = () => {
|
||||||
return { status: key, count: 0 };
|
return { status: key, count: 0 };
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const res = await request(`/site-api/${siteId}/orders/count`, {
|
const res = await request(
|
||||||
params: { ...baseWhere, status: rawStatus },
|
`/site-api/${siteId}/orders/count`,
|
||||||
});
|
{
|
||||||
|
params: { ...baseWhere, status: rawStatus },
|
||||||
|
},
|
||||||
|
);
|
||||||
const totalCount = Number(res?.data?.total || 0);
|
const totalCount = Number(res?.data?.total || 0);
|
||||||
return { status: key, count: totalCount };
|
return { status: key, count: totalCount };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
||||||
|
|
@ -20,11 +20,10 @@ import {
|
||||||
} from '@ant-design/pro-components';
|
} from '@ant-design/pro-components';
|
||||||
import { Button, Space, Tag } from 'antd';
|
import { Button, Space, Tag } from 'antd';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
import weekOfYear from 'dayjs/plugin/weekOfYear';
|
||||||
import ReactECharts from 'echarts-for-react';
|
import ReactECharts from 'echarts-for-react';
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import weekOfYear from 'dayjs/plugin/weekOfYear';
|
|
||||||
import zhCN from 'i18n-iso-countries/langs/zh';
|
|
||||||
countries.registerLocale(zhCN);
|
|
||||||
dayjs.extend(weekOfYear);
|
dayjs.extend(weekOfYear);
|
||||||
const highlightText = (text: string, keyword: string) => {
|
const highlightText = (text: string, keyword: string) => {
|
||||||
if (!keyword) return text;
|
if (!keyword) return text;
|
||||||
|
|
@ -143,7 +142,7 @@ const ListPage: React.FC = () => {
|
||||||
});
|
});
|
||||||
if (success) {
|
if (success) {
|
||||||
const res = data?.sort(() => -1);
|
const res = data?.sort(() => -1);
|
||||||
const formatMap = {
|
const formatMap = {
|
||||||
month: 'YYYY-MM',
|
month: 'YYYY-MM',
|
||||||
week: 'YYYY年第WW周',
|
week: 'YYYY年第WW周',
|
||||||
day: 'YYYY-MM-DD',
|
day: 'YYYY-MM-DD',
|
||||||
|
|
@ -151,14 +150,16 @@ const ListPage: React.FC = () => {
|
||||||
const format = formatMap[params.grouping] || 'YYYY-MM-DD';
|
const format = formatMap[params.grouping] || 'YYYY-MM-DD';
|
||||||
|
|
||||||
if (params.grouping === 'week') {
|
if (params.grouping === 'week') {
|
||||||
setXAxis(res?.map((v) => {
|
setXAxis(
|
||||||
const [year, week] = v.order_date.split('-');
|
res?.map((v) => {
|
||||||
return `${year}年第${week}周`;
|
const [year, week] = v.order_date.split('-');
|
||||||
}));
|
return `${year}年第${week}周`;
|
||||||
|
}),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
setXAxis(res?.map((v) => dayjs(v.order_date).format(format)));
|
setXAxis(res?.map((v) => dayjs(v.order_date).format(format)));
|
||||||
}
|
}
|
||||||
setSeries([
|
setSeries([
|
||||||
{
|
{
|
||||||
name: 'TOGO CPC订单数',
|
name: 'TOGO CPC订单数',
|
||||||
type: 'line',
|
type: 'line',
|
||||||
|
|
@ -612,7 +613,7 @@ const ListPage: React.FC = () => {
|
||||||
name="date"
|
name="date"
|
||||||
/>
|
/>
|
||||||
{/* <ProFormText label="关键词" name="keyword" /> */}
|
{/* <ProFormText label="关键词" name="keyword" /> */}
|
||||||
<ProFormSelect
|
<ProFormSelect
|
||||||
label="统计周期"
|
label="统计周期"
|
||||||
name="grouping"
|
name="grouping"
|
||||||
initialValue="day"
|
initialValue="day"
|
||||||
|
|
|
||||||
|
|
@ -51,9 +51,15 @@ const ListPage: React.FC = () => {
|
||||||
show: true,
|
show: true,
|
||||||
formatter: function (params) {
|
formatter: function (params) {
|
||||||
if (!params.value) return '';
|
if (!params.value) return '';
|
||||||
return Math.abs(params.value)
|
return (
|
||||||
+ '\n'
|
Math.abs(params.value) +
|
||||||
+ Math.abs(data?.inactiveRes?.find((item) => item.order_month === params.name)?.new_user_total || 0);
|
'\n' +
|
||||||
|
Math.abs(
|
||||||
|
data?.inactiveRes?.find(
|
||||||
|
(item) => item.order_month === params.name,
|
||||||
|
)?.new_user_total || 0,
|
||||||
|
)
|
||||||
|
);
|
||||||
},
|
},
|
||||||
color: '#000000',
|
color: '#000000',
|
||||||
},
|
},
|
||||||
|
|
@ -71,9 +77,15 @@ const ListPage: React.FC = () => {
|
||||||
show: true,
|
show: true,
|
||||||
formatter: function (params) {
|
formatter: function (params) {
|
||||||
if (!params.value) return '';
|
if (!params.value) return '';
|
||||||
return Math.abs(params.value)
|
return (
|
||||||
+ '\n'
|
Math.abs(params.value) +
|
||||||
+ Math.abs(data?.inactiveRes?.find((item) => item.order_month === params.name)?.old_user_total || 0);
|
'\n' +
|
||||||
|
Math.abs(
|
||||||
|
data?.inactiveRes?.find(
|
||||||
|
(item) => item.order_month === params.name,
|
||||||
|
)?.old_user_total || 0,
|
||||||
|
)
|
||||||
|
);
|
||||||
},
|
},
|
||||||
color: '#000000',
|
color: '#000000',
|
||||||
},
|
},
|
||||||
|
|
@ -93,10 +105,17 @@ const ListPage: React.FC = () => {
|
||||||
show: true,
|
show: true,
|
||||||
formatter: function (params) {
|
formatter: function (params) {
|
||||||
if (!params.value) return '';
|
if (!params.value) return '';
|
||||||
return Math.abs(params.value)
|
return (
|
||||||
+ '\n' +
|
Math.abs(params.value) +
|
||||||
+Math.abs(data?.res?.find((item) => item.order_month === params.name &&
|
'\n' +
|
||||||
item.first_order_month_group === v)?.total || 0);
|
+Math.abs(
|
||||||
|
data?.res?.find(
|
||||||
|
(item) =>
|
||||||
|
item.order_month === params.name &&
|
||||||
|
item.first_order_month_group === v,
|
||||||
|
)?.total || 0,
|
||||||
|
)
|
||||||
|
);
|
||||||
},
|
},
|
||||||
color: '#000000',
|
color: '#000000',
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,249 @@
|
||||||
|
import { ordercontrollerGetordersales } from '@/servers/api/order';
|
||||||
|
import { sitecontrollerAll } from '@/servers/api/site';
|
||||||
|
import {
|
||||||
|
ActionType,
|
||||||
|
PageContainer,
|
||||||
|
ProColumns,
|
||||||
|
ProFormSwitch,
|
||||||
|
ProTable,
|
||||||
|
} from '@ant-design/pro-components';
|
||||||
|
import { Button } from 'antd';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { saveAs } from 'file-saver';
|
||||||
|
import { useRef, useState } from 'react';
|
||||||
|
import * as XLSX from 'xlsx';
|
||||||
|
|
||||||
|
const ListPage: React.FC = () => {
|
||||||
|
const actionRef = useRef<ActionType>();
|
||||||
|
const formRef = useRef();
|
||||||
|
const [total, setTotal] = useState(0);
|
||||||
|
const [isSource, setIsSource] = useState(false);
|
||||||
|
const [yooneTotal, setYooneTotal] = useState({});
|
||||||
|
|
||||||
|
const columns: ProColumns<API.OrderSaleDTO>[] = [
|
||||||
|
{
|
||||||
|
title: '时间段',
|
||||||
|
dataIndex: 'dateRange',
|
||||||
|
valueType: 'dateTimeRange',
|
||||||
|
hideInTable: true,
|
||||||
|
formItemProps: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请选择时间段',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '排除套装',
|
||||||
|
dataIndex: 'exceptPackage',
|
||||||
|
valueType: 'switch',
|
||||||
|
hideInTable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '产品名称',
|
||||||
|
dataIndex: 'sku',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '产品名称',
|
||||||
|
dataIndex: 'name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '站点',
|
||||||
|
dataIndex: 'siteId',
|
||||||
|
valueType: 'select',
|
||||||
|
request: async () => {
|
||||||
|
const { data = [] } = await sitecontrollerAll();
|
||||||
|
return data.map((item) => ({
|
||||||
|
label: item.name,
|
||||||
|
value: item.id,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
hideInTable: true,
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// title: '分类',
|
||||||
|
// dataIndex: 'categoryName',
|
||||||
|
// hideInSearch: true,
|
||||||
|
// hideInTable: isSource,
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
title: '数量',
|
||||||
|
dataIndex: 'totalQuantity',
|
||||||
|
hideInSearch: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '一单订单数',
|
||||||
|
dataIndex: 'firstOrderCount',
|
||||||
|
hideInSearch: true,
|
||||||
|
render(_, record) {
|
||||||
|
if (isSource) return record.firstOrderCount;
|
||||||
|
return `${record.firstOrderCount}(${record.firstOrderYOONEBoxCount})`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '两单订单数',
|
||||||
|
dataIndex: 'secondOrderCount',
|
||||||
|
hideInSearch: true,
|
||||||
|
render(_, record) {
|
||||||
|
if (isSource) return record.secondOrderCount;
|
||||||
|
return `${record.secondOrderCount}(${record.secondOrderYOONEBoxCount})`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '三单订单数',
|
||||||
|
dataIndex: 'thirdOrderCount',
|
||||||
|
hideInSearch: true,
|
||||||
|
render(_, record) {
|
||||||
|
if (isSource) return record.thirdOrderCount;
|
||||||
|
return `${record.thirdOrderCount}(${record.thirdOrderYOONEBoxCount})`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '三单以上订单数',
|
||||||
|
dataIndex: 'moreThirdOrderCount',
|
||||||
|
hideInSearch: true,
|
||||||
|
render(_, record) {
|
||||||
|
if (isSource) return record.moreThirdOrderCount;
|
||||||
|
return `${record.moreThirdOrderCount}(${record.moreThirdOrderYOONEBoxCount})`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '订单数',
|
||||||
|
dataIndex: 'totalOrders',
|
||||||
|
hideInSearch: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
<PageContainer ghost>
|
||||||
|
<ProTable
|
||||||
|
headerTitle="查询表格"
|
||||||
|
actionRef={actionRef}
|
||||||
|
formRef={formRef}
|
||||||
|
rowKey="id"
|
||||||
|
params={{ isSource }}
|
||||||
|
form={{
|
||||||
|
// ignoreRules: false,
|
||||||
|
initialValues: {
|
||||||
|
dateRange: [dayjs().startOf('month'), dayjs().endOf('month')],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
request={async ({ dateRange, ...param }) => {
|
||||||
|
const [startDate, endDate] = dateRange.values();
|
||||||
|
const { data, success } = await ordercontrollerGetordersales({
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
...param,
|
||||||
|
});
|
||||||
|
if (success) {
|
||||||
|
setTotal(data?.totalQuantity || 0);
|
||||||
|
setYooneTotal({
|
||||||
|
yoone3Quantity: data?.yoone3Quantity || 0,
|
||||||
|
yoone6Quantity: data?.yoone6Quantity || 0,
|
||||||
|
yoone9Quantity: data?.yoone9Quantity || 0,
|
||||||
|
yoone12Quantity: data?.yoone12Quantity || 0,
|
||||||
|
yoone12QuantityNew: data?.yoone12QuantityNew || 0,
|
||||||
|
yoone15Quantity: data?.yoone15Quantity || 0,
|
||||||
|
yoone18Quantity: data?.yoone18Quantity || 0,
|
||||||
|
zexQuantity: data?.zexQuantity || 0,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
total: data?.total || 0,
|
||||||
|
data: data?.items || [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
setTotal(0);
|
||||||
|
setYooneTotal({});
|
||||||
|
return {
|
||||||
|
data: [],
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
columns={columns}
|
||||||
|
dateFormatter="number"
|
||||||
|
footer={() => `总计: ${total}`}
|
||||||
|
toolBarRender={() => [
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={async () => {
|
||||||
|
const { dateRange, param } = formRef.current?.getFieldsValue();
|
||||||
|
const [startDate, endDate] = dateRange.values();
|
||||||
|
const { data, success } = await ordercontrollerGetordersales({
|
||||||
|
startDate: dayjs(startDate).valueOf(),
|
||||||
|
endDate: dayjs(endDate).valueOf(),
|
||||||
|
...param,
|
||||||
|
current: 1,
|
||||||
|
pageSize: 20000,
|
||||||
|
});
|
||||||
|
if (!success) return;
|
||||||
|
// 表头
|
||||||
|
const headers = ['产品名', '数量'];
|
||||||
|
|
||||||
|
// 数据行
|
||||||
|
const rows = (data?.items || []).map((item) => {
|
||||||
|
return [item.name, item.totalQuantity];
|
||||||
|
});
|
||||||
|
|
||||||
|
// 导出
|
||||||
|
const sheet = XLSX.utils.aoa_to_sheet([headers, ...rows]);
|
||||||
|
const book = XLSX.utils.book_new();
|
||||||
|
XLSX.utils.book_append_sheet(book, sheet, '销售');
|
||||||
|
const buffer = XLSX.write(book, {
|
||||||
|
bookType: 'xlsx',
|
||||||
|
type: 'array',
|
||||||
|
});
|
||||||
|
const blob = new Blob([buffer], {
|
||||||
|
type: 'application/octet-stream',
|
||||||
|
});
|
||||||
|
saveAs(blob, '销售.xlsx');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
导出
|
||||||
|
</Button>,
|
||||||
|
<ProFormSwitch
|
||||||
|
label="原产品"
|
||||||
|
fieldProps={{
|
||||||
|
value: isSource,
|
||||||
|
onChange: () => setIsSource(!isSource),
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
background: '#fff',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
padding: '10px',
|
||||||
|
marginTop: '20px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
YOONE:{' '}
|
||||||
|
{(yooneTotal.yoone3Quantity || 0) +
|
||||||
|
(yooneTotal.yoone6Quantity || 0) +
|
||||||
|
(yooneTotal.yoone9Quantity || 0) +
|
||||||
|
(yooneTotal.yoone12Quantity || 0) +
|
||||||
|
(yooneTotal.yoone15Quantity || 0) +
|
||||||
|
(yooneTotal.yoone18Quantity || 0) +
|
||||||
|
(yooneTotal.zexQuantity || 0)}
|
||||||
|
</div>
|
||||||
|
<div>YOONE 3MG: {yooneTotal.yoone3Quantity || 0}</div>
|
||||||
|
<div>YOONE 6MG: {yooneTotal.yoone6Quantity || 0}</div>
|
||||||
|
<div>YOONE 9MG: {yooneTotal.yoone9Quantity || 0}</div>
|
||||||
|
<div>YOONE 12MG新: {yooneTotal.yoone12QuantityNew || 0}</div>
|
||||||
|
<div>
|
||||||
|
YOONE 12MG白:{' '}
|
||||||
|
{(yooneTotal.yoone12Quantity || 0) -
|
||||||
|
(yooneTotal.yoone12QuantityNew || 0)}
|
||||||
|
</div>
|
||||||
|
<div>YOONE 15MG: {yooneTotal.yoone15Quantity || 0}</div>
|
||||||
|
<div>YOONE 18MG: {yooneTotal.yoone18Quantity || 0}</div>
|
||||||
|
<div>ZEX: {yooneTotal.zexQuantity || 0}</div>
|
||||||
|
</div>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ListPage;
|
||||||
|
|
@ -41,6 +41,10 @@ const ListPage: React.FC = () => {
|
||||||
valueType: 'switch',
|
valueType: 'switch',
|
||||||
hideInTable: true,
|
hideInTable: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: '产品名称',
|
||||||
|
dataIndex: 'sku',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: '产品名称',
|
title: '产品名称',
|
||||||
dataIndex: 'name',
|
dataIndex: 'name',
|
||||||
|
|
@ -73,37 +77,31 @@ const ListPage: React.FC = () => {
|
||||||
title: '一单订单数',
|
title: '一单订单数',
|
||||||
dataIndex: 'firstOrderCount',
|
dataIndex: 'firstOrderCount',
|
||||||
hideInSearch: true,
|
hideInSearch: true,
|
||||||
render(_, record) {
|
},
|
||||||
if (isSource) return record.firstOrderCount;
|
{
|
||||||
return `${record.firstOrderCount}(${record.firstOrderYOONEBoxCount})`;
|
title: '一单YOONE盒数',
|
||||||
},
|
dataIndex: 'firstOrderYOONEBoxCount',
|
||||||
|
hideInSearch: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '两单订单数',
|
title: '两单订单数',
|
||||||
dataIndex: 'secondOrderCount',
|
dataIndex: 'secondOrderCount',
|
||||||
hideInSearch: true,
|
hideInSearch: true,
|
||||||
render(_, record) {
|
},
|
||||||
if (isSource) return record.secondOrderCount;
|
{
|
||||||
return `${record.secondOrderCount}(${record.secondOrderYOONEBoxCount})`;
|
title: '两单YOONE盒数',
|
||||||
},
|
dataIndex: 'secondOrderYOONEBoxCount',
|
||||||
|
hideInSearch: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '三单订单数',
|
title: '三单订单数',
|
||||||
dataIndex: 'thirdOrderCount',
|
dataIndex: 'thirdOrderCount',
|
||||||
hideInSearch: true,
|
hideInSearch: true,
|
||||||
render(_, record) {
|
|
||||||
if (isSource) return record.thirdOrderCount;
|
|
||||||
return `${record.thirdOrderCount}(${record.thirdOrderYOONEBoxCount})`;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '三单以上订单数',
|
title: '三单YOONE盒数',
|
||||||
dataIndex: 'moreThirdOrderCount',
|
dataIndex: 'thirdOrderYOONEBoxCount',
|
||||||
hideInSearch: true,
|
hideInSearch: true,
|
||||||
render(_, record) {
|
|
||||||
if (isSource) return record.moreThirdOrderCount;
|
|
||||||
return `${record.moreThirdOrderCount}(${record.moreThirdOrderYOONEBoxCount})`;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '订单数',
|
title: '订单数',
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,9 @@ import {
|
||||||
ProTable,
|
ProTable,
|
||||||
} from '@ant-design/pro-components';
|
} from '@ant-design/pro-components';
|
||||||
import { App, Button, Divider, Popconfirm, Space, Tag } from 'antd';
|
import { App, Button, Divider, Popconfirm, Space, Tag } from 'antd';
|
||||||
import { useRef } from 'react';
|
|
||||||
import * as countries from 'i18n-iso-countries';
|
import * as countries from 'i18n-iso-countries';
|
||||||
import zhCN from 'i18n-iso-countries/langs/zh';
|
import zhCN from 'i18n-iso-countries/langs/zh';
|
||||||
|
import { useRef } from 'react';
|
||||||
|
|
||||||
// 初始化中文语言包
|
// 初始化中文语言包
|
||||||
countries.registerLocale(zhCN);
|
countries.registerLocale(zhCN);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue