Compare commits

..

2 Commits

Author SHA1 Message Date
tikkhun d0097aec38 feat(Product): 新增品牌空间页面
refactor: 优化订单列表和统计页面的代码格式和逻辑

style: 调整多个页面的代码格式和导入顺序

fix: 修复订单列表中的物流信息显示问题

chore: 更新路由配置添加品牌空间页面
2026-01-14 19:57:15 +08:00
tikkhun 66256acee0 feat(产品分类): 在分类页面显示短名称并支持编辑短名称字段
feat(CSV工具): 增强SKU生成功能并支持CSV文件解析

1. 在分类页面显示短名称并支持编辑短名称字段
2. 重构SKU生成逻辑,支持更多属性组合
3. 添加对CSV文件的支持,优化中文编码处理
4. 增加尺寸和数量属性支持
5. 添加为single类型生成bundle SKU的选项
6. 优化表单选项显示,同时展示名称和短名称
2026-01-14 19:54:13 +08:00
25 changed files with 1667 additions and 470 deletions

View File

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

View File

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

View File

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

View File

@ -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: '标签',

View File

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

View File

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

View File

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

View File

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

View File

@ -1,16 +1,14 @@
import { productcontrollerGetcategoriesall } from '@/servers/api/product';
import { UploadOutlined } from '@ant-design/icons'; import { UploadOutlined } from '@ant-design/icons';
import { import {
PageContainer, PageContainer,
ProForm, ProForm,
ProFormSelect, ProFormSelect,
ProFormText,
} from '@ant-design/pro-components'; } from '@ant-design/pro-components';
import { request } from '@umijs/max'; import { request } from '@umijs/max';
import { Button, Card, Col, Input, message, Row, Upload } from 'antd'; import { Button, Card, Checkbox, Col, Input, message, Row, Upload } from 'antd';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import * as XLSX from 'xlsx'; import * as XLSX from 'xlsx';
import { productcontrollerGetcategoriesall } from '@/servers/api/product';
// 定义站点接口 // 定义站点接口
interface Site { interface Site {
@ -20,14 +18,22 @@ interface Site {
isDisabled?: boolean; isDisabled?: boolean;
} }
// 定义选项接口,用于下拉选择框的选项
interface Option {
name: string; // 显示名称
shortName: string; // 短名称用于生成SKU
}
// 定义配置接口 // 定义配置接口
interface SkuConfig { interface SkuConfig {
brands: string[]; brands: Option[];
categories: string[]; categories: Option[];
flavors: string[]; flavors: Option[];
strengths: string[]; strengths: Option[];
humidities: string[]; humidities: Option[];
versions: string[]; versions: Option[];
sizes: Option[];
quantities: Option[];
} }
// 定义通用属性映射接口用于存储属性名称和shortName的对应关系 // 定义通用属性映射接口用于存储属性名称和shortName的对应关系
@ -43,6 +49,8 @@ interface AttributeMappings {
strengths: AttributeMapping; strengths: AttributeMapping;
humidities: AttributeMapping; humidities: AttributeMapping;
versions: AttributeMapping; versions: AttributeMapping;
sizes: AttributeMapping;
quantities: AttributeMapping;
} }
/** /**
@ -57,6 +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 [config, setConfig] = useState<SkuConfig>({ const [config, setConfig] = useState<SkuConfig>({
brands: [], brands: [],
categories: [], categories: [],
@ -64,16 +74,22 @@ const CsvTool: React.FC = () => {
strengths: [], strengths: [],
humidities: [], humidities: [],
versions: [], versions: [],
sizes: [],
quantities: [],
}); });
// 所有属性名称到shortName的映射 // 所有属性名称到shortName的映射
const [attributeMappings, setAttributeMappings] = useState<AttributeMappings>({ const [attributeMappings, setAttributeMappings] = useState<AttributeMappings>(
brands: {}, {
categories: {}, brands: {},
flavors: {}, categories: {},
strengths: {}, flavors: {},
humidities: {}, strengths: {},
versions: {} humidities: {},
}); versions: {},
sizes: {},
quantities: {},
},
);
// 在组件加载时获取站点列表和字典数据 // 在组件加载时获取站点列表和字典数据
useEffect(() => { useEffect(() => {
@ -92,21 +108,24 @@ const CsvTool: React.FC = () => {
const dictListResponse = await request('/dict/list'); const dictListResponse = await request('/dict/list');
const dictList = dictListResponse?.data || dictListResponse || []; const dictList = dictListResponse?.data || dictListResponse || [];
// 3. 根据字典名称获取字典项返回包含name和shortName的对象数组 // 3. 根据字典名称获取字典项返回包含name和shortName的完整对象数组
const getDictItems = async (dictName: string) => { const getDictItems = async (dictName: string) => {
try { try {
const dict = dictList.find((d: any) => d.name === dictName); const dict = dictList.find((d: any) => d.name === dictName);
if (!dict) { if (!dict) {
console.warn(`Dictionary ${dictName} not found`); console.warn(`Dictionary ${dictName} not found`);
return { names: [], mapping: {} }; return { options: [], mapping: {} };
} }
const itemsResponse = await request('/dict/items', { const itemsResponse = await request('/dict/items', {
params: { dictId: dict.id }, params: { dictId: dict.id },
}); });
const items = itemsResponse?.data || itemsResponse || []; const items = itemsResponse?.data || itemsResponse || [];
// 提取名称数组 // 创建完整的选项数组
const names = items.map((item: any) => item.name); const options = items.map((item: any) => ({
name: item.name,
shortName: item.shortName || item.name,
}));
// 创建name到shortName的映射 // 创建name到shortName的映射
const mapping = items.reduce((acc: AttributeMapping, item: any) => { const mapping = items.reduce((acc: AttributeMapping, item: any) => {
@ -114,58 +133,94 @@ const CsvTool: React.FC = () => {
return acc; return acc;
}, {}); }, {});
return { names, mapping }; return { options, mapping };
} catch (error) { } catch (error) {
console.error(`Failed to fetch items for ${dictName}:`, error); console.error(`Failed to fetch items for ${dictName}:`, error);
return { names: [], mapping: {} }; return { options: [], mapping: {} };
} }
}; };
// 4. 获取所有字典项(品牌、口味、强度、湿度、版本) // 4. 获取所有字典项(品牌、口味、强度、湿度、版本、尺寸、数量)
const [brandResult, flavorResult, strengthResult, humidityResult, versionResult] = await Promise.all([ const [
brandResult,
flavorResult,
strengthResult,
humidityResult,
versionResult,
sizeResult,
quantityResult,
] = await Promise.all([
getDictItems('brand'), getDictItems('brand'),
getDictItems('flavor'), getDictItems('flavor'),
getDictItems('strength'), getDictItems('strength'),
getDictItems('humidity'), getDictItems('humidity'),
getDictItems('version'), getDictItems('version'),
getDictItems('size'),
getDictItems('quantity'),
]); ]);
// 5. 获取商品分类列表 // 5. 获取商品分类列表
const categoriesResponse = await productcontrollerGetcategoriesall(); const categoriesResponse = await productcontrollerGetcategoriesall();
const categories = categoriesResponse?.data?.map((category: any) => category.name) || []; const categoryOptions =
categoriesResponse?.data?.map((category: any) => ({
name: category.name,
shortName: category.shortName || category.name,
})) || [];
// 商品分类的映射如果分类有shortName的话 // 商品分类的映射如果分类有shortName的话
const categoryMapping = categoriesResponse?.data?.reduce((acc: AttributeMapping, category: any) => { const categoryMapping =
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;
},
{},
) || {};
// 7. 设置所有属性映射 // 6. 设置所有属性映射
setAttributeMappings({ setAttributeMappings({
brands: brandResult.mapping, brands: brandResult.mapping,
categories: categoryMapping, categories: categoryMapping,
flavors: flavorResult.mapping, flavors: flavorResult.mapping,
strengths: strengthResult.mapping, strengths: strengthResult.mapping,
humidities: humidityResult.mapping, humidities: humidityResult.mapping,
versions: versionResult.mapping versions: versionResult.mapping,
sizes: sizeResult.mapping,
quantities: quantityResult.mapping,
}); });
// 更新配置状态 // 更新配置状态
const newConfig = { const newConfig = {
brands: brandResult.names, brands: brandResult.options,
categories, categories: categoryOptions,
flavors: flavorResult.names, flavors: flavorResult.options,
strengths: strengthResult.names, strengths: strengthResult.options,
humidities: humidityResult.names, humidities: humidityResult.options,
versions: versionResult.names, versions: versionResult.options,
sizes: sizeResult.options,
quantities: quantityResult.options,
}; };
setConfig(newConfig); setConfig(newConfig);
form.setFieldsValue(newConfig); // 设置表单值时只需要name数组
form.setFieldsValue({
brands: brandResult.options.map((opt) => opt.name),
categories: categoryOptions.map((opt) => opt.name),
flavors: flavorResult.options.map((opt) => opt.name),
strengths: strengthResult.options.map((opt) => opt.name),
humidities: humidityResult.options.map((opt) => opt.name),
versions: versionResult.options.map((opt) => opt.name),
sizes: sizeResult.options.map((opt) => opt.name),
quantities: quantityResult.options.map((opt) => opt.name),
generateBundleSkuForSingle: true,
});
message.success({ content: '数据加载成功', key: 'loading' }); message.success({ content: '数据加载成功', key: 'loading' });
} catch (error) { } catch (error) {
console.error('Failed to fetch data:', error); console.error('Failed to fetch data:', error);
message.error({ content: '数据加载失败,请刷新页面重试', key: 'loading' }); message.error({
content: '数据加载失败,请刷新页面重试',
key: 'loading',
});
} }
}; };
@ -184,46 +239,95 @@ const CsvTool: React.FC = () => {
setFile(uploadedFile); setFile(uploadedFile);
const reader = new FileReader(); const reader = new FileReader();
reader.onload = (e) => {
try {
const data = e.target?.result;
// 如果是ArrayBuffer使用type: 'array'来处理
const workbook = XLSX.read(data, { type: 'array' });
const sheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[sheetName];
const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
if (jsonData.length < 2) { // 检查是否为CSV文件
message.error('文件为空或缺少表头!'); const isCsvFile = uploadedFile.name.match(/\.csv$/i);
setCsvData([]);
return;
}
// 将数组转换为对象数组 if (isCsvFile) {
const headers = jsonData[0] as string[]; // 对于CSV文件使用readAsText并指定UTF-8编码以正确处理中文
const rows = jsonData.slice(1).map((rowArray: any) => { reader.onload = (e) => {
const rowData: { [key: string]: any } = {}; try {
headers.forEach((header, index) => { const textData = e.target?.result as string;
rowData[header] = rowArray[index]; // 使用XLSX.read处理CSV文本数据指定type为'csv'并设置编码
const workbook = XLSX.read(textData, {
type: 'string',
codepage: 65001, // UTF-8 encoding
cellText: true,
cellDates: true,
}); });
return rowData; const sheetName = workbook.SheetNames[0];
}); const worksheet = workbook.Sheets[sheetName];
const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
if (jsonData.length < 2) {
message.error('文件为空或缺少表头!');
setCsvData([]);
return;
}
// 将数组转换为对象数组
const headers = jsonData[0] as string[];
const rows = jsonData.slice(1).map((rowArray: any) => {
const rowData: { [key: string]: any } = {};
headers.forEach((header, index) => {
rowData[header] = rowArray[index];
});
return rowData;
});
message.success(`成功解析 ${rows.length} 条数据.`);
setCsvData(rows);
setProcessedData([]); // 清空旧的处理结果
} catch (error) {
message.error('CSV文件解析失败,请检查文件格式和编码!');
console.error('CSV Parse Error:', error);
setCsvData([]);
}
};
reader.readAsText(uploadedFile, 'UTF-8');
} else {
// 对于Excel文件继续使用readAsArrayBuffer
reader.onload = (e) => {
try {
const data = e.target?.result;
// 如果是ArrayBuffer使用type: 'array'来处理
const workbook = XLSX.read(data, { type: 'array' });
const sheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[sheetName];
const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
if (jsonData.length < 2) {
message.error('文件为空或缺少表头!');
setCsvData([]);
return;
}
// 将数组转换为对象数组
const headers = jsonData[0] as string[];
const rows = jsonData.slice(1).map((rowArray: any) => {
const rowData: { [key: string]: any } = {};
headers.forEach((header, index) => {
rowData[header] = rowArray[index];
});
return rowData;
});
message.success(`成功解析 ${rows.length} 条数据.`);
setCsvData(rows);
setProcessedData([]); // 清空旧的处理结果
} catch (error) {
message.error('Excel文件解析失败,请检查文件格式!');
console.error('Excel Parse Error:', error);
setCsvData([]);
}
};
reader.readAsArrayBuffer(uploadedFile);
}
message.success(`成功解析 ${rows.length} 条数据.`);
setCsvData(rows);
setProcessedData([]); // 清空旧的处理结果
} catch (error) {
message.error('文件解析失败,请检查文件格式!');
console.error('File Parse Error:', error);
setCsvData([]);
}
};
reader.onerror = (error) => { reader.onerror = (error) => {
message.error('文件读取失败!'); message.error('文件读取失败!');
console.error('File Read Error:', error); console.error('File Read Error:', error);
}; };
// 使用readAsArrayBuffer替代已弃用的readAsBinaryString
reader.readAsArrayBuffer(uploadedFile);
return false; // 阻止antd Upload组件的默认上传行为 return false; // 阻止antd Upload组件的默认上传行为
}; };
@ -252,15 +356,19 @@ const CsvTool: React.FC = () => {
* @param {string} strength - * @param {string} strength -
* @param {string} humidity - 湿 * @param {string} humidity - 湿
* @param {string} version - * @param {string} version -
* @param {string} type -
* @returns {string} SKU * @returns {string} SKU
*/ */
const generateSku = ( const generateSku = (
brand: string, brand: string,
version: string,
category: string, category: string,
flavor: string, flavor: string,
strength: string, strength: string,
humidity: string, humidity: string,
version: string size: string,
quantity?: any,
type?: string,
): string => { ): string => {
// 构建SKU组件不包含站点前缀 // 构建SKU组件不包含站点前缀
const skuComponents: string[] = []; const skuComponents: string[] = [];
@ -271,9 +379,15 @@ const CsvTool: React.FC = () => {
const brandShortName = attributeMappings.brands[brand] || brand; const brandShortName = attributeMappings.brands[brand] || brand;
skuComponents.push(brandShortName); skuComponents.push(brandShortName);
} }
if (version) {
// 使用版本的shortName如果没有则使用版本名称
const versionShortName = attributeMappings.versions[version] || version;
skuComponents.push(versionShortName);
}
if (category) { if (category) {
// 使用分类的shortName如果没有则使用分类名称 // 使用分类的shortName如果没有则使用分类名称
const categoryShortName = attributeMappings.categories[category] || category; const categoryShortName =
attributeMappings.categories[category] || category;
skuComponents.push(categoryShortName); skuComponents.push(categoryShortName);
} }
if (flavor) { if (flavor) {
@ -283,24 +397,83 @@ 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);
} }
if (version) {
// 使用版本的shortName如果没有则使用版本名称 if (size) {
const versionShortName = attributeMappings.versions[version] || version; // 使用尺寸的shortName如果没有则使用尺寸名称
skuComponents.push(versionShortName); const sizeShortName = attributeMappings.sizes[size] || size;
skuComponents.push(sizeShortName);
} }
// 如果type为single且启用了生成bundle SKU则添加quantity
if (
quantity
) {
console.log(quantity, attributeMappings.quantities[quantity])
// 使用quantity的shortName如果没有则使用quantity但匹配 4 个零
const quantityShortName = attributeMappings.quantities[quantity] || Number(quantity).toString().padStart(4, '0');
skuComponents.push(quantityShortName);
}
// 合并所有组件,使用短横线分隔 // 合并所有组件,使用短横线分隔
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
@ -311,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}`;
@ -320,7 +493,7 @@ const CsvTool: React.FC = () => {
}); });
// 使用分号分隔所有站点的siteSkus // 使用分号分隔所有站点的siteSkus
return siteSkus.join(';').toUpperCase(); return [baseSku, ...siteSkus].join(';').toUpperCase();
}; };
/** /**
@ -352,32 +525,141 @@ const CsvTool: React.FC = () => {
const strength = row.attribute_strength || ''; const strength = row.attribute_strength || '';
const humidity = row.attribute_humidity || ''; const humidity = row.attribute_humidity || '';
const version = row.attribute_version || ''; const version = row.attribute_version || '';
const size = row.attribute_size || row.size || '';
// 将quantity保存到attribute_quantity字段
const quantity = row.attribute_quantity || row.quantity;
// 获取产品类型
const type = row.type || '';
// 生成基础SKU不包含站点前缀 // 生成基础SKU不包含站点前缀
const baseSku = generateSku(brand, category, flavor, strength, humidity, version); const baseSku = generateSku(
brand,
version,
category,
flavor,
strength,
humidity,
size,
quantity,
type,
);
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和siteSkus的行数据将SKU直接保存到sku栏
return { ...row, GeneratedSKU: baseSku, siteSkus }; return {
...row,
sku: baseSku, // 直接生成在sku栏
generatedName: name,
// name: name, // 生成的产品名称
siteSkus,
attribute_quantity: quantity, // 确保quantity保存到attribute_quantity
};
}); });
setProcessedData(dataWithSku); // Determine which data to use for processing and download
message.success({ content: 'SKU生成成功!正在自动下载...', key: 'processing' }); let finalData = dataWithSku;
// 自动下载 // If generateBundleSkuForSingle is enabled, generate bundle products for single products
downloadData(dataWithSku); if (generateBundleSkuForSingle) {
// Filter out single records
const singleRecords = dataWithSku.filter(
(row) => row.type === 'single',
);
// Get quantity values from the config (same source as other attributes like brand)
const quantityValues = config.quantities.map(quantity=>quantity.name)
// Generate bundle products for each single record and quantity
const generatedBundleRecords = singleRecords.flatMap((singleRecord) => {
return quantityValues.map((quantity) => {
// Extract all necessary attributes from the single record
const brand = singleRecord.attribute_brand || '';
const version = singleRecord.attribute_version || '';
const category = singleRecord.category || '';
const flavor = singleRecord.attribute_flavor || '';
const strength = singleRecord.attribute_strength || '';
const humidity = singleRecord.attribute_humidity || '';
const size = singleRecord.attribute_size || singleRecord.size || '';
// Generate bundle SKU with the quantity
const bundleSku = generateSku(
brand,
version,
category,
flavor,
strength,
humidity,
size,
quantity,
'bundle',
);
// Generate bundle name with the quantity
const bundleName = generateName(
brand,
version,
category,
flavor,
strength,
humidity,
size,
quantity,
'bundle',
);
// Generate siteSkus for the bundle
const bundleSiteSkus = generateSiteSkus(bundleSku);
// Create the bundle record
return {
...singleRecord,
type: 'bundle', // Change type to bundle
sku: bundleSku, // Use the new bundle SKU
name: bundleName, // Use the new bundle name
siteSkus: bundleSiteSkus,
attribute_quantity: quantity, // Set the attribute_quantity
component_1_sku: singleRecord.sku, // Set component_1_sku to the single product's sku
component_1_quantity: Number(quantity), // Set component_1_quantity to the same as attribute_quantity
};
});
});
// Combine original dataWithSku with generated bundle records
finalData = [...dataWithSku, ...generatedBundleRecords];
}
// Set the processed data
setProcessedData(finalData);
message.success({
content: 'SKU生成成功!正在自动下载...',
key: 'processing',
});
// 自动下载 the final data (with or without generated bundle products)
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]}>
@ -397,6 +679,10 @@ const CsvTool: React.FC = () => {
placeholder="请输入品牌,按回车确认" placeholder="请输入品牌,按回车确认"
rules={[{ required: true, message: '至少需要一个品牌' }]} rules={[{ required: true, message: '至少需要一个品牌' }]}
tooltip="品牌名称会作为SKU的第一个组成部分" tooltip="品牌名称会作为SKU的第一个组成部分"
options={config.brands.map((opt) => ({
label: `${opt.name} (${opt.shortName})`,
value: opt.name,
}))}
/> />
<ProFormSelect <ProFormSelect
name="categories" name="categories"
@ -405,6 +691,10 @@ const CsvTool: React.FC = () => {
placeholder="请输入分类,按回车确认" placeholder="请输入分类,按回车确认"
rules={[{ required: true, message: '至少需要一个分类' }]} rules={[{ required: true, message: '至少需要一个分类' }]}
tooltip="分类名称会作为SKU的第二个组成部分" tooltip="分类名称会作为SKU的第二个组成部分"
options={config.categories.map((opt) => ({
label: `${opt.name} (${opt.shortName})`,
value: opt.name,
}))}
/> />
<ProFormSelect <ProFormSelect
name="flavors" name="flavors"
@ -413,6 +703,10 @@ const CsvTool: React.FC = () => {
placeholder="请输入口味,按回车确认" placeholder="请输入口味,按回车确认"
rules={[{ required: true, message: '至少需要一个口味' }]} rules={[{ required: true, message: '至少需要一个口味' }]}
tooltip="口味名称会作为SKU的第三个组成部分" tooltip="口味名称会作为SKU的第三个组成部分"
options={config.flavors.map((opt) => ({
label: `${opt.name} (${opt.shortName})`,
value: opt.name,
}))}
/> />
<ProFormSelect <ProFormSelect
name="strengths" name="strengths"
@ -420,6 +714,10 @@ const CsvTool: React.FC = () => {
mode="tags" mode="tags"
placeholder="请输入强度,按回车确认" placeholder="请输入强度,按回车确认"
tooltip="强度信息会作为SKU的第四个组成部分" tooltip="强度信息会作为SKU的第四个组成部分"
options={config.strengths.map((opt) => ({
label: `${opt.name} (${opt.shortName})`,
value: opt.name,
}))}
/> />
<ProFormSelect <ProFormSelect
name="humidities" name="humidities"
@ -427,6 +725,10 @@ const CsvTool: React.FC = () => {
mode="tags" mode="tags"
placeholder="请输入湿度,按回车确认" placeholder="请输入湿度,按回车确认"
tooltip="湿度信息会作为SKU的第五个组成部分" tooltip="湿度信息会作为SKU的第五个组成部分"
options={config.humidities.map((opt) => ({
label: `${opt.name} (${opt.shortName})`,
value: opt.name,
}))}
/> />
<ProFormSelect <ProFormSelect
name="versions" name="versions"
@ -434,7 +736,47 @@ const CsvTool: React.FC = () => {
mode="tags" mode="tags"
placeholder="请输入版本,按回车确认" placeholder="请输入版本,按回车确认"
tooltip="版本信息会作为SKU的第六个组成部分" tooltip="版本信息会作为SKU的第六个组成部分"
options={config.versions.map((opt) => ({
label: `${opt.name} (${opt.shortName})`,
value: opt.name,
}))}
/> />
<ProFormSelect
name="sizes"
label="尺寸列表"
mode="tags"
placeholder="请输入尺寸,按回车确认"
tooltip="尺寸信息会作为SKU的第七个组成部分"
options={config.sizes.map((opt) => ({
label: `${opt.name} (${opt.shortName})`,
value: opt.name,
}))}
/>
<ProFormSelect
name="quantities"
label="数量列表"
mode="tags"
placeholder="请输入数量,按回车确认"
tooltip="数量信息会作为bundle SKU的组成部分"
options={config.quantities.map((opt) => ({
label: `${opt.name} (${opt.shortName})`,
value: opt.name,
}))}
fieldProps={{ allowClear: true }}
/>
<ProForm.Item
name="generateBundleSkuForSingle"
label="为type=single生成bundle产品数据行"
tooltip="为类型为single的记录生成包含quantity的bundle SKU"
valuePropName="checked"
initialValue={true}
>
<Checkbox onChange={setGenerateBundleSkuForSingle}>
single类型生成bundle SKU
</Checkbox>
</ProForm.Item>
</ProForm> </ProForm>
</Card> </Card>
@ -445,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.shortName}</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>
@ -492,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' }}
> >
@ -501,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>

View File

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

View File

@ -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 [];
} }

View File

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

View File

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

View File

@ -1,4 +1,3 @@
import { showBatchOperationResult } from '@/utils/showResult';
import { import {
productcontrollerBatchsynctosite, productcontrollerBatchsynctosite,
productcontrollerGetproductlist, productcontrollerGetproductlist,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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: '订单数',

View File

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