feat(客户分析): 添加数据分析列表页面并迁移客户统计功能

- 新增数据分析列表页面组件
- 将原客户统计功能迁移至新页面
- 添加cubejs客户端依赖用于数据分析
- 调整路由配置指向新页面
- 修复店铺布局z-index问题
This commit is contained in:
tikkhun 2026-01-21 14:37:37 +08:00
parent 2471eb5299
commit 1fbfd95003
5 changed files with 299 additions and 292 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',
// }
],
},
{

View File

@ -16,6 +16,8 @@
"@ant-design/charts": "^2.2.6",
"@ant-design/icons": "^5.0.1",
"@ant-design/pro-components": "^2.4.4",
"@cubejs-client/core": "^1.6.4",
"@cubejs-client/react": "^1.6.4",
"@fingerprintjs/fingerprintjs": "^4.6.2",
"@monaco-editor/react": "^4.7.0",
"@tinymce/tinymce-react": "^6.3.0",

View File

@ -1,289 +0,0 @@
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

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

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