forked from yoone/WEB
1
0
Fork 0

Compare commits

...

10 Commits

Author SHA1 Message Date
zhuotianyuan 67aa625785 feat(api类型): 添加shipmentPlatform和address_id字段到DTO类型 2026-01-22 17:42:05 +08:00
zhuotianyuan 1d9838f72e feat(订单): 添加发货平台选择功能并优化表单验证
- 在发货表单中新增发货平台选择器
- 将公司名称字段改为非必填
- 添加控制台日志用于调试
- 优化地址选择器的数据处理逻辑
2026-01-22 17:35:53 +08:00
tikkhun 3690fd00f7 feat: 添加客户统计页面并更新物流信息显示
- 新增客户统计页面组件
- 在物流DTO中添加tracking_id字段注释
- 移除不再使用的cubejs-client依赖
- 修复店铺布局中的加载状态变量名
- 优化订单页面物流信息显示逻辑,支持更多字段展示
2026-01-22 15:12:48 +08:00
tikkhun 1fbfd95003 feat(客户分析): 添加数据分析列表页面并迁移客户统计功能
- 新增数据分析列表页面组件
- 将原客户统计功能迁移至新页面
- 添加cubejs客户端依赖用于数据分析
- 调整路由配置指向新页面
- 修复店铺布局z-index问题
2026-01-21 14:37:37 +08:00
tikkhun 2471eb5299 fix(站点编辑): 修复编辑表单中区域数据格式问题
确保编辑表单中的区域数据格式一致,将areas字段转换为code数组
在多个组件中添加数据格式转换逻辑
添加调试信息输出以方便问题排查
2026-01-20 18:00:13 +08:00
tikkhun d78b587035 refactor: 清理代码格式并移除未使用的BrandSpace组件
style: 统一字符串引号和代码缩进格式
fix: 修复订单同步日期范围参数格式
feat: 添加产品分组列表API接口
2026-01-20 17:20:41 +08:00
tikkhun 2ee8964bce feat(产品): 添加产品分组功能页面
实现产品按属性分组展示功能,支持按分类和属性筛选
2026-01-20 17:19:02 +08:00
tikkhun d2a84a9a4a feat(api) 运行 openapit2ts 2026-01-17 14:28:21 +08:00
tikkhun 04b9fed6f7 feat(Product/List): 添加图片列到产品列表
feat(Product/BrandSpace): 重构品牌空间页面布局和逻辑

refactor(.umirc): 调整产品路由顺序

fix(Product/CsvTool): 修复复选框事件处理并移除调试日志
2026-01-16 15:58:56 +08:00
tikkhun 49e3049681 refactor(DictItemImportButton): 移除冗余的成功检查逻辑
该逻辑已在API层处理,无需在前端重复检查
2026-01-16 15:58:55 +08:00
20 changed files with 1048 additions and 873 deletions

View File

@ -134,9 +134,14 @@ export default defineConfig({
},
{
name: '数据分析列表',
path: '/customer/statistic',
component: './Customer/Statistic',
path: '/customer/statistic/list',
component: './Customer/StatisticList',
},
// {
// name: '客户统计',
// path: '/customer/statistic/home',
// component: './Customer/Statistic',
// }
],
},
{
@ -149,11 +154,6 @@ export default defineConfig({
path: '/product/list',
component: './Product/List',
},
{
name: '产品属性排列',
path: '/product/permutation',
component: './Product/Permutation',
},
{
name: '产品分类',
path: '/product/category',
@ -164,10 +164,15 @@ export default defineConfig({
path: '/product/attribute',
component: './Product/Attribute',
},
{
name: '产品属性排列',
path: '/product/permutation',
component: './Product/Permutation',
},
{
name: '产品品牌空间',
path: '/product/brandspace',
component: './Product/BrandSpace',
path: '/product/groupBy',
component: './Product/GroupBy',
},
// sync
{

View File

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

View File

@ -1,289 +1,7 @@
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>
);
},
},
];
export default function Statistic() {
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>
<div>
<h1></h1>
</div>
);
};
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

