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

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

View File

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

View File

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

View File

@ -239,7 +239,10 @@ const WpToolPage: React.FC = () => {
useEffect(() => {
const fetchAllConfigs = async () => {
try {
message.loading({ content: '正在加载字典配置...', key: 'loading-config' });
message.loading({
content: '正在加载字典配置...',
key: 'loading-config',
});
// 1. 获取所有字典列表以找到对应的 ID
const dictListResponse = await request('/dict/list');
@ -303,13 +306,22 @@ const WpToolPage: React.FC = () => {
message.success({ content: '字典配置加载成功', key: 'loading-config' });
// 显示加载结果统计
const totalItems = brands.length + fruitKeys.length + mintKeys.length + flavorKeys.length +
strengthKeys.length + sizeKeys.length + humidityKeys.length + categoryKeys.length;
const totalItems =
brands.length +
fruitKeys.length +
mintKeys.length +
flavorKeys.length +
strengthKeys.length +
sizeKeys.length +
humidityKeys.length +
categoryKeys.length;
console.log(`字典配置加载完成: 共 ${totalItems} 个配置项`);
} catch (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(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.siteapicontrollerBatchordersParams,
body: Record<string, any>,
body: API.BatchOperationDTO,
options?: { [key: string]: any },
) {
const { siteId: param0, ...queryParams } = params;
return request<Record<string, any>>(`/site-api/${param0}/orders/batch`, {
method: 'POST',
headers: {
'Content-Type': 'text/plain',
return request<API.BatchOperationResultDTO>(
`/site-api/${param0}/orders/batch`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
params: { ...queryParams },
data: body,
...(options || {}),
},
params: { ...queryParams },
data: body,
...(options || {}),
});
);
}
/** 此处后端没有提供注释 POST /site-api/${param0}/orders/batch-ship */
@ -381,19 +384,22 @@ export async function siteapicontrollerCreateproduct(
export async function siteapicontrollerBatchproducts(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.siteapicontrollerBatchproductsParams,
body: Record<string, any>,
body: API.BatchOperationDTO,
options?: { [key: string]: any },
) {
const { siteId: param0, ...queryParams } = params;
return request<Record<string, any>>(`/site-api/${param0}/products/batch`, {
method: 'POST',
headers: {
'Content-Type': 'text/plain',
return request<API.BatchOperationResultDTO>(
`/site-api/${param0}/products/batch`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
params: { ...queryParams },
data: body,
...(options || {}),
},
params: { ...queryParams },
data: body,
...(options || {}),
});
);
}
/** 此处后端没有提供注释 GET /site-api/${param0}/products/export */

View File

@ -40,6 +40,39 @@ declare namespace API {
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 = {
/** 订单ID */
order_id?: string;