forked from yoone/WEB
1
0
Fork 0

feat(客户管理): 添加历史订单组件和地址展示优化

- 新增HistoryOrders组件用于展示客户历史订单及统计信息
- 创建Address组件统一处理地址展示逻辑
- 优化客户列表页的地址展示和操作列
- 更新API类型定义和批量操作接口
- 调整代码格式和样式
This commit is contained in:
tikkhun 2025-12-24 15:18:38 +08:00 committed by 黄珑
parent 8524cc1ec0
commit 96cc6d3dda
8 changed files with 442 additions and 90 deletions

View File

@ -16,6 +16,7 @@ export default defineConfig({
layout: { layout: {
title: 'YOONE', title: 'YOONE',
}, },
esbuildMinifyIIFE: true,
define: { define: {
UMI_APP_API_URL, UMI_APP_API_URL,
}, },

View File

@ -0,0 +1,38 @@
import React from 'react';
interface AddressProps {
address: {
address_1?: string;
address_2?: string;
city?: string;
state?: string;
postcode?: string;
country?: string;
phone?: string;
};
style?: React.CSSProperties;
}
const Address: React.FC<AddressProps> = ({ address, style }) => {
if (!address) {
return <span>-</span>;
}
const { address_1, address_2, city, state, postcode, country, phone } =
address;
return (
<div style={{ fontSize: 12, ...style }}>
<div>
{address_1} {address_2}
</div>
<div>
{city}, {state}, {postcode}
</div>
<div>{country}</div>
<div>{phone}</div>
</div>
);
};
export default Address;

View File

@ -0,0 +1,255 @@
import { ordercontrollerGetorders } from '@/servers/api/order';
import { siteapicontrollerGetorders } from '@/servers/api/siteApi';
import {
App,
Col,
Modal,
Row,
Spin,
Statistic,
Table,
Tag,
Typography,
} from 'antd';
import dayjs from 'dayjs';
import { useState } from 'react';
const { Text, Title } = Typography;
interface HistoryOrdersProps {
customer: API.UnifiedCustomerDTO;
siteId?: number;
}
interface OrderStats {
totalOrders: number;
totalAmount: number;
yooneOrders: number;
yooneAmount: number;
}
const HistoryOrders: React.FC<HistoryOrdersProps> = ({ customer, siteId }) => {
const { message } = App.useApp();
const [modalVisible, setModalVisible] = useState(false);
const [orders, setOrders] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const [stats, setStats] = useState<OrderStats>({
totalOrders: 0,
totalAmount: 0,
yooneOrders: 0,
yooneAmount: 0,
});
// 计算订单统计信息
const calculateStats = (orders: any[]) => {
let totalOrders = 0;
let totalAmount = 0;
let yooneOrders = 0;
let yooneAmount = 0;
orders.forEach((order) => {
totalOrders++;
// total是字符串需要转换为数字
const orderTotal = parseFloat(order.total || '0');
totalAmount += orderTotal;
// 检查订单中是否包含yoone商品
let hasYoone = false;
let orderYooneAmount = 0;
// 优先使用line_items如果没有则使用items
const items = order.line_items || order.items || [];
if (Array.isArray(items)) {
items.forEach((item: any) => {
// 检查商品名称或SKU是否包含yoone不区分大小写
const itemName = (item.name || '').toLowerCase();
const sku = (item.sku || '').toLowerCase();
if (itemName.includes('yoone') || sku.includes('yoone')) {
hasYoone = true;
const itemTotal = parseFloat(item.total || item.price || '0');
orderYooneAmount += itemTotal;
}
});
}
if (hasYoone) {
yooneOrders++;
yooneAmount += orderYooneAmount;
}
});
return {
totalOrders,
totalAmount,
yooneOrders,
yooneAmount,
};
};
// 获取客户订单数据
const fetchOrders = async () => {
setLoading(true);
try {
const response = await ordercontrollerGetorders({
customer_email: customer.email,
});
if (response) {
const orderList = response.items || [];
setOrders(orderList);
const calculatedStats = calculateStats(orderList);
setStats(calculatedStats);
} else {
message.error('获取订单数据失败');
}
} catch (error) {
console.error('获取订单失败:', error);
message.error('获取订单失败');
} finally {
setLoading(false);
}
};
// 打开弹框时获取数据
const handleOpenModal = () => {
setModalVisible(true);
fetchOrders();
};
// 订单表格列配置
const orderColumns = [
{
title: '订单号',
dataIndex: 'externalOrderId',
key: 'externalOrderId',
width: 120,
},
{
title: '订单状态',
dataIndex: 'status',
key: 'status',
width: 100,
render: (status: string) => {
const statusMap: Record<string, string> = {
pending: '待处理',
processing: '处理中',
'on-hold': '等待中',
completed: '已完成',
cancelled: '已取消',
refunded: '已退款',
failed: '失败',
};
return <Tag color="blue">{statusMap[status] || status}</Tag>;
},
},
{
title: '订单金额',
dataIndex: 'total',
key: 'total',
width: 100,
render: (total: string, record: any) => (
<Text>
{record.currency_symbol || '$'}
{parseFloat(total || '0').toFixed(2)}
</Text>
),
},
{
title: '创建时间',
dataIndex: 'date_created',
key: 'date_created',
width: 140,
render: (date: string) => (
<Text>{date ? dayjs(date).format('YYYY-MM-DD HH:mm') : '-'}</Text>
),
},
{
title: '包含Yoone',
key: 'hasYoone',
width: 80,
render: (_: any, record: any) => {
let hasYoone = false;
const items = record.line_items || record.items || [];
if (Array.isArray(items)) {
hasYoone = items.some((item: any) => {
const itemName = (item.name || '').toLowerCase();
const sku = (item.sku || '').toLowerCase();
return itemName.includes('yoone') || sku.includes('yoone');
});
}
return hasYoone ? <Tag color="green"></Tag> : <Tag></Tag>;
},
},
];
return (
<>
<a onClick={handleOpenModal}></a>
<Modal
title={`${customer.fullname || customer.email} 的历史订单`}
open={modalVisible}
onCancel={() => setModalVisible(false)}
footer={null}
width={1000}
>
<Spin spinning={loading}>
{/* 统计信息 */}
<Row gutter={16} style={{ marginBottom: 24 }}>
<Col span={6}>
<Statistic
title="总订单数"
value={stats.totalOrders}
prefix="#"
/>
</Col>
<Col span={6}>
<Statistic
title="总金额"
value={stats.totalAmount}
precision={2}
prefix="$"
/>
</Col>
<Col span={6}>
<Statistic
title="Yoone订单数"
value={stats.yooneOrders}
prefix="#"
/>
</Col>
<Col span={6}>
<Statistic
title="Yoone金额"
value={stats.yooneAmount}
precision={2}
prefix="$"
/>
</Col>
</Row>
{/* 订单列表 */}
<Title level={4} style={{ marginTop: 24 }}>
</Title>
<Table
columns={orderColumns}
dataSource={orders}
rowKey="id"
pagination={{
pageSize: 10,
showSizeChanger: true,
showTotal: (total) => `${total}`,
}}
scroll={{ x: 800 }}
/>
</Spin>
</Modal>
</>
);
};
export default HistoryOrders;

View File

@ -16,8 +16,8 @@ import {
ProTable, ProTable,
} from '@ant-design/pro-components'; } from '@ant-design/pro-components';
import { App, Avatar, Button, Rate, Space, Tag, Tooltip } from 'antd'; import { App, Avatar, Button, Rate, Space, Tag, Tooltip } from 'antd';
import dayjs from 'dayjs';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import HistoryOrders from './HistoryOrders';
// 地址格式化函数 // 地址格式化函数
const formatAddress = (address: any) => { const formatAddress = (address: any) => {
@ -42,7 +42,7 @@ const formatAddress = (address: any) => {
postcode, postcode,
country, country,
phone: addressPhone, phone: addressPhone,
email: addressEmail email: addressEmail,
} = address; } = address;
const parts = []; const parts = [];
@ -73,7 +73,10 @@ const formatAddress = (address: any) => {
}; };
// 地址卡片组件 // 地址卡片组件
const AddressCell: React.FC<{ address: any; title: string }> = ({ address, title }) => { const AddressCell: React.FC<{ address: any; title: string }> = ({
address,
title,
}) => {
const formattedAddress = formatAddress(address); const formattedAddress = formatAddress(address);
if (formattedAddress === '-') { if (formattedAddress === '-') {
@ -91,13 +94,15 @@ const AddressCell: React.FC<{ address: any; title: string }> = ({ address, title
} }
placement="topLeft" placement="topLeft"
> >
<div style={{ <div
style={{
maxWidth: 200, maxWidth: 200,
overflow: 'hidden', overflow: 'hidden',
textOverflow: 'ellipsis', textOverflow: 'ellipsis',
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
cursor: 'pointer' cursor: 'pointer',
}}> }}
>
{formattedAddress} {formattedAddress}
</div> </div>
</Tooltip> </Tooltip>
@ -126,8 +131,11 @@ const CustomerList: React.FC = () => {
// 根据站点ID获取站点名称 // 根据站点ID获取站点名称
const getSiteName = (siteId: number | undefined | null) => { const getSiteName = (siteId: number | undefined | null) => {
if (!siteId) return '-'; if (!siteId) return '-';
const site = sites.find(s => s.id === siteId); if (typeof siteId === 'string') {
console.log(`site`,site) return siteId;
}
const site = sites.find((s) => s.id === siteId);
console.log(`site`, site);
return site ? site.name : String(siteId); return site ? site.name : String(siteId);
}; };
@ -161,9 +169,8 @@ const CustomerList: React.FC = () => {
return []; return [];
} }
}, },
render: (siteId, record) => { render: (siteId: any) => {
return siteId return <span>{getSiteName(siteId) || '-'}</span>;
return getSiteName(record.site_id) || '-';
}, },
}, },
{ {
@ -187,8 +194,8 @@ const CustomerList: React.FC = () => {
sorter: true, sorter: true,
render: (_, record) => { render: (_, record) => {
return ( return (
record.fullName || record.fullname ||
`${record.firstName || ''} ${record.lastName || ''}`.trim() || `${record.first_name || ''} ${record.last_name || ''}`.trim() ||
record.username || record.username ||
'-' '-'
); );
@ -228,6 +235,7 @@ const CustomerList: React.FC = () => {
title: '评分', title: '评分',
dataIndex: 'rate', dataIndex: 'rate',
width: 120, width: 120,
hideInSearch: true,
render: (_, record) => { render: (_, record) => {
return ( return (
<Rate <Rate
@ -246,7 +254,7 @@ const CustomerList: React.FC = () => {
message.error(e?.message || '设置评分失败'); message.error(e?.message || '设置评分失败');
} }
}} }}
value={record.rate} value={record.raw?.rate || 0}
allowHalf allowHalf
/> />
); );
@ -257,9 +265,10 @@ const CustomerList: React.FC = () => {
dataIndex: 'tags', dataIndex: 'tags',
hideInSearch: true, hideInSearch: true,
render: (_, record) => { render: (_, record) => {
const tags = record.raw?.tags || [];
return ( return (
<Space size={[0, 8]} wrap> <Space size={[0, 8]} wrap>
{(record.tags || []).map((tag: string) => { {tags.map((tag: string) => {
return ( return (
<Tag <Tag
key={tag} key={tag}
@ -302,7 +311,6 @@ const CustomerList: React.FC = () => {
hideInSearch: true, hideInSearch: true,
sorter: true, sorter: true,
width: 140, width: 140,
}, },
{ {
title: '操作', title: '操作',
@ -314,20 +322,12 @@ const CustomerList: React.FC = () => {
return ( return (
<Space direction="vertical" size="small"> <Space direction="vertical" size="small">
<AddTag <AddTag
email={record.email} email={record.email || ''}
tags={record.tags} tags={record.raw?.tags || []}
tableRef={actionRef} tableRef={actionRef}
/> />
<Button {/* 订单 */}
type="link" <HistoryOrders customer={record} siteId={record.raw?.site_id} />
size="small"
onClick={() => {
// 这里可以添加查看客户详情的逻辑
message.info('客户详情功能开发中...');
}}
>
</Button>
</Space> </Space>
); );
}, },
@ -347,7 +347,9 @@ const CustomerList: React.FC = () => {
...params, ...params,
current: params.current?.toString(), current: params.current?.toString(),
pageSize: params.pageSize?.toString(), pageSize: params.pageSize?.toString(),
...(key ? { sorterKey: key, sorterValue: sorter[key] as string } : {}), ...(key
? { sorterKey: key, sorterValue: sorter[key] as string }
: {}),
}); });
return { return {
@ -488,7 +490,11 @@ const SyncCustomersModal: React.FC<{
const handleSync = async (values: { siteId: number }) => { const handleSync = async (values: { siteId: number }) => {
try { try {
setLoading(true); setLoading(true);
const { success, message: msg, data } = await customercontrollerSynccustomers({ const {
success,
message: msg,
data,
} = await customercontrollerSynccustomers({
siteId: values.siteId, siteId: values.siteId,
}); });
@ -500,7 +506,7 @@ const SyncCustomersModal: React.FC<{
synced = 0, synced = 0,
created = 0, created = 0,
updated = 0, updated = 0,
errors = [] errors = [],
} = result; } = result;
let resultMessage = `同步完成!共处理 ${total} 个客户:`; let resultMessage = `同步完成!共处理 ${total} 个客户:`;
@ -516,20 +522,24 @@ const SyncCustomersModal: React.FC<{
<div> <div>
<div>{resultMessage}</div> <div>{resultMessage}</div>
<div style={{ marginTop: 8, fontSize: 12, color: '#faad14' }}> <div style={{ marginTop: 8, fontSize: 12, color: '#faad14' }}>
{errors.slice(0, 3).map((err: any) => err.email || err.error).join(', ')}
{errors
.slice(0, 3)
.map((err: any) => err.email || err.error)
.join(', ')}
{errors.length > 3 && `${errors.length - 3} 个错误...`} {errors.length > 3 && `${errors.length - 3} 个错误...`}
</div> </div>
</div> </div>
), ),
duration: 8, duration: 8,
key: 'sync-result' key: 'sync-result',
}); });
} else { } else {
// 完全成功 // 完全成功
message.success({ message.success({
content: resultMessage, content: resultMessage,
duration: 4, duration: 4,
key: 'sync-result' key: 'sync-result',
}); });
} }

View File

@ -1,3 +1,4 @@
import Address from '@/components/Address';
import { import {
DeleteFilled, DeleteFilled,
EditOutlined, EditOutlined,
@ -187,19 +188,15 @@ const CustomerPage: React.FC = () => {
hideInSearch: true, hideInSearch: true,
render: (_, record) => { render: (_, record) => {
const { billing } = record; const { billing } = record;
if (!billing) return '-'; return <Address address={billing} />;
return ( },
<div style={{ fontSize: 12 }}> },
<div> {
{billing.address_1} {billing.address_2} title: '物流地址',
</div> dataIndex: 'shipping',
<div> hideInSearch: true,
{billing.city}, {billing.state}, {billing.postcode} render: (shipping) => {
</div> return <Address address={shipping} />;
<div>{billing.country}</div>
<div>{billing.phone}</div>
</div>
);
}, },
}, },
{ {

View File

@ -239,7 +239,10 @@ const WpToolPage: React.FC = () => {
useEffect(() => { useEffect(() => {
const fetchAllConfigs = async () => { const fetchAllConfigs = async () => {
try { try {
message.loading({ content: '正在加载字典配置...', key: 'loading-config' }); message.loading({
content: '正在加载字典配置...',
key: 'loading-config',
});
// 1. 获取所有字典列表以找到对应的 ID // 1. 获取所有字典列表以找到对应的 ID
const dictListResponse = await request('/dict/list'); const dictListResponse = await request('/dict/list');
@ -303,13 +306,22 @@ const WpToolPage: React.FC = () => {
message.success({ content: '字典配置加载成功', key: 'loading-config' }); message.success({ content: '字典配置加载成功', key: 'loading-config' });
// 显示加载结果统计 // 显示加载结果统计
const totalItems = brands.length + fruitKeys.length + mintKeys.length + flavorKeys.length + const totalItems =
strengthKeys.length + sizeKeys.length + humidityKeys.length + categoryKeys.length; brands.length +
fruitKeys.length +
mintKeys.length +
flavorKeys.length +
strengthKeys.length +
sizeKeys.length +
humidityKeys.length +
categoryKeys.length;
console.log(`字典配置加载完成: 共 ${totalItems} 个配置项`); console.log(`字典配置加载完成: 共 ${totalItems} 个配置项`);
} catch (error) { } catch (error) {
console.error('Failed to fetch configs:', error); console.error('Failed to fetch configs:', error);
message.error({ content: '获取字典配置失败,请刷新页面重试', key: 'loading-config' }); message.error({
content: '获取字典配置失败,请刷新页面重试',
key: 'loading-config',
});
} }
}; };

View File

@ -262,19 +262,22 @@ export async function siteapicontrollerCreateorder(
export async function siteapicontrollerBatchorders( export async function siteapicontrollerBatchorders(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象) // 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.siteapicontrollerBatchordersParams, params: API.siteapicontrollerBatchordersParams,
body: Record<string, any>, body: API.BatchOperationDTO,
options?: { [key: string]: any }, options?: { [key: string]: any },
) { ) {
const { siteId: param0, ...queryParams } = params; const { siteId: param0, ...queryParams } = params;
return request<Record<string, any>>(`/site-api/${param0}/orders/batch`, { return request<API.BatchOperationResultDTO>(
`/site-api/${param0}/orders/batch`,
{
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'text/plain', 'Content-Type': 'application/json',
}, },
params: { ...queryParams }, params: { ...queryParams },
data: body, data: body,
...(options || {}), ...(options || {}),
}); },
);
} }
/** 此处后端没有提供注释 POST /site-api/${param0}/orders/batch-ship */ /** 此处后端没有提供注释 POST /site-api/${param0}/orders/batch-ship */
@ -381,19 +384,22 @@ export async function siteapicontrollerCreateproduct(
export async function siteapicontrollerBatchproducts( export async function siteapicontrollerBatchproducts(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象) // 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.siteapicontrollerBatchproductsParams, params: API.siteapicontrollerBatchproductsParams,
body: Record<string, any>, body: API.BatchOperationDTO,
options?: { [key: string]: any }, options?: { [key: string]: any },
) { ) {
const { siteId: param0, ...queryParams } = params; const { siteId: param0, ...queryParams } = params;
return request<Record<string, any>>(`/site-api/${param0}/products/batch`, { return request<API.BatchOperationResultDTO>(
`/site-api/${param0}/products/batch`,
{
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'text/plain', 'Content-Type': 'application/json',
}, },
params: { ...queryParams }, params: { ...queryParams },
data: body, data: body,
...(options || {}), ...(options || {}),
}); },
);
} }
/** 此处后端没有提供注释 GET /site-api/${param0}/products/export */ /** 此处后端没有提供注释 GET /site-api/${param0}/products/export */

View File

@ -40,6 +40,39 @@ declare namespace API {
ids: any[]; ids: any[];
}; };
type BatchErrorItemDTO = {
/** 错误项标识如ID、邮箱等 */
identifier?: string;
/** 错误信息 */
error?: string;
};
type BatchOperationDTO = {
/** 要创建的数据列表 */
create?: any[];
/** 要更新的数据列表 */
update?: any[];
/** 要删除的ID列表 */
delete?: string[];
};
type BatchOperationResultDTO = {
/** 总处理数量 */
total?: number;
/** 成功处理数量 */
processed?: number;
/** 创建数量 */
created?: number;
/** 更新数量 */
updated?: number;
/** 删除数量 */
deleted?: number;
/** 跳过的数量 */
skipped?: number;
/** 错误列表 */
errors?: BatchErrorItemDTO[];
};
type BatchShipOrderItemDTO = { type BatchShipOrderItemDTO = {
/** 订单ID */ /** 订单ID */
order_id?: string; order_id?: string;