@ -52,12 +52,6 @@ const DictItemImportButton: React.FC<DictItemImportButtonProps> = ({
});
}
// 检查返回结果是否包含 success 字段
if (result && result.success !== undefined && !result.success) {
throw new Error(result.message || '导入失败');
}
onSuccess?.(result);
// 显示导入结果详情
showImportResult(result);

View File

@ -77,6 +77,7 @@ import {
} from 'antd';
import React, { useMemo, useRef, useState } from 'react';
import RelatedOrders from '../../Subscription/Orders/RelatedOrders';
import dayjs from 'dayjs';
const ListPage: React.FC = () => {
const actionRef = useRef<ActionType>();
@ -502,7 +503,10 @@ const ListPage: React.FC = () => {
success,
message: errMsg,
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) {
throw new Error(errMsg);
}
@ -990,7 +994,7 @@ const Detail: React.FC<{
<ul>
{record?.items?.map((item: any) => (
<li key={item.id}>
{item.name}:{item.quantity}
{item.name}({item.sku}):{item.quantity}
</li>
))}
</ul>
@ -1015,7 +1019,7 @@ const Detail: React.FC<{
<ul>
{record?.sales?.map((item: any) => (
<li key={item.id}>
{item.name}:{item.quantity}
{item.name}({item.sku}):{item.quantity}
</li>
))}
</ul>
@ -1264,7 +1268,10 @@ const Shipping: React.FC<{
const [rates, setRates] = useState<API.RateDTO[]>([]);
const [ratesLoading, setRatesLoading] = useState(false);
const { message } = App.useApp();
const [shipmentPlatforms, setShipmentPlatforms] = useState([
{ label: 'uniuni', value: 'uniuni' },
{ label: 'tms.freightwaves', value: 'freightwaves' },
]);
return (
<ModalForm
formRef={formRef}
@ -1293,6 +1300,7 @@ const Shipping: React.FC<{
await ordercontrollerGetorderdetail({
orderId: id,
});
console.log('success data',success,data)
if (!success || !data) return {};
data.sales = data.sales?.reduce(
(acc: API.OrderSale[], cur: API.OrderSale) => {
@ -1315,7 +1323,8 @@ const Shipping: React.FC<{
if (reShipping) data.sales = [{}];
let shipmentInfo = localStorage.getItem('shipmentInfo');
if (shipmentInfo) shipmentInfo = JSON.parse(shipmentInfo);
return {
const a = {
shipmentPlatform: 'uniuni',
...data,
// payment_method_id: shipmentInfo?.payment_method_id,
stockPointId: shipmentInfo?.stockPointId,
@ -1375,6 +1384,8 @@ const Shipping: React.FC<{
},
},
};
console.log('data',a)
return a
}}
onFinish={async ({
customer_note,
@ -1438,7 +1449,18 @@ const Shipping: React.FC<{
}
}}
>
<ProFormText label="订单号" readonly name={'externalOrderId'} />
<Row gutter={16}>
<Col span={8}>
<ProFormSelect
name="shipmentPlatform"
label="发货平台"
options={shipmentPlatforms}
placeholder="请选择发货平台"
rules={[{ required: true, message: '请选择一个选项' }]}
/>
</Col>
</Row>
<ProFormText label="订单号" readonly name='externalOrderId' />
<ProFormText label="客户备注" readonly name="customer_note" />
<ProFormList
label="后台备注"
@ -1507,16 +1529,16 @@ const Shipping: React.FC<{
<ProFormList
label="发货产品"
name="sales"
// rules={[
// {
// required: true,
// message: '至少需要一个商品',
// validator: (_, value) =>
// value && value.length > 0
//</Col> ? Promise.resolve()
// : Promise.reject('至少需要一个商品'),
// },
// ]}
// rules={[
// {
// required: true,
// message: '至少需要一个商品',
// validator: (_, value) =>
// value && value.length > 0
//</Col> ? Promise.resolve()
// : Promise.reject('至少需要一个商品'),
// },
// ]}
>
<ProForm.Group>
<ProFormSelect
@ -1570,16 +1592,20 @@ const Shipping: React.FC<{
title="发货信息"
extra={
<AddressPicker
onChange={({
address,
phone_number,
phone_number_extension,
stockPointId,
}) => {
onChange={(row) => {
console.log(row);
const {
address,
phone_number,
phone_number_extension,
stockPointId,
} = row;
formRef?.current?.setFieldsValue({
stockPointId,
// address_id: row.id,
details: {
origin: {
address,
phone_number: {
phone: phone_number,
@ -1592,6 +1618,11 @@ const Shipping: React.FC<{
/>
}
>
{/* <ProFormText
label="address_id"
name={'address_id'}
rules={[{ required: true, message: '请输入ID' }]}
/> */}
<ProFormSelect
name="stockPointId"
width="md"
@ -1684,7 +1715,7 @@ const Shipping: React.FC<{
<ProFormText
label="公司名称"
name={['details', 'destination', 'name']}
rules={[{ required: true, message: '请输入公司名称' }]}
rules={[{ message: '请输入公司名称' }]}
/>
<ProFormItem
name={['details', 'destination', 'address', 'country']}
@ -2014,6 +2045,7 @@ const Shipping: React.FC<{
details.origin.phone_number.number =
details.origin.phone_number.phone;
const res = await logisticscontrollerGetshipmentfee({
shipmentPlatform: data.shipmentPlatform,
stockPointId: data.stockPointId,
sender: details.origin.contact_name,
@ -2340,7 +2372,7 @@ const CreateOrder: React.FC<{
<ProFormText
label="公司名称"
name={['billing', 'company']}
rules={[{ required: true, message: '请输入公司名称' }]}
rules={[{ message: '请输入公司名称' }]}
/>
<ProFormItem
name={['billing', 'country']}
@ -2426,6 +2458,11 @@ const AddressPicker: React.FC<{
value: item.id,
}));
},
},
{
title: 'id',
dataIndex: ['id'],
hideInSearch: true,
},
{
title: '地区',

View File

@ -1,452 +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);
// 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

@ -415,12 +415,11 @@ const CsvTool: React.FC = () => {
}
// 如果type为single且启用了生成bundle SKU则添加quantity
if (
quantity
) {
console.log(quantity, attributeMappings.quantities[quantity])
if (quantity) {
// 使用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);
}
// 合并所有组件,使用短横线分隔
@ -464,9 +463,7 @@ const CsvTool: React.FC = () => {
if (size) nameComponents.push(size);
// 如果有数量且类型为bundle或者生成bundle的single产品则添加数量
if (
type==='bundle' && quantity
) {
if (type === 'bundle' && quantity) {
nameComponents.push(String(quantity));
}
@ -570,7 +567,7 @@ const CsvTool: React.FC = () => {
// Determine which data to use for processing and download
let finalData = dataWithSku;
console.log('generateBundleSkuForSingle', generateBundleSkuForSingle);
// If generateBundleSkuForSingle is enabled, generate bundle products for single products
if (generateBundleSkuForSingle) {
// Filter out single records
@ -579,7 +576,9 @@ const CsvTool: React.FC = () => {
);
// 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
const generatedBundleRecords = singleRecords.flatMap((singleRecord) => {
@ -773,7 +772,11 @@ const CsvTool: React.FC = () => {
valuePropName="checked"
initialValue={true}
>
<Checkbox onChange={setGenerateBundleSkuForSingle}>
<Checkbox
onChange={(e) =>
setGenerateBundleSkuForSingle(e.target.checked)
}
>
single类型生成bundle SKU
</Checkbox>
</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

@ -234,6 +234,12 @@ const List: React.FC = () => {
</>
),
},
{
title: '图片',
dataIndex: 'image',
width: 100,
valueType: 'image',
},
{
title: '名称',
dataIndex: 'name',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
// @ts-ignore
/* eslint-disable */
// API 更新时间:
// API 唯一标识:
// API 更新时间
// API 唯一标识
import * as area from './area';
import * as category from './category';
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 */
export async function productcontrollerGetattributelist(
// 叠加生成的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 */
export async function productcontrollerImportproductscsv(
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 */
export async function productcontrollerSearchproducts(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)

View File

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

View File

@ -145,6 +145,8 @@ declare namespace API {
price?: number;
/** 促销价格 */
promotionPrice?: number;
/** 产品图片URL */
image?: string;
/** 属性列表 */
attributes?: any[];
/** 商品类型 */
@ -266,6 +268,8 @@ declare namespace API {
sku?: string;
/** 分类ID (DictItem ID) */
categoryId?: number;
/** 分类名称 */
categoryName?: string;
/** 站点 SKU 列表 */
siteSkus?: any[];
/** 属性列表 */
@ -274,6 +278,8 @@ declare namespace API {
price?: number;
/** 促销价格 */
promotionPrice?: number;
/** 产品图片URL */
image?: string;
/** 商品类型 */
type?: 'single' | 'bundle';
/** 产品组成 */
@ -450,6 +456,8 @@ declare namespace API {
};
type FulfillmentDTO = {
/** 物流id */
tracking_id?: string;
/** 物流单号 */
tracking_number?: string;
/** 物流公司 */
@ -645,6 +653,10 @@ declare namespace API {
createdAt: string;
/** 更新时间 */
updatedAt: string;
/** 订单项列表 */
orderItems?: any;
/** 销售项列表 */
orderSales?: any;
};
type OrderAddress = {
@ -695,28 +707,48 @@ declare namespace API {
};
type ordercontrollerGetorderitemsParams = {
/** 是否为原产品还是库存产品 */
isSource?: boolean;
exceptPackage?: boolean;
/** 页码 */
current?: number;
/** 每页大小 */
pageSize?: number;
/** 排序对象,格式如 { productName: "asc", sku: "desc" } */
orderBy?: any;
/** 是否排除套餐 */
exceptPackage?: boolean;
/** 站点ID */
siteId?: number;
/** 名称 */
name?: string;
/** SKU */
sku?: string;
/** 开始日期 */
startDate?: string;
/** 结束日期 */
endDate?: string;
};
type ordercontrollerGetordersalesParams = {
/** 是否为原产品还是库存产品 */
isSource?: boolean;
exceptPackage?: boolean;
/** 页码 */
current?: number;
/** 每页大小 */
pageSize?: number;
/** 排序对象,格式如 { productName: "asc", sku: "desc" } */
orderBy?: any;
/** 是否排除套餐 */
exceptPackage?: boolean;
/** 站点ID */
siteId?: number;
/** 名称 */
name?: string;
/** SKU */
sku?: string;
/** 开始日期 */
startDate?: string;
/** 结束日期 */
endDate?: string;
};
@ -820,6 +852,10 @@ declare namespace API {
createdAt: string;
/** 更新时间 */
updatedAt: string;
/** 订单项列表 */
orderItems?: any;
/** 销售项列表 */
orderSales?: any;
items?: OrderItem[];
sales?: OrderSale[];
refundItems?: OrderRefundItem[];
@ -929,16 +965,26 @@ declare namespace API {
orderId?: number;
siteId?: number;
externalOrderItemId?: string;
parentProductId?: number;
productId?: number;
name?: string;
/** sku */
sku?: string;
quantity?: number;
isPackage?: boolean;
isYoone?: boolean;
isZex?: boolean;
size?: number;
isYooneNew?: boolean;
/** 商品品类 */
category?: string;
/** 品牌 */
brand?: string;
/** 口味 */
flavor?: string;
/** 湿度 */
humidity?: string;
/** 尺寸 */
size?: string;
strength?: string;
/** 版本 */
version?: string;
/** 创建时间 */
createdAt?: string;
/** 更新时间 */
@ -950,16 +996,26 @@ declare namespace API {
orderId?: number;
siteId?: number;
externalOrderItemId?: string;
parentProductId?: number;
productId?: number;
name?: string;
/** sku */
sku?: string;
quantity?: number;
isPackage?: boolean;
isYoone?: boolean;
isZex?: boolean;
size?: number;
isYooneNew?: boolean;
/** 商品品类 */
category?: string;
/** 品牌 */
brand?: string;
/** 口味 */
flavor?: string;
/** 湿度 */
humidity?: string;
/** 尺寸 */
size?: string;
strength?: string;
/** 版本 */
version?: string;
/** 创建时间 */
createdAt?: string;
/** 更新时间 */
@ -994,6 +1050,7 @@ declare namespace API {
endDate?: string;
keyword?: string;
siteId?: number;
country?: any;
purchaseType?: 'all' | 'first_purchase' | 'repeat_purchase';
orderType?: 'all' | 'cpc' | 'non_cpc';
brand?: 'all' | 'zyn' | 'yoone' | 'zolt';
@ -1034,10 +1091,14 @@ declare namespace API {
shortDescription?: string;
/** 产品描述 */
description?: string;
/** 产品图片URL */
image?: string;
/** 价格 */
price?: number;
/** 促销价格 */
promotionPrice?: number;
/** 分类 ID */
categoryId?: number;
/** 库存组成 */
components?: ProductStockComponent[];
/** 站点 SKU 列表 */
@ -1134,6 +1195,10 @@ declare namespace API {
id: number;
};
type productcontrollerGetallproductsParams = {
brand?: string;
};
type productcontrollerGetattributeallParams = {
dictName?: string;
};
@ -1149,6 +1214,11 @@ declare namespace API {
id: number;
};
type productcontrollerGetgroupedproductsParams = {
attribute?: string;
brand?: string;
};
type productcontrollerGetproductbyidParams = {
id: number;
};
@ -1161,6 +1231,25 @@ declare namespace API {
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 = {
/** 页码 */
page?: number;
@ -1176,6 +1265,8 @@ declare namespace API {
where?: any;
/** 排序对象,例如 { "sku": "desc" } */
orderBy?: any;
/** 分组字段,例如 "categoryId" */
groupBy?: string;
};
type productcontrollerGetproductsiteskusParams = {
@ -1371,15 +1462,25 @@ declare namespace API {
};
type QueryOrderSalesDTO = {
/** 是否为原产品还是库存产品 */
isSource?: boolean;
exceptPackage?: boolean;
/** 页码 */
current?: number;
/** 每页大小 */
pageSize?: number;
/** 排序对象,格式如 { productName: "asc", sku: "desc" } */
orderBy?: any;
/** 是否排除套餐 */
exceptPackage?: boolean;
/** 站点ID */
siteId?: number;
/** 名称 */
name?: string;
/** SKU */
sku?: string;
/** 开始日期 */
startDate?: string;
/** 结束日期 */
endDate?: string;
};
@ -1526,9 +1627,11 @@ declare namespace API {
details?: ShippingDetailsDTO;
stockPointId?: number;
orderIds?: number[];
shipmentPlatform?: string;
};
type ShipmentFeeBookDTO = {
shipmentPlatform?: string;
stockPointId?: number;
sender?: string;
startPhone?: string;
@ -1549,6 +1652,7 @@ declare namespace API {
dimensionUom?: string;
weight?: number;
weightUom?: string;
address_id?: number;
};
type ShippingAddress = {
@ -1708,6 +1812,8 @@ declare namespace API {
where?: any;
/** 排序对象,例如 { "sku": "desc" } */
orderBy?: any;
/** 分组字段,例如 "categoryId" */
groupBy?: string;
siteId: number;
};
@ -1726,6 +1832,8 @@ declare namespace API {
where?: any;
/** 排序对象,例如 { "sku": "desc" } */
orderBy?: any;
/** 分组字段,例如 "categoryId" */
groupBy?: string;
siteId: number;
};
@ -1744,6 +1852,8 @@ declare namespace API {
where?: any;
/** 排序对象,例如 { "sku": "desc" } */
orderBy?: any;
/** 分组字段,例如 "categoryId" */
groupBy?: string;
siteId: number;
};
@ -1762,6 +1872,8 @@ declare namespace API {
where?: any;
/** 排序对象,例如 { "sku": "desc" } */
orderBy?: any;
/** 分组字段,例如 "categoryId" */
groupBy?: string;
siteId: number;
};
@ -1780,6 +1892,8 @@ declare namespace API {
where?: any;
/** 排序对象,例如 { "sku": "desc" } */
orderBy?: any;
/** 分组字段,例如 "categoryId" */
groupBy?: string;
siteId: number;
};
@ -1798,6 +1912,8 @@ declare namespace API {
where?: any;
/** 排序对象,例如 { "sku": "desc" } */
orderBy?: any;
/** 分组字段,例如 "categoryId" */
groupBy?: string;
siteId: number;
};
@ -1816,6 +1932,8 @@ declare namespace API {
where?: any;
/** 排序对象,例如 { "sku": "desc" } */
orderBy?: any;
/** 分组字段,例如 "categoryId" */
groupBy?: string;
customerId: number;
siteId: number;
};
@ -1840,6 +1958,8 @@ declare namespace API {
where?: any;
/** 排序对象,例如 { "sku": "desc" } */
orderBy?: any;
/** 分组字段,例如 "categoryId" */
groupBy?: string;
siteId: number;
};
@ -1862,6 +1982,8 @@ declare namespace API {
where?: any;
/** 排序对象,例如 { "sku": "desc" } */
orderBy?: any;
/** 分组字段,例如 "categoryId" */
groupBy?: string;
siteId: number;
};
@ -1895,6 +2017,8 @@ declare namespace API {
where?: any;
/** 排序对象,例如 { "sku": "desc" } */
orderBy?: any;
/** 分组字段,例如 "categoryId" */
groupBy?: string;
siteId: number;
};
@ -1918,6 +2042,8 @@ declare namespace API {
where?: any;
/** 排序对象,例如 { "sku": "desc" } */
orderBy?: any;
/** 分组字段,例如 "categoryId" */
groupBy?: string;
siteId: number;
};
@ -1936,6 +2062,8 @@ declare namespace API {
where?: any;
/** 排序对象,例如 { "sku": "desc" } */
orderBy?: any;
/** 分组字段,例如 "categoryId" */
groupBy?: string;
siteId: number;
};
@ -1954,6 +2082,8 @@ declare namespace API {
where?: any;
/** 排序对象,例如 { "sku": "desc" } */
orderBy?: any;
/** 分组字段,例如 "categoryId" */
groupBy?: string;
siteId: number;
};
@ -1977,6 +2107,8 @@ declare namespace API {
where?: any;
/** 排序对象,例如 { "sku": "desc" } */
orderBy?: any;
/** 分组字段,例如 "categoryId" */
groupBy?: string;
siteId: number;
};
@ -2928,6 +3060,8 @@ declare namespace API {
where?: any;
/** 排序对象,例如 { "sku": "desc" } */
orderBy?: any;
/** 分组字段,例如 "categoryId" */
groupBy?: string;
};
type UnifiedShippingLineDTO = {
@ -3088,12 +3222,16 @@ declare namespace API {
sku?: string;
/** 分类ID (DictItem ID) */
categoryId?: number;
/** 分类名称 */
categoryName?: string;
/** 站点 SKU 列表 */
siteSkus?: any[];
/** 价格 */
price?: number;
/** 促销价格 */
promotionPrice?: number;
/** 产品图片URL */
image?: string;
/** 属性列表 */
attributes?: any[];
/** 商品类型 */
@ -3144,6 +3282,8 @@ declare namespace API {
stockPointIds?: any;
/** 站点网站URL */
websiteUrl?: string;
/** Webhook URL */
webhookUrl?: string;
};
type UpdateStockDTO = {