fix: 少量关于产品和订单的修改 #47

Merged
zksu merged 6 commits from zksu/WEB:main into main 2026-01-22 07:14:44 +00:00
19 changed files with 991 additions and 887 deletions

View File

@ -134,9 +134,14 @@ export default defineConfig({
}, },
{ {
name: '数据分析列表', name: '数据分析列表',
path: '/customer/statistic', path: '/customer/statistic/list',
component: './Customer/Statistic', component: './Customer/StatisticList',
}, },
// {
// name: '客户统计',
// path: '/customer/statistic/home',
// component: './Customer/Statistic',
// }
], ],
}, },
{ {
@ -166,8 +171,8 @@ export default defineConfig({
}, },
{ {
name: '产品品牌空间', name: '产品品牌空间',
path: '/product/brandspace', path: '/product/groupBy',
component: './Product/BrandSpace', component: './Product/GroupBy',
}, },
// sync // sync
{ {

View File

@ -4,8 +4,8 @@ import {
ActionType, ActionType,
DrawerForm, DrawerForm,
ProForm, ProForm,
ProFormSelect,
ProFormDateRangePicker, ProFormDateRangePicker,
ProFormSelect,
} from '@ant-design/pro-components'; } from '@ant-design/pro-components';
import { Button } from 'antd'; import { Button } from 'antd';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
@ -24,7 +24,12 @@ interface SyncFormProps {
* @param {SyncFormProps} props * @param {SyncFormProps} props
* @returns {React.ReactElement} * @returns {React.ReactElement}
*/ */
const SyncForm: React.FC<SyncFormProps> = ({ tableRef, onFinish, siteId, dateRange }) => { const SyncForm: React.FC<SyncFormProps> = ({
tableRef,
onFinish,
siteId,
dateRange,
}) => {
// 使用 antd 的 App 组件提供的 message API // 使用 antd 的 App 组件提供的 message API
const [loading, setLoading] = React.useState(false); const [loading, setLoading] = React.useState(false);
@ -52,11 +57,10 @@ const SyncForm: React.FC<SyncFormProps> = ({ tableRef, onFinish, siteId, dateRan
// 返回一个抽屉表单 // 返回一个抽屉表单
return ( return (
<DrawerForm<API.ordercontrollerSyncorderParams> <DrawerForm<API.ordercontrollerSyncorderParams>
initialValues={{ initialValues={{
dateRange: [dayjs().subtract(1, 'week'), dayjs()], dateRange: [dayjs().subtract(1, 'week'), dayjs()],
}} }}
title="同步订单"
title="同步订单"
// 表单的触发器,一个带图标的按钮 // 表单的触发器,一个带图标的按钮
trigger={ trigger={
<Button key="syncSite" type="primary"> <Button key="syncSite" type="primary">
@ -74,7 +78,6 @@ const SyncForm: React.FC<SyncFormProps> = ({ tableRef, onFinish, siteId, dateRan
onFinish={onFinish} onFinish={onFinish}
> >
<ProForm.Group> <ProForm.Group>
{/* 站点选择框 */} {/* 站点选择框 */}
<ProFormSelect <ProFormSelect
name="siteId" name="siteId"
@ -93,20 +96,19 @@ const SyncForm: React.FC<SyncFormProps> = ({ tableRef, onFinish, siteId, dateRan
/> />
<ProFormDateRangePicker <ProFormDateRangePicker
name="dateRange" name="dateRange"
label="同步日期范围" label="同步日期范围"
placeholder={['开始日期', '结束日期']} placeholder={['开始日期', '结束日期']}
transform={(value) => {
transform={(value) => { return {
return { dateRange: value,
dateRange: value, };
}; }}
}} fieldProps={{
fieldProps={{ showTime: false,
showTime: false, style: { width: '100%' },
style: { width: '100%' }, }}
}} />
/>
</ProForm.Group> </ProForm.Group>
</DrawerForm> </DrawerForm>
); );

View File

@ -1,289 +1,7 @@
import { HistoryOrder } from '@/pages/Statistics/Order'; export default function Statistic() {
import {
customercontrollerAddtag,
customercontrollerDeltag,
customercontrollerGetcustomerlist,
customercontrollerGettags,
customercontrollerSetrate,
} from '@/servers/api/customer';
import {
ActionType,
ModalForm,
PageContainer,
ProColumns,
ProFormSelect,
ProTable,
} from '@ant-design/pro-components';
import { App, Button, Rate, Space, Tag } from 'antd';
import dayjs from 'dayjs';
import { useRef, useState } from 'react';
const ListPage: React.FC = () => {
const actionRef = useRef<ActionType>();
const { message } = App.useApp();
const columns: ProColumns[] = [
{
title: '用户名',
dataIndex: 'username',
hideInSearch: true,
render: (_, record) => {
if (record.billing.first_name || record.billing.last_name)
return record.billing.first_name + ' ' + record.billing.last_name;
return record.shipping.first_name + ' ' + record.shipping.last_name;
},
},
{
title: '邮箱',
dataIndex: 'email',
},
{
title: '客户编号',
dataIndex: 'customerId',
render: (_, record) => {
if (!record.customerId) return '-';
return String(record.customerId).padStart(6, 0);
},
sorter: true,
},
{
title: '首单时间',
dataIndex: 'first_purchase_date',
valueType: 'dateMonth',
sorter: true,
render: (_, record) =>
record.first_purchase_date
? dayjs(record.first_purchase_date).format('YYYY-MM-DD HH:mm:ss')
: '-',
// search: {
// transform: (value: string) => {
// return { month: value };
// },
// },
},
{
title: '尾单时间',
hideInSearch: true,
dataIndex: 'last_purchase_date',
valueType: 'dateTime',
sorter: true,
},
{
title: '订单数',
dataIndex: 'orders',
hideInSearch: true,
sorter: true,
},
{
title: '金额',
dataIndex: 'total',
hideInSearch: true,
sorter: true,
},
{
title: 'YOONE订单数',
dataIndex: 'yoone_orders',
hideInSearch: true,
sorter: true,
},
{
title: 'YOONE金额',
dataIndex: 'yoone_total',
hideInSearch: true,
sorter: true,
},
{
title: '等级',
hideInSearch: true,
render: (_, record) => {
if (!record.yoone_orders || !record.yoone_total) return '-';
if (Number(record.yoone_orders) === 1 && Number(record.yoone_total) > 0)
return 'B';
return '-';
},
},
{
title: '评星',
dataIndex: 'rate',
width: 200,
render: (_, record) => {
return (
<Rate
onChange={async (val) => {
try {
const { success, message: msg } =
await customercontrollerSetrate({
id: record.customerId,
rate: val,
});
if (success) {
message.success(msg);
actionRef.current?.reload();
}
} catch (e) {
message.error(e.message);
}
}}
value={record.rate}
/>
);
},
},
{
title: '联系电话',
dataIndex: 'phone',
hideInSearch: true,
render: (_, record) => record?.billing.phone || record?.shipping.phone,
},
{
title: '账单地址',
dataIndex: 'billing',
render: (_, record) =>
JSON.stringify(record?.billing || record?.shipping),
},
{
title: '标签',
dataIndex: 'tags',
render: (_, record) => {
return (
<Space>
{(record.tags || []).map((tag) => {
return (
<Tag
key={tag}
closable
onClose={async () => {
const { success, message: msg } =
await customercontrollerDeltag({
email: record.email,
tag,
});
return false;
}}
>
{tag}
</Tag>
);
})}
</Space>
);
},
},
{
title: '操作',
dataIndex: 'option',
valueType: 'option',
fixed: 'right',
render: (_, record) => {
return (
<Space>
<AddTag
email={record.email}
tags={record.tags}
tableRef={actionRef}
/>
<HistoryOrder
email={record.email}
tags={record.tags}
tableRef={actionRef}
/>
</Space>
);
},
},
];
return ( return (
<PageContainer ghost> <div>
<ProTable <h1></h1>
scroll={{ x: 'max-content' }} </div>
headerTitle="查询表格"
actionRef={actionRef}
rowKey="id"
request={async (params, sorter) => {
const key = Object.keys(sorter)[0];
const { data, success } = await customercontrollerGetcustomerlist({
...params,
...(key ? { sorterKey: key, sorterValue: sorter[key] } : {}),
});
return {
total: data?.total || 0,
data: data?.items || [],
success,
};
}}
columns={columns}
/>
</PageContainer>
); );
}; }
export const AddTag: React.FC<{
email: string;
tags?: string[];
tableRef: React.MutableRefObject<ActionType | undefined>;
}> = ({ email, tags, tableRef }) => {
const { message } = App.useApp();
const [tagList, setTagList] = useState<string[]>([]);
return (
<ModalForm
title={`修改标签 - ${email}`}
trigger={<Button></Button>}
width={800}
modalProps={{
destroyOnHidden: true,
}}
submitter={false}
>
<ProFormSelect
mode="tags"
allowClear
name="tag"
label="标签"
request={async () => {
const { data, success } = await customercontrollerGettags();
if (!success) return [];
setTagList(tags || []);
return data
.filter((tag) => {
return !(tags || []).includes(tag);
})
.map((tag) => ({ label: tag, value: tag }));
}}
fieldProps={{
value: tagList, // 当前值
onChange: async (newValue) => {
const added = newValue.filter((x) => !tagList.includes(x));
const removed = tagList.filter((x) => !newValue.includes(x));
if (added.length) {
const { success, message: msg } = await customercontrollerAddtag({
email,
tag: added[0],
});
if (!success) {
message.error(msg);
return;
}
}
if (removed.length) {
const { success, message: msg } = await customercontrollerDeltag({
email,
tag: removed[0],
});
if (!success) {
message.error(msg);
return;
}
}
tableRef?.current?.reload();
setTagList(newValue);
},
}}
></ProFormSelect>
</ModalForm>
);
};
export default ListPage;

View File

@ -0,0 +1,289 @@
import { HistoryOrder } from '@/pages/Statistics/Order';
import {
customercontrollerAddtag,
customercontrollerDeltag,
customercontrollerGetcustomerlist,
customercontrollerGettags,
customercontrollerSetrate,
} from '@/servers/api/customer';
import {
ActionType,
ModalForm,
PageContainer,
ProColumns,
ProFormSelect,
ProTable,
} from '@ant-design/pro-components';
import { App, Button, Rate, Space, Tag } from 'antd';
import dayjs from 'dayjs';
import { useRef, useState } from 'react';
const ListPage: React.FC = () => {
const actionRef = useRef<ActionType>();
const { message } = App.useApp();
const columns: ProColumns[] = [
{
title: '用户名',
dataIndex: 'username',
hideInSearch: true,
render: (_, record) => {
if (record.billing.first_name || record.billing.last_name)
return record.billing.first_name + ' ' + record.billing.last_name;
return record.shipping.first_name + ' ' + record.shipping.last_name;
},
},
{
title: '邮箱',
dataIndex: 'email',
},
{
title: '客户编号',
dataIndex: 'customerId',
render: (_, record) => {
if (!record.customerId) return '-';
return String(record.customerId).padStart(6, 0);
},
sorter: true,
},
{
title: '首单时间',
dataIndex: 'first_purchase_date',
valueType: 'dateMonth',
sorter: true,
render: (_, record) =>
record.first_purchase_date
? dayjs(record.first_purchase_date).format('YYYY-MM-DD HH:mm:ss')
: '-',
// search: {
// transform: (value: string) => {
// return { month: value };
// },
// },
},
{
title: '尾单时间',
hideInSearch: true,
dataIndex: 'last_purchase_date',
valueType: 'dateTime',
sorter: true,
},
{
title: '订单数',
dataIndex: 'orders',
hideInSearch: true,
sorter: true,
},
{
title: '金额',
dataIndex: 'total',
hideInSearch: true,
sorter: true,
},
{
title: 'YOONE订单数',
dataIndex: 'yoone_orders',
hideInSearch: true,
sorter: true,
},
{
title: 'YOONE金额',
dataIndex: 'yoone_total',
hideInSearch: true,
sorter: true,
},
{
title: '等级',
hideInSearch: true,
render: (_, record) => {
if (!record.yoone_orders || !record.yoone_total) return '-';
if (Number(record.yoone_orders) === 1 && Number(record.yoone_total) > 0)
return 'B';
return '-';
},
},
{
title: '评星',
dataIndex: 'rate',
width: 200,
render: (_, record) => {
return (
<Rate
onChange={async (val) => {
try {
const { success, message: msg } =
await customercontrollerSetrate({
id: record.customerId,
rate: val,
});
if (success) {
message.success(msg);
actionRef.current?.reload();
}
} catch (e) {
message.error(e.message);
}
}}
value={record.rate}
/>
);
},
},
{
title: '联系电话',
dataIndex: 'phone',
hideInSearch: true,
render: (_, record) => record?.billing.phone || record?.shipping.phone,
},
{
title: '账单地址',
dataIndex: 'billing',
render: (_, record) =>
JSON.stringify(record?.billing || record?.shipping),
},
{
title: '标签',
dataIndex: 'tags',
render: (_, record) => {
return (
<Space>
{(record.tags || []).map((tag) => {
return (
<Tag
key={tag}
closable
onClose={async () => {
const { success, message: msg } =
await customercontrollerDeltag({
email: record.email,
tag,
});
return false;
}}
>
{tag}
</Tag>
);
})}
</Space>
);
},
},
{
title: '操作',
dataIndex: 'option',
valueType: 'option',
fixed: 'right',
render: (_, record) => {
return (
<Space>
<AddTag
email={record.email}
tags={record.tags}
tableRef={actionRef}
/>
<HistoryOrder
email={record.email}
tags={record.tags}
tableRef={actionRef}
/>
</Space>
);
},
},
];
return (
<PageContainer ghost>
<ProTable
scroll={{ x: 'max-content' }}
headerTitle="查询表格"
actionRef={actionRef}
rowKey="id"
request={async (params, sorter) => {
const key = Object.keys(sorter)[0];
const { data, success } = await customercontrollerGetcustomerlist({
...params,
...(key ? { sorterKey: key, sorterValue: sorter[key] } : {}),
});
return {
total: data?.total || 0,
data: data?.items || [],
success,
};
}}
columns={columns}
/>
</PageContainer>
);
};
export const AddTag: React.FC<{
email: string;
tags?: string[];
tableRef: React.MutableRefObject<ActionType | undefined>;
}> = ({ email, tags, tableRef }) => {
const { message } = App.useApp();
const [tagList, setTagList] = useState<string[]>([]);
return (
<ModalForm
title={`修改标签 - ${email}`}
trigger={<Button></Button>}
width={800}
modalProps={{
destroyOnHidden: true,
}}
submitter={false}
>
<ProFormSelect
mode="tags"
allowClear
name="tag"
label="标签"
request={async () => {
const { data, success } = await customercontrollerGettags();
if (!success) return [];
setTagList(tags || []);
return data
.filter((tag) => {
return !(tags || []).includes(tag);
})
.map((tag) => ({ label: tag, value: tag }));
}}
fieldProps={{
value: tagList, // 当前值
onChange: async (newValue) => {
const added = newValue.filter((x) => !tagList.includes(x));
const removed = tagList.filter((x) => !newValue.includes(x));
if (added.length) {
const { success, message: msg } = await customercontrollerAddtag({
email,
tag: added[0],
});
if (!success) {
message.error(msg);
return;
}
}
if (removed.length) {
const { success, message: msg } = await customercontrollerDeltag({
email,
tag: removed[0],
});
if (!success) {
message.error(msg);
return;
}
}
tableRef?.current?.reload();
setTagList(newValue);
},
}}
></ProFormSelect>
</ModalForm>
);
};
export default ListPage;

View File

@ -502,7 +502,10 @@ const ListPage: React.FC = () => {
success, success,
message: errMsg, message: errMsg,
data, data,
} = await ordercontrollerSyncorders(values,{after:values.dateRange?.[0]+'T00:00:00Z',before:values.dateRange?.[1]+'T23:59:59Z'}); } = await ordercontrollerSyncorders(values, {
after: values.dateRange?.[0] + 'T00:00:00Z',
before: values.dateRange?.[1] + 'T23:59:59Z',
});
if (!success) { if (!success) {
throw new Error(errMsg); throw new Error(errMsg);
} }
@ -990,7 +993,7 @@ const Detail: React.FC<{
<ul> <ul>
{record?.items?.map((item: any) => ( {record?.items?.map((item: any) => (
<li key={item.id}> <li key={item.id}>
{item.name}:{item.quantity} {item.name}({item.sku}):{item.quantity}
</li> </li>
))} ))}
</ul> </ul>
@ -1015,7 +1018,7 @@ const Detail: React.FC<{
<ul> <ul>
{record?.sales?.map((item: any) => ( {record?.sales?.map((item: any) => (
<li key={item.id}> <li key={item.id}>
{item.name}:{item.quantity} {item.name}({item.sku}):{item.quantity}
</li> </li>
))} ))}
</ul> </ul>
@ -1507,16 +1510,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
//</Col> ? Promise.resolve() //</Col> ? Promise.resolve()
// : Promise.reject('至少需要一个商品'), // : Promise.reject('至少需要一个商品'),
// }, // },
// ]} // ]}
> >
<ProForm.Group> <ProForm.Group>
<ProFormSelect <ProFormSelect

View File

@ -1,486 +0,0 @@
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);
const [attributeValuesLoading, setAttributeValuesLoading] = 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) => {
setAttributeValuesLoading(true);
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('获取属性值列表失败');
// Clear attribute values on error
setAttributeValues([]);
} finally {
setAttributeValuesLoading(false);
}
};
// 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 = response?.data?.items || [];
setProducts(productList);
} catch (error) {
console.error('Failed to fetch products:', error);
message.error('获取产品列表失败');
// Clear products on error
setProducts([]);
} 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
loading={attributeValuesLoading}
>
{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', overflowX: 'auto' }}>
<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>
) : attributeValues.length > 0 ? (
<div style={{ minWidth: 'max-content' }}>
<Row gutter={[16, 16]}>
{attributeValues.map((attributeValue) => {
// Filter products for this attribute value
if(!Array.isArray(products)){
console.log('products',products)
return null
}
const attributeValueProducts = selectedAttribute
? (products || []).filter((product) =>
product.attributes &&
product.attributes[selectedAttribute] === attributeValue.id
)
: [];
return (
<Col key={attributeValue.id} style={{ minWidth: 300 }}>
<Card
title={
<Space>
{attributeValue.image && (
<Image
src={attributeValue.image}
style={{
width: 24,
height: 24,
objectFit: 'cover',
borderRadius: 4,
}}
/>
)}
<span>
{attributeValue.titleCN || attributeValue.title || attributeValue.name}
</span>
</Space>
}
style={{ width: 300 }}
>
{attributeValueProducts.length > 0 ? (
<div style={{ maxHeight: 600, overflowY: 'auto' }}>
{attributeValueProducts.map((product) => (
<Card
key={product.id}
size="small"
hoverable
style={{ marginBottom: 8 }}
>
<div
style={{
height: 100,
overflow: 'hidden',
marginBottom: 8,
}}
>
<Image
src={
product.image ||
'https://via.placeholder.com/100x100?text=No+Image'
}
alt={product.name}
style={{
width: '100%',
height: '100%',
objectFit: 'cover',
}}
/>
</div>
<div>
<Text
type="secondary"
style={{ fontSize: 12, marginBottom: 4 }}
>
{product.sku}
</Text>
<div style={{ marginTop: 4 }}>
<Text ellipsis style={{ width: '100%', display: 'block' }}>
{product.name}
</Text>
</div>
<div style={{ marginTop: 8 }}>
<Text
strong
style={{ fontSize: 14, color: '#ff4d4f' }}
>
¥{product.price || '--'}
</Text>
</div>
</div>
</Card>
))}
</div>
) : (
<div style={{ textAlign: 'center', padding: '32px' }}>
<Text type="secondary"></Text>
</div>
)}
</Card>
</Col>
);
})}
</Row>
</div>
) : (
<div
style={{
textAlign: 'center',
padding: '64px',
background: '#fafafa',
borderRadius: 8,
}}
>
<Text type="secondary"></Text>
</div>
)}
</Content>
</Layout>
</Layout>
</PageContainer>
);
};
export default BrandSpace;

View File

@ -415,11 +415,11 @@ const CsvTool: React.FC = () => {
} }
// 如果type为single且启用了生成bundle SKU则添加quantity // 如果type为single且启用了生成bundle SKU则添加quantity
if ( if (quantity) {
quantity
) {
// 使用quantity的shortName如果没有则使用quantity但匹配 4 个零 // 使用quantity的shortName如果没有则使用quantity但匹配 4 个零
const quantityShortName = attributeMappings.quantities[quantity] || Number(quantity).toString().padStart(4, '0'); const quantityShortName =
attributeMappings.quantities[quantity] ||
Number(quantity).toString().padStart(4, '0');
skuComponents.push(quantityShortName); skuComponents.push(quantityShortName);
} }
// 合并所有组件,使用短横线分隔 // 合并所有组件,使用短横线分隔
@ -463,9 +463,7 @@ const CsvTool: React.FC = () => {
if (size) nameComponents.push(size); if (size) nameComponents.push(size);
// 如果有数量且类型为bundle或者生成bundle的single产品则添加数量 // 如果有数量且类型为bundle或者生成bundle的single产品则添加数量
if ( if (type === 'bundle' && quantity) {
type==='bundle' && quantity
) {
nameComponents.push(String(quantity)); nameComponents.push(String(quantity));
} }
@ -569,7 +567,7 @@ const CsvTool: React.FC = () => {
// Determine which data to use for processing and download // Determine which data to use for processing and download
let finalData = dataWithSku; let finalData = dataWithSku;
console.log('generateBundleSkuForSingle',generateBundleSkuForSingle) console.log('generateBundleSkuForSingle', generateBundleSkuForSingle);
// 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
@ -578,7 +576,9 @@ const CsvTool: React.FC = () => {
); );
// Get quantity values from the config (same source as other attributes like brand) // Get quantity values from the config (same source as other attributes like brand)
const quantityValues = config.quantities.map(quantity=>quantity.name) const quantityValues = config.quantities.map(
(quantity) => quantity.name,
);
// 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) => {
@ -772,7 +772,11 @@ const CsvTool: React.FC = () => {
valuePropName="checked" valuePropName="checked"
initialValue={true} initialValue={true}
> >
<Checkbox onChange={(e)=> setGenerateBundleSkuForSingle(e.target.checked)}> <Checkbox
onChange={(e) =>
setGenerateBundleSkuForSingle(e.target.checked)
}
>
single类型生成bundle SKU single类型生成bundle SKU
</Checkbox> </Checkbox>
</ProForm.Item> </ProForm.Item>

View File

@ -0,0 +1,368 @@
import React, { useEffect, useState, useMemo } from 'react';
import { PageContainer, ProFromSelect } from '@ant-design/pro-components';
import { Card, Collapse, Divider, Image, Select, Space, Typography, message } from 'antd';
import { categorycontrollerGetall } from '@/servers/api/category';
import { productcontrollerGetproductlistgrouped } from '@/servers/api/product';
import { dictcontrollerGetdictitems } from '@/servers/api/dict';
// Define interfaces
interface Category {
id: number;
name: string;
title: string;
attributes: string[]; // List of attribute names for this category
}
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]: number }; // attribute name to attribute value id mapping
price?: number;
}
// Grouped products by attribute value
interface GroupedProducts {
[attributeValueId: string]: Product[];
}
// ProductCard component for displaying single product
const ProductCard: React.FC<{ product: Product }> = ({ product }) => {
return (
<Card hoverable style={{ width: 240 }}>
{/* <div style={{ height: 180, overflow: 'hidden', marginBottom: '12px' }}>
<Image
src={product.image || 'https://via.placeholder.com/240x180?text=No+Image'}
alt={product.name}
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
/>
</div> */}
<div>
<Typography.Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: '4px' }}>
{product.sku}
</Typography.Text>
<Typography.Text ellipsis style={{ width: '100%', display: 'block', marginBottom: '8px' }}>
{product.name}
</Typography.Text>
<Typography.Text strong style={{ fontSize: 16, color: '#ff4d4f', display: 'block' }}>
¥{product.price || '--'}
</Typography.Text>
</div>
</Card>
);
};
// ProductGroup component for displaying grouped products
const ProductGroup: React.FC<{
attributeValueId: string;
groupProducts: Product[];
attributeValue: AttributeValue | undefined;
attributeName: string;
}> = ({ attributeValueId, groupProducts, attributeValue }) => {
// State for collapse control
const [isCollapsed, setIsCollapsed] = useState(false);
// Create collapse panel header
const panelHeader = (
<Space>
{attributeValue?.image && (
<Image
src={attributeValue.image}
style={{ width: 24, height: 24, objectFit: 'cover', borderRadius: 4 }}
/>
)}
<Typography.Title level={5} style={{ margin: 0 }}>
<span>
{attributeValue?.titleCN || attributeValue?.title || attributeValue?.name || attributeValueId||'未知'}
( {groupProducts.length} )
</span>
</Typography.Title>
</Space>
);
return (
<Collapse
activeKey={isCollapsed ? [] : [attributeValueId]}
onChange={(key) => setIsCollapsed(Array.isArray(key) && key.length === 0)}
ghost
bordered={false}
items={[
{
key: attributeValueId,
label: panelHeader,
children: (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '16px', paddingTop: '8px' }}>
{groupProducts.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
),
},
]}
/>
);
};
// Main ProductGroupBy component
const ProductGroupBy: React.FC = () => {
// State management
const [categories, setCategories] = useState<Category[]>([]);
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
// Store selected values for each attribute
const [attributeFilters, setAttributeFilters] = useState<{ [key: string]: number | null }>({});
// Group by attribute
const [groupByAttribute, setGroupByAttribute] = useState<string | null>(null);
// Products
const [products, setProducts] = useState<Product[]>([]);
const [groupedProducts, setGroupedProducts] = useState<GroupedProducts>({});
const [loading, setLoading] = useState(false);
// Extract all unique attributes from categories
const categoryAttributes = useMemo(() => {
if (!selectedCategory) return [];
const categoryItem = categories.find((category: any) => category.name === selectedCategory);
if (!categoryItem) return [];
const attributesList: Attribute[] = categoryItem.attributes.map((attribute: any, index) => ({
...attribute.attributeDict,
id: index + 1,
}));
return attributesList;
}, [selectedCategory]);
// Fetch categories list
const fetchCategories = async () => {
try {
const response = await categorycontrollerGetall();
const rawCategories = Array.isArray(response) ? response : response?.data || [];
setCategories(rawCategories);
// Set default category
if (rawCategories.length > 0) {
const defaultCategory = rawCategories.find((category: any) => category.name === 'nicotine-pouches');
setSelectedCategory(defaultCategory?.name || rawCategories[0].name);
}
} catch (error) {
console.error('Failed to fetch categories:', error);
message.error('获取分类列表失败');
}
};
// Update category attributes when selected category changes
useEffect(() => {
if (!selectedCategory) return;
const category = categories.find(cat => cat.name === selectedCategory);
if (!category) return;
// Get attributes for this category
const attributesForCategory = categoryAttributes.filter(attr =>
attr.name === 'brand' || category.attributes.includes(attr.name)
);
// Reset attribute filters when category changes
const newFilters: { [key: string]: number | null } = {};
attributesForCategory.forEach(attr => {
newFilters[attr.name] = null;
});
setAttributeFilters(newFilters);
// Set default group by attribute
if (attributesForCategory.length > 0) {
setGroupByAttribute(attributesForCategory[0].name);
}
}, [selectedCategory, categories, categoryAttributes]);
// Handle attribute filter change
const handleAttributeFilterChange = (attributeName: string, value: number | null) => {
setAttributeFilters(prev => ({ ...prev, [attributeName]: value }));
};
// Fetch products based on filters
const fetchProducts = async () => {
if (!selectedCategory || !groupByAttribute) return;
setLoading(true);
try {
const params: any = {
category: selectedCategory,
groupBy: groupByAttribute
};
const response = await productcontrollerGetproductlistgrouped(params);
const grouped = response?.data || {};
setGroupedProducts(grouped);
// Flatten grouped products to get all products
const allProducts = Object.values(grouped).flat() as Product[];
setProducts(allProducts);
} catch (error) {
console.error('Failed to fetch grouped products:', error);
message.error('获取分组产品列表失败');
setProducts([]);
setGroupedProducts({});
} finally {
setLoading(false);
}
};
// Initial data fetch
useEffect(() => {
fetchCategories();
}, []);
// Fetch products when filters change
useEffect(() => {
fetchProducts();
}, [selectedCategory, attributeFilters, groupByAttribute]);
// Destructure antd components
const { Title, Text } = Typography;
return (
<PageContainer title="品牌空间">
<div style={{ padding: '16px', background: '#fff' }}>
{/* Filter Section */}
<div style={{ marginBottom: '24px' }}>
<Title level={4} style={{ marginBottom: '16px' }}></Title>
<Space direction="vertical" size="large">
{/* Category Filter */}
<div>
<Text strong></Text>
<Select
placeholder="请选择分类"
style={{ width: 300, marginLeft: '8px' }}
value={selectedCategory}
onChange={setSelectedCategory}
allowClear
showSearch
optionFilterProp="children"
>
{categories.map(category => (
<Option key={category.id} value={category.name}>
{category.title}
</Option>
))}
</Select>
</div>
{/* Attribute Filters */}
{categoryAttributes.length > 0 && (
<div>
<Text strong></Text>
<Space direction="vertical" style={{ marginTop: '8px', width: '100%' }}>
{categoryAttributes.map(attr => (
<div key={attr.id} style={{ display: 'flex', alignItems: 'center' }}>
<Text style={{ width: '100px' }}>{attr.title}</Text>
<ProFromSelect
placeholder={`请选择${attr.title}`}
style={{ width: 300 }}
value={attributeFilters[attr.name] || null}
onChange={value => handleAttributeFilterChange(attr.name, value)}
allowClear
showSearch
optionFilterProp="children"
request={async (params) => {
try {
console.log('params', params,attr);
const response = await dictcontrollerGetdictitems({ dictId: attr.name });
const rawValues = Array.isArray(response) ? response : response?.data?.items || [];
const filteredValues = rawValues.filter((value: any) =>
value.dictId === attr.name || value.dict?.id === attr.name || value.dict?.name === attr.name
);
return {
options: filteredValues.map((value: any) => ({
label: `${value.name}${value.titleCN || value.title}`,
value: value.id
}))
};
} catch (error) {
console.error(`Failed to fetch ${attr.title} values:`, error);
message.error(`获取${attr.title}属性值失败`);
return { options: [] };
}
}}
/>
</div>
))}
</Space>
</div>
)}
{/* Group By Attribute */}
{categoryAttributes.length > 0 && (
<div>
<Text strong></Text>
<Select
placeholder="请选择分组属性"
style={{ width: 300, marginLeft: '8px' }}
value={groupByAttribute}
onChange={setGroupByAttribute}
showSearch
optionFilterProp="children"
>
{categoryAttributes.map(attr => (
<Option key={attr.id} value={attr.name}>
{attr.title}
</Option>
))}
</Select>
</div>
)}
</Space>
</div>
<Divider />
{/* Products Section */}
<div>
<Title level={4} style={{ marginBottom: '16px' }}> ({products.length} )</Title>
{loading ? (
<div style={{ textAlign: 'center', padding: '64px' }}>
<Text>...</Text>
</div>
) : groupByAttribute && Object.keys(groupedProducts).length > 0 ? (
<div style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
{Object.entries(groupedProducts).map(([attrValueId, groupProducts]) => {
return (
<ProductGroup
key={attrValueId}
attributeValueId={attrValueId}
groupProducts={groupProducts}
// attributeValue={}
attributeName={groupByAttribute!}
/>
);
})}
</div>
) : (
<div style={{ textAlign: 'center', padding: '64px', background: '#fafafa', borderRadius: 8 }}>
<Text type="secondary"></Text>
</div>
)}
</div>
</div>
</PageContainer>
);
};
export default ProductGroupBy;

View File

@ -235,10 +235,10 @@ const List: React.FC = () => {
), ),
}, },
{ {
title: "图片", title: '图片',
dataIndex: 'image', dataIndex: 'image',
width: 100, width: 100,
valueType:'image' valueType: 'image',
}, },
{ {
title: '名称', title: '名称',
@ -254,7 +254,6 @@ const List: React.FC = () => {
); );
}, },
}, },
{ {
title: '价格', title: '价格',

View File

@ -58,7 +58,7 @@ export interface SiteItem {
const SiteList: React.FC = () => { const SiteList: React.FC = () => {
const actionRef = useRef<ActionType>(); const actionRef = useRef<ActionType>();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [editing, setEditing] = useState<SiteItem | null>(null); const [editing, setEditing] = useState<SiteItem & { areas: string[] } | null>(null);
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]); const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
const [batchEditOpen, setBatchEditOpen] = useState(false); const [batchEditOpen, setBatchEditOpen] = useState(false);
const [batchEditForm] = Form.useForm(); const [batchEditForm] = Form.useForm();
@ -202,8 +202,12 @@ const SiteList: React.FC = () => {
</a> </a>
), ),
}, },
{ title: 'webhook地址', dataIndex: 'webhookUrl', width: 280, hideInSearch: true }, {
title: 'webhook地址',
dataIndex: 'webhookUrl',
hideInSearch: true,
},
{ {
title: 'SKU 前缀', title: 'SKU 前缀',
dataIndex: 'skuPrefix', dataIndex: 'skuPrefix',
@ -288,7 +292,13 @@ const SiteList: React.FC = () => {
<Button <Button
size="small" size="small"
onClick={() => { onClick={() => {
setEditing(row); function normalEditing(row:SiteItem){
return {
...row,
areas: row.areas?.map(area=>area.code) || [],
}
}
setEditing(normalEditing(row));
setOpen(true); setOpen(true);
}} }}
> >

View File

@ -78,6 +78,7 @@ const EditSiteForm: React.FC<EditSiteFormProps> = ({
}} }}
layout="vertical" layout="vertical"
> >
{JSON.stringify(initialValues)}
<ProFormText <ProFormText
name="name" name="name"
label="名称" label="名称"
@ -169,7 +170,6 @@ const EditSiteForm: React.FC<EditSiteFormProps> = ({
label="区域" label="区域"
mode="multiple" mode="multiple"
placeholder="请选择区域" placeholder="请选择区域"
showSearch showSearch
filterOption={(input, option) => filterOption={(input, option) =>
(option?.label ?? '').toLowerCase().includes(input.toLowerCase()) (option?.label ?? '').toLowerCase().includes(input.toLowerCase())

View File

@ -15,7 +15,7 @@ const ShopLayout: React.FC = () => {
const location = useLocation(); const location = useLocation();
const [editModalOpen, setEditModalOpen] = useState(false); const [editModalOpen, setEditModalOpen] = useState(false);
const [editingSite, setEditingSite] = useState<SiteItem | null>(null); const [editingSite, setEditingSite] = useState<SiteItem & { areas: string[] } | null>(null);
const fetchSites = async () => { const fetchSites = async () => {
try { try {
@ -95,7 +95,7 @@ const ShopLayout: React.FC = () => {
<Row gutter={16} style={{ height: 'calc(100vh - 100px)' }}> <Row gutter={16} style={{ height: 'calc(100vh - 100px)' }}>
<Col span={4} style={{ height: '100%' }}> <Col span={4} style={{ height: '100%' }}>
<Sider <Sider
style={{ background: 'white', height: '100%', overflow: 'hidden' }} style={{ background: 'white', height: '100%', overflow: 'hidden', zIndex: 1 }}
> >
<div style={{ padding: '0 10px 16px' }}> <div style={{ padding: '0 10px 16px' }}>
<div <div
@ -103,6 +103,7 @@ const ShopLayout: React.FC = () => {
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
flexWrap: 'wrap', flexWrap: 'wrap',
gap: '4px'
}} }}
> >
<Select <Select
@ -119,13 +120,18 @@ const ShopLayout: React.FC = () => {
/> />
<Button <Button
icon={<EditOutlined />} icon={<EditOutlined />}
style={{ marginLeft: 8 }}
onClick={() => { onClick={() => {
const currentSite = sites.find( const currentSite = sites.find(
(site) => site.id === Number(siteId), (site) => site.id === Number(siteId),
); );
if (currentSite) { if (currentSite) {
setEditingSite(currentSite); function normalizeEditing(site: SiteItem) {
return {
...site,
areas: site.areas?.map(area => area.code) || [],
}
}
setEditingSite(normalizeEditing(currentSite));
setEditModalOpen(true); setEditModalOpen(true);
} else { } else {
message.warning('请先选择一个店铺'); message.warning('请先选择一个店铺');

View File

@ -228,21 +228,28 @@ const OrdersPage: React.FC = () => {
// 遍历物流信息数组, 显示每个物流的提供商和单号 // 遍历物流信息数组, 显示每个物流的提供商和单号
return ( return (
<div> <div>
{record.fulfillments.map((item: any, index: number) => ( {record.fulfillments.map((item, index: number) => (
<div <div
key={index} key={index}
style={{ display: 'flex', flexDirection: 'column' }} style={{ display: 'flex', flexDirection: 'column' }}
> >
<span> <span>
{item.tracking_provider {item.shipping_provider
? `快递方式: ${item.tracking_provider}` ? `快递方式: ${item.shipping_provider}`
: ''} : ''}
</span> </span>
{item.tracking_number {
? `物流单号: ${item.tracking_number}` item.shipping_method
: ''} ? `发货方式: ${item.shipping_method}`
: ''
}
<span> <span>
{item.date_shipped ? `发货日期: ${item.date_shipped}` : ''} {item.tracking_number
? `物流单号: ${item.tracking_number}`
: ''}
</span>
<span>
{item.date_created ? `发货日期: ${item.date_created}` : ''}
</span> </span>
</div> </div>
))} ))}

View File

@ -2,7 +2,6 @@ import { ORDER_STATUS_ENUM } from '@/constants';
import { AddTag } from '@/pages/Customer/List'; import { AddTag } from '@/pages/Customer/List';
import { customercontrollerDeltag } from '@/servers/api/customer'; import { customercontrollerDeltag } from '@/servers/api/customer';
import { sitecontrollerAll } from '@/servers/api/site'; import { sitecontrollerAll } from '@/servers/api/site';
import * as countries from 'i18n-iso-countries';
import { import {
statisticscontrollerGetorderbydate, statisticscontrollerGetorderbydate,
statisticscontrollerGetorderbyemail, statisticscontrollerGetorderbyemail,
@ -22,6 +21,7 @@ import { Button, Space, Tag } from 'antd';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import weekOfYear from 'dayjs/plugin/weekOfYear'; import weekOfYear from 'dayjs/plugin/weekOfYear';
import ReactECharts from 'echarts-for-react'; import ReactECharts from 'echarts-for-react';
import * as countries from 'i18n-iso-countries';
import { useEffect, useMemo, useRef, useState } from 'react'; import { useEffect, useMemo, useRef, useState } from 'react';
dayjs.extend(weekOfYear); dayjs.extend(weekOfYear);
@ -40,15 +40,15 @@ const highlightText = (text: string, keyword: string) => {
}; };
// 获取所有国家/地区的选项 // 获取所有国家/地区的选项
const getCountryOptions = () => { const getCountryOptions = () => {
// 获取所有国家的 ISO 代码 // 获取所有国家的 ISO 代码
const countryCodes = countries.getAlpha2Codes(); const countryCodes = countries.getAlpha2Codes();
// 将国家代码转换为选项数组 // 将国家代码转换为选项数组
return Object.keys(countryCodes).map((code) => ({ return Object.keys(countryCodes).map((code) => ({
label: countries.getName(code, 'zh') || code, // 使用中文名称, 如果没有则使用代码 label: countries.getName(code, 'zh') || code, // 使用中文名称, 如果没有则使用代码
value: code, value: code,
})); }));
}; };
const ListPage: React.FC = () => { const ListPage: React.FC = () => {
const [xAxis, setXAxis] = useState([]); const [xAxis, setXAxis] = useState([]);
@ -635,18 +635,17 @@ const ListPage: React.FC = () => {
}} }}
/> />
<ProFormSelect <ProFormSelect
name="country" name="country"
label="区域" label="区域"
mode="multiple" mode="multiple"
placeholder="请选择区域" placeholder="请选择区域"
showSearch
showSearch filterOption={(input, option) =>
filterOption={(input, option) => (option?.label ?? '').toLowerCase().includes(input.toLowerCase())
(option?.label ?? '').toLowerCase().includes(input.toLowerCase()) }
} options={getCountryOptions()}
options={getCountryOptions()} />
/>
{/* <ProFormSelect {/* <ProFormSelect
label="类型" label="类型"
name="purchaseType" name="purchaseType"

View File

@ -8,16 +8,16 @@ import {
ActionType, ActionType,
PageContainer, PageContainer,
ProColumns, ProColumns,
ProTable,
ProForm, ProForm,
ProFormSelect, ProFormSelect,
ProTable,
} from '@ant-design/pro-components'; } from '@ant-design/pro-components';
import { Space, Tag } from 'antd'; import { Space, Tag } from 'antd';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import ReactECharts from 'echarts-for-react'; import ReactECharts from 'echarts-for-react';
import { HistoryOrder } from '../Order';
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 { HistoryOrder } from '../Order';
countries.registerLocale(zhCN); countries.registerLocale(zhCN);
const ListPage: React.FC = () => { const ListPage: React.FC = () => {
const [data, setData] = useState({}); const [data, setData] = useState({});
@ -25,12 +25,14 @@ const ListPage: React.FC = () => {
country: ['CA'], country: ['CA'],
}; };
function handleSubmit(values: typeof initialValues) { function handleSubmit(values: typeof initialValues) {
statisticscontrollerGetordersource({params: values}).then(({ data, success }) => { statisticscontrollerGetordersource({ params: values }).then(
if (success) setData(data); ({ data, success }) => {
}); if (success) setData(data);
},
);
} }
useEffect(() => { useEffect(() => {
handleSubmit(initialValues) handleSubmit(initialValues);
}, []); }, []);
const option = useMemo(() => { const option = useMemo(() => {
@ -287,7 +289,6 @@ const ListPage: React.FC = () => {
); );
}, },
}, },
]; ];
return ( return (
@ -297,7 +298,6 @@ const ListPage: React.FC = () => {
layout="inline" layout="inline"
onFinish={handleSubmit} onFinish={handleSubmit}
> >
<ProFormSelect <ProFormSelect
name="country" name="country"
label="区域" label="区域"
@ -343,8 +343,6 @@ const ListPage: React.FC = () => {
); );
}; };
// 获取所有国家/地区的选项 // 获取所有国家/地区的选项
const getCountryOptions = () => { const getCountryOptions = () => {
// 获取所有国家的 ISO 代码 // 获取所有国家的 ISO 代码

View File

@ -1,7 +1,7 @@
// @ts-ignore // @ts-ignore
/* eslint-disable */ /* eslint-disable */
// API 更新时间: // API 更新时间
// API 唯一标识: // API 唯一标识
import * as area from './area'; import * as area from './area';
import * as category from './category'; import * as category from './category';
import * as customer from './customer'; import * as customer from './customer';

View File

@ -125,6 +125,21 @@ export async function productcontrollerBindproductsiteskus(
}); });
} }
/** 此处后端没有提供注释 GET /product/all */
export async function productcontrollerGetallproducts(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.productcontrollerGetallproductsParams,
options?: { [key: string]: any },
) {
return request<API.ProductListRes>('/product/all', {
method: 'GET',
params: {
...params,
},
...(options || {}),
});
}
/** 此处后端没有提供注释 GET /product/attribute */ /** 此处后端没有提供注释 GET /product/attribute */
export async function productcontrollerGetattributelist( export async function productcontrollerGetattributelist(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象) // 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
@ -539,6 +554,21 @@ export async function productcontrollerCompatflavorsall(options?: {
}); });
} }
/** 此处后端没有提供注释 GET /product/grouped */
export async function productcontrollerGetgroupedproducts(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.productcontrollerGetgroupedproductsParams,
options?: { [key: string]: any },
) {
return request<any>('/product/grouped', {
method: 'GET',
params: {
...params,
},
...(options || {}),
});
}
/** 此处后端没有提供注释 POST /product/import */ /** 此处后端没有提供注释 POST /product/import */
export async function productcontrollerImportproductscsv( export async function productcontrollerImportproductscsv(
body: {}, body: {},
@ -593,6 +623,21 @@ export async function productcontrollerGetproductlist(
}); });
} }
/** 此处后端没有提供注释 GET /product/list/grouped */
export async function productcontrollerGetproductlistgrouped(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.productcontrollerGetproductlistgroupedParams,
options?: { [key: string]: any },
) {
return request<Record<string, any>>('/product/list/grouped', {
method: 'GET',
params: {
...params,
},
...(options || {}),
});
}
/** 此处后端没有提供注释 GET /product/search */ /** 此处后端没有提供注释 GET /product/search */
export async function productcontrollerSearchproducts( export async function productcontrollerSearchproducts(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象) // 叠加生成的Param类型 (非body参数swagger默认没有生成对象)

View File

@ -78,7 +78,7 @@ export async function statisticscontrollerGetorderbyemail(
} }
/** 此处后端没有提供注释 GET /statistics/orderSource */ /** 此处后端没有提供注释 GET /statistics/orderSource */
export async function statisticscontrollerGetordersorce(options?: { export async function statisticscontrollerGetordersource(options?: {
[key: string]: any; [key: string]: any;
}) { }) {
return request<any>('/statistics/orderSource', { return request<any>('/statistics/orderSource', {

View File

@ -145,6 +145,8 @@ declare namespace API {
price?: number; price?: number;
/** 促销价格 */ /** 促销价格 */
promotionPrice?: number; promotionPrice?: number;
/** 产品图片URL */
image?: string;
/** 属性列表 */ /** 属性列表 */
attributes?: any[]; attributes?: any[];
/** 商品类型 */ /** 商品类型 */
@ -266,6 +268,8 @@ declare namespace API {
sku?: string; sku?: string;
/** 分类ID (DictItem ID) */ /** 分类ID (DictItem ID) */
categoryId?: number; categoryId?: number;
/** 分类名称 */
categoryName?: string;
/** 站点 SKU 列表 */ /** 站点 SKU 列表 */
siteSkus?: any[]; siteSkus?: any[];
/** 属性列表 */ /** 属性列表 */
@ -274,6 +278,8 @@ declare namespace API {
price?: number; price?: number;
/** 促销价格 */ /** 促销价格 */
promotionPrice?: number; promotionPrice?: number;
/** 产品图片URL */
image?: string;
/** 商品类型 */ /** 商品类型 */
type?: 'single' | 'bundle'; type?: 'single' | 'bundle';
/** 产品组成 */ /** 产品组成 */
@ -450,6 +456,8 @@ declare namespace API {
}; };
type FulfillmentDTO = { type FulfillmentDTO = {
/** 物流id */
tracking_id?: string;
/** 物流单号 */ /** 物流单号 */
tracking_number?: string; tracking_number?: string;
/** 物流公司 */ /** 物流公司 */
@ -645,6 +653,10 @@ declare namespace API {
createdAt: string; createdAt: string;
/** 更新时间 */ /** 更新时间 */
updatedAt: string; updatedAt: string;
/** 订单项列表 */
orderItems?: any;
/** 销售项列表 */
orderSales?: any;
}; };
type OrderAddress = { type OrderAddress = {
@ -695,28 +707,48 @@ declare namespace API {
}; };
type ordercontrollerGetorderitemsParams = { type ordercontrollerGetorderitemsParams = {
/** 是否为原产品还是库存产品 */
isSource?: boolean; isSource?: boolean;
exceptPackage?: boolean;
/** 页码 */ /** 页码 */
current?: number; current?: number;
/** 每页大小 */ /** 每页大小 */
pageSize?: number; pageSize?: number;
/** 排序对象,格式如 { productName: "asc", sku: "desc" } */
orderBy?: any;
/** 是否排除套餐 */
exceptPackage?: boolean;
/** 站点ID */
siteId?: number; siteId?: number;
/** 名称 */
name?: string; name?: string;
/** SKU */
sku?: string;
/** 开始日期 */
startDate?: string; startDate?: string;
/** 结束日期 */
endDate?: string; endDate?: string;
}; };
type ordercontrollerGetordersalesParams = { type ordercontrollerGetordersalesParams = {
/** 是否为原产品还是库存产品 */
isSource?: boolean; isSource?: boolean;
exceptPackage?: boolean;
/** 页码 */ /** 页码 */
current?: number; current?: number;
/** 每页大小 */ /** 每页大小 */
pageSize?: number; pageSize?: number;
/** 排序对象,格式如 { productName: "asc", sku: "desc" } */
orderBy?: any;
/** 是否排除套餐 */
exceptPackage?: boolean;
/** 站点ID */
siteId?: number; siteId?: number;
/** 名称 */
name?: string; name?: string;
/** SKU */
sku?: string;
/** 开始日期 */
startDate?: string; startDate?: string;
/** 结束日期 */
endDate?: string; endDate?: string;
}; };
@ -820,6 +852,10 @@ declare namespace API {
createdAt: string; createdAt: string;
/** 更新时间 */ /** 更新时间 */
updatedAt: string; updatedAt: string;
/** 订单项列表 */
orderItems?: any;
/** 销售项列表 */
orderSales?: any;
items?: OrderItem[]; items?: OrderItem[];
sales?: OrderSale[]; sales?: OrderSale[];
refundItems?: OrderRefundItem[]; refundItems?: OrderRefundItem[];
@ -929,16 +965,26 @@ declare namespace API {
orderId?: number; orderId?: number;
siteId?: number; siteId?: number;
externalOrderItemId?: string; externalOrderItemId?: string;
parentProductId?: number;
productId?: number; productId?: number;
name?: string; name?: string;
/** sku */ /** sku */
sku?: string; sku?: string;
quantity?: number; quantity?: number;
isPackage?: boolean; isPackage?: boolean;
isYoone?: boolean; /** 商品品类 */
isZex?: boolean; category?: string;
size?: number; /** 品牌 */
isYooneNew?: boolean; brand?: string;
/** 口味 */
flavor?: string;
/** 湿度 */
humidity?: string;
/** 尺寸 */
size?: string;
strength?: string;
/** 版本 */
version?: string;
/** 创建时间 */ /** 创建时间 */
createdAt?: string; createdAt?: string;
/** 更新时间 */ /** 更新时间 */
@ -950,16 +996,26 @@ declare namespace API {
orderId?: number; orderId?: number;
siteId?: number; siteId?: number;
externalOrderItemId?: string; externalOrderItemId?: string;
parentProductId?: number;
productId?: number; productId?: number;
name?: string; name?: string;
/** sku */ /** sku */
sku?: string; sku?: string;
quantity?: number; quantity?: number;
isPackage?: boolean; isPackage?: boolean;
isYoone?: boolean; /** 商品品类 */
isZex?: boolean; category?: string;
size?: number; /** 品牌 */
isYooneNew?: boolean; brand?: string;
/** 口味 */
flavor?: string;
/** 湿度 */
humidity?: string;
/** 尺寸 */
size?: string;
strength?: string;
/** 版本 */
version?: string;
/** 创建时间 */ /** 创建时间 */
createdAt?: string; createdAt?: string;
/** 更新时间 */ /** 更新时间 */
@ -994,6 +1050,7 @@ declare namespace API {
endDate?: string; endDate?: string;
keyword?: string; keyword?: string;
siteId?: number; siteId?: number;
country?: any;
purchaseType?: 'all' | 'first_purchase' | 'repeat_purchase'; purchaseType?: 'all' | 'first_purchase' | 'repeat_purchase';
orderType?: 'all' | 'cpc' | 'non_cpc'; orderType?: 'all' | 'cpc' | 'non_cpc';
brand?: 'all' | 'zyn' | 'yoone' | 'zolt'; brand?: 'all' | 'zyn' | 'yoone' | 'zolt';
@ -1034,10 +1091,14 @@ declare namespace API {
shortDescription?: string; shortDescription?: string;
/** 产品描述 */ /** 产品描述 */
description?: string; description?: string;
/** 产品图片URL */
image?: string;
/** 价格 */ /** 价格 */
price?: number; price?: number;
/** 促销价格 */ /** 促销价格 */
promotionPrice?: number; promotionPrice?: number;
/** 分类 ID */
categoryId?: number;
/** 库存组成 */ /** 库存组成 */
components?: ProductStockComponent[]; components?: ProductStockComponent[];
/** 站点 SKU 列表 */ /** 站点 SKU 列表 */
@ -1134,6 +1195,10 @@ declare namespace API {
id: number; id: number;
}; };
type productcontrollerGetallproductsParams = {
brand?: string;
};
type productcontrollerGetattributeallParams = { type productcontrollerGetattributeallParams = {
dictName?: string; dictName?: string;
}; };
@ -1149,6 +1214,11 @@ declare namespace API {
id: number; id: number;
}; };
type productcontrollerGetgroupedproductsParams = {
attribute?: string;
brand?: string;
};
type productcontrollerGetproductbyidParams = { type productcontrollerGetproductbyidParams = {
id: number; id: number;
}; };
@ -1161,6 +1231,25 @@ declare namespace API {
id: number; id: number;
}; };
type productcontrollerGetproductlistgroupedParams = {
/** 页码 */
page?: number;
/** 每页数量 */
per_page?: number;
/** 查询时间范围开始 */
after?: string;
/** 查询时间范围结束 */
before?: string;
/** 搜索关键词 */
search?: string;
/** 过滤条件对象 */
where?: any;
/** 排序对象,例如 { "sku": "desc" } */
orderBy?: any;
/** 分组字段,例如 "categoryId" */
groupBy?: string;
};
type productcontrollerGetproductlistParams = { type productcontrollerGetproductlistParams = {
/** 页码 */ /** 页码 */
page?: number; page?: number;
@ -1176,6 +1265,8 @@ declare namespace API {
where?: any; where?: any;
/** 排序对象,例如 { "sku": "desc" } */ /** 排序对象,例如 { "sku": "desc" } */
orderBy?: any; orderBy?: any;
/** 分组字段,例如 "categoryId" */
groupBy?: string;
}; };
type productcontrollerGetproductsiteskusParams = { type productcontrollerGetproductsiteskusParams = {
@ -1371,15 +1462,25 @@ declare namespace API {
}; };
type QueryOrderSalesDTO = { type QueryOrderSalesDTO = {
/** 是否为原产品还是库存产品 */
isSource?: boolean; isSource?: boolean;
exceptPackage?: boolean;
/** 页码 */ /** 页码 */
current?: number; current?: number;
/** 每页大小 */ /** 每页大小 */
pageSize?: number; pageSize?: number;
/** 排序对象,格式如 { productName: "asc", sku: "desc" } */
orderBy?: any;
/** 是否排除套餐 */
exceptPackage?: boolean;
/** 站点ID */
siteId?: number; siteId?: number;
/** 名称 */
name?: string; name?: string;
/** SKU */
sku?: string;
/** 开始日期 */
startDate?: string; startDate?: string;
/** 结束日期 */
endDate?: string; endDate?: string;
}; };
@ -1708,6 +1809,8 @@ declare namespace API {
where?: any; where?: any;
/** 排序对象,例如 { "sku": "desc" } */ /** 排序对象,例如 { "sku": "desc" } */
orderBy?: any; orderBy?: any;
/** 分组字段,例如 "categoryId" */
groupBy?: string;
siteId: number; siteId: number;
}; };
@ -1726,6 +1829,8 @@ declare namespace API {
where?: any; where?: any;
/** 排序对象,例如 { "sku": "desc" } */ /** 排序对象,例如 { "sku": "desc" } */
orderBy?: any; orderBy?: any;
/** 分组字段,例如 "categoryId" */
groupBy?: string;
siteId: number; siteId: number;
}; };
@ -1744,6 +1849,8 @@ declare namespace API {
where?: any; where?: any;
/** 排序对象,例如 { "sku": "desc" } */ /** 排序对象,例如 { "sku": "desc" } */
orderBy?: any; orderBy?: any;
/** 分组字段,例如 "categoryId" */
groupBy?: string;
siteId: number; siteId: number;
}; };
@ -1762,6 +1869,8 @@ declare namespace API {
where?: any; where?: any;
/** 排序对象,例如 { "sku": "desc" } */ /** 排序对象,例如 { "sku": "desc" } */
orderBy?: any; orderBy?: any;
/** 分组字段,例如 "categoryId" */
groupBy?: string;
siteId: number; siteId: number;
}; };
@ -1780,6 +1889,8 @@ declare namespace API {
where?: any; where?: any;
/** 排序对象,例如 { "sku": "desc" } */ /** 排序对象,例如 { "sku": "desc" } */
orderBy?: any; orderBy?: any;
/** 分组字段,例如 "categoryId" */
groupBy?: string;
siteId: number; siteId: number;
}; };
@ -1798,6 +1909,8 @@ declare namespace API {
where?: any; where?: any;
/** 排序对象,例如 { "sku": "desc" } */ /** 排序对象,例如 { "sku": "desc" } */
orderBy?: any; orderBy?: any;
/** 分组字段,例如 "categoryId" */
groupBy?: string;
siteId: number; siteId: number;
}; };
@ -1816,6 +1929,8 @@ declare namespace API {
where?: any; where?: any;
/** 排序对象,例如 { "sku": "desc" } */ /** 排序对象,例如 { "sku": "desc" } */
orderBy?: any; orderBy?: any;
/** 分组字段,例如 "categoryId" */
groupBy?: string;
customerId: number; customerId: number;
siteId: number; siteId: number;
}; };
@ -1840,6 +1955,8 @@ declare namespace API {
where?: any; where?: any;
/** 排序对象,例如 { "sku": "desc" } */ /** 排序对象,例如 { "sku": "desc" } */
orderBy?: any; orderBy?: any;
/** 分组字段,例如 "categoryId" */
groupBy?: string;
siteId: number; siteId: number;
}; };
@ -1862,6 +1979,8 @@ declare namespace API {
where?: any; where?: any;
/** 排序对象,例如 { "sku": "desc" } */ /** 排序对象,例如 { "sku": "desc" } */
orderBy?: any; orderBy?: any;
/** 分组字段,例如 "categoryId" */
groupBy?: string;
siteId: number; siteId: number;
}; };
@ -1895,6 +2014,8 @@ declare namespace API {
where?: any; where?: any;
/** 排序对象,例如 { "sku": "desc" } */ /** 排序对象,例如 { "sku": "desc" } */
orderBy?: any; orderBy?: any;
/** 分组字段,例如 "categoryId" */
groupBy?: string;
siteId: number; siteId: number;
}; };
@ -1918,6 +2039,8 @@ declare namespace API {
where?: any; where?: any;
/** 排序对象,例如 { "sku": "desc" } */ /** 排序对象,例如 { "sku": "desc" } */
orderBy?: any; orderBy?: any;
/** 分组字段,例如 "categoryId" */
groupBy?: string;
siteId: number; siteId: number;
}; };
@ -1936,6 +2059,8 @@ declare namespace API {
where?: any; where?: any;
/** 排序对象,例如 { "sku": "desc" } */ /** 排序对象,例如 { "sku": "desc" } */
orderBy?: any; orderBy?: any;
/** 分组字段,例如 "categoryId" */
groupBy?: string;
siteId: number; siteId: number;
}; };
@ -1954,6 +2079,8 @@ declare namespace API {
where?: any; where?: any;
/** 排序对象,例如 { "sku": "desc" } */ /** 排序对象,例如 { "sku": "desc" } */
orderBy?: any; orderBy?: any;
/** 分组字段,例如 "categoryId" */
groupBy?: string;
siteId: number; siteId: number;
}; };
@ -1977,6 +2104,8 @@ declare namespace API {
where?: any; where?: any;
/** 排序对象,例如 { "sku": "desc" } */ /** 排序对象,例如 { "sku": "desc" } */
orderBy?: any; orderBy?: any;
/** 分组字段,例如 "categoryId" */
groupBy?: string;
siteId: number; siteId: number;
}; };
@ -2928,6 +3057,8 @@ declare namespace API {
where?: any; where?: any;
/** 排序对象,例如 { "sku": "desc" } */ /** 排序对象,例如 { "sku": "desc" } */
orderBy?: any; orderBy?: any;
/** 分组字段,例如 "categoryId" */
groupBy?: string;
}; };
type UnifiedShippingLineDTO = { type UnifiedShippingLineDTO = {
@ -3088,12 +3219,16 @@ declare namespace API {
sku?: string; sku?: string;
/** 分类ID (DictItem ID) */ /** 分类ID (DictItem ID) */
categoryId?: number; categoryId?: number;
/** 分类名称 */
categoryName?: string;
/** 站点 SKU 列表 */ /** 站点 SKU 列表 */
siteSkus?: any[]; siteSkus?: any[];
/** 价格 */ /** 价格 */
price?: number; price?: number;
/** 促销价格 */ /** 促销价格 */
promotionPrice?: number; promotionPrice?: number;
/** 产品图片URL */
image?: string;
/** 属性列表 */ /** 属性列表 */
attributes?: any[]; attributes?: any[];
/** 商品类型 */ /** 商品类型 */
@ -3144,6 +3279,8 @@ declare namespace API {
stockPointIds?: any; stockPointIds?: any;
/** 站点网站URL */ /** 站点网站URL */
websiteUrl?: string; websiteUrl?: string;
/** Webhook URL */
webhookUrl?: string;
}; };
type UpdateStockDTO = { type UpdateStockDTO = {