From 96cc6d3dda1ce019a8eab2a28b8fa59a14be711c Mon Sep 17 00:00:00 2001 From: tikkhun Date: Wed, 24 Dec 2025 15:18:38 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=AE=A2=E6=88=B7=E7=AE=A1=E7=90=86):=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=8E=86=E5=8F=B2=E8=AE=A2=E5=8D=95=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=E5=92=8C=E5=9C=B0=E5=9D=80=E5=B1=95=E7=A4=BA=E4=BC=98?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增HistoryOrders组件用于展示客户历史订单及统计信息 - 创建Address组件统一处理地址展示逻辑 - 优化客户列表页的地址展示和操作列 - 更新API类型定义和批量操作接口 - 调整代码格式和样式 --- .umirc.ts | 1 + src/components/Address.tsx | 38 ++++ src/pages/Customer/List/HistoryOrders.tsx | 255 ++++++++++++++++++++++ src/pages/Customer/List/index.tsx | 112 +++++----- src/pages/Site/Shop/Customers/index.tsx | 23 +- src/pages/Woo/Product/TagTool/index.tsx | 28 ++- src/servers/api/siteApi.ts | 42 ++-- src/servers/api/typings.d.ts | 33 +++ 8 files changed, 442 insertions(+), 90 deletions(-) create mode 100644 src/components/Address.tsx create mode 100644 src/pages/Customer/List/HistoryOrders.tsx diff --git a/.umirc.ts b/.umirc.ts index b585857..f2d21f1 100644 --- a/.umirc.ts +++ b/.umirc.ts @@ -16,6 +16,7 @@ export default defineConfig({ layout: { title: 'YOONE', }, + esbuildMinifyIIFE: true, define: { UMI_APP_API_URL, }, diff --git a/src/components/Address.tsx b/src/components/Address.tsx new file mode 100644 index 0000000..111619b --- /dev/null +++ b/src/components/Address.tsx @@ -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 = ({ address, style }) => { + if (!address) { + return -; + } + + const { address_1, address_2, city, state, postcode, country, phone } = + address; + + return ( +
+
+ {address_1} {address_2} +
+
+ {city}, {state}, {postcode} +
+
{country}
+
{phone}
+
+ ); +}; + +export default Address; diff --git a/src/pages/Customer/List/HistoryOrders.tsx b/src/pages/Customer/List/HistoryOrders.tsx new file mode 100644 index 0000000..006e0e1 --- /dev/null +++ b/src/pages/Customer/List/HistoryOrders.tsx @@ -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 = ({ customer, siteId }) => { + const { message } = App.useApp(); + const [modalVisible, setModalVisible] = useState(false); + const [orders, setOrders] = useState([]); + const [loading, setLoading] = useState(false); + const [stats, setStats] = useState({ + 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 = { + pending: '待处理', + processing: '处理中', + 'on-hold': '等待中', + completed: '已完成', + cancelled: '已取消', + refunded: '已退款', + failed: '失败', + }; + return {statusMap[status] || status}; + }, + }, + { + title: '订单金额', + dataIndex: 'total', + key: 'total', + width: 100, + render: (total: string, record: any) => ( + + {record.currency_symbol || '$'} + {parseFloat(total || '0').toFixed(2)} + + ), + }, + { + title: '创建时间', + dataIndex: 'date_created', + key: 'date_created', + width: 140, + render: (date: string) => ( + {date ? dayjs(date).format('YYYY-MM-DD HH:mm') : '-'} + ), + }, + { + 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 ? : ; + }, + }, + ]; + + return ( + <> + 历史订单 + + setModalVisible(false)} + footer={null} + width={1000} + > + + {/* 统计信息 */} + + + + + + + + + + + + + + + + {/* 订单列表 */} + + 订单详情 + + `共 ${total} 条`, + }} + scroll={{ x: 800 }} + /> + + + + ); +}; + +export default HistoryOrders; diff --git a/src/pages/Customer/List/index.tsx b/src/pages/Customer/List/index.tsx index a9a309d..2280537 100644 --- a/src/pages/Customer/List/index.tsx +++ b/src/pages/Customer/List/index.tsx @@ -16,13 +16,13 @@ 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) => { if (!address) return '-'; - + if (typeof address === 'string') { try { address = JSON.parse(address); @@ -30,7 +30,7 @@ const formatAddress = (address: any) => { return address; } } - + const { first_name, last_name, @@ -42,46 +42,49 @@ const formatAddress = (address: any) => { postcode, country, phone: addressPhone, - email: addressEmail + email: addressEmail, } = address; - + const parts = []; - + // 姓名 const fullName = [first_name, last_name].filter(Boolean).join(' '); if (fullName) parts.push(fullName); - + // 公司 if (company) parts.push(company); - + // 地址行 if (address_1) parts.push(address_1); if (address_2) parts.push(address_2); - + // 城市、州、邮编 const locationParts = [city, state, postcode].filter(Boolean).join(', '); if (locationParts) parts.push(locationParts); - + // 国家 if (country) parts.push(country); - + // 联系方式 if (addressPhone) parts.push(`电话: ${addressPhone}`); if (addressEmail) parts.push(`邮箱: ${addressEmail}`); - + return parts.join(', '); }; // 地址卡片组件 -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 === '-') { return -; } - + return ( - {title}: @@ -91,13 +94,15 @@ const AddressCell: React.FC<{ address: any; title: string }> = ({ address, title } placement="topLeft" > -
+
{formattedAddress}
@@ -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 {getSiteName(siteId) || '-'}; }, }, { @@ -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 ( { 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 ( - {(record.tags || []).map((tag: string) => { + {tags.map((tag: string) => { return ( { hideInSearch: true, sorter: true, width: 140, - }, { title: '操作', @@ -314,20 +322,12 @@ const CustomerList: React.FC = () => { return ( - + {/* 订单 */} + ); }, @@ -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<{
{resultMessage}
- 失败详情:{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} 个错误...`}
), duration: 8, - key: 'sync-result' + key: 'sync-result', }); } else { // 完全成功 message.success({ content: resultMessage, duration: 4, - key: 'sync-result' + key: 'sync-result', }); } diff --git a/src/pages/Site/Shop/Customers/index.tsx b/src/pages/Site/Shop/Customers/index.tsx index fa82d18..b701dfb 100644 --- a/src/pages/Site/Shop/Customers/index.tsx +++ b/src/pages/Site/Shop/Customers/index.tsx @@ -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 ( -
-
- {billing.address_1} {billing.address_2} -
-
- {billing.city}, {billing.state}, {billing.postcode} -
-
{billing.country}
-
{billing.phone}
-
- ); + return
; + }, + }, + { + title: '物流地址', + dataIndex: 'shipping', + hideInSearch: true, + render: (shipping) => { + return
; }, }, { diff --git a/src/pages/Woo/Product/TagTool/index.tsx b/src/pages/Woo/Product/TagTool/index.tsx index e398d92..a42c4b8 100644 --- a/src/pages/Woo/Product/TagTool/index.tsx +++ b/src/pages/Woo/Product/TagTool/index.tsx @@ -239,8 +239,11 @@ 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'); // 处理后端统一响应格式 @@ -297,19 +300,28 @@ const WpToolPage: React.FC = () => { humidityKeys, categoryKeys, }; - + setConfig(newConfig); form.setFieldsValue(newConfig); 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', + }); } }; diff --git a/src/servers/api/siteApi.ts b/src/servers/api/siteApi.ts index 824ddba..9745d48 100644 --- a/src/servers/api/siteApi.ts +++ b/src/servers/api/siteApi.ts @@ -262,19 +262,22 @@ export async function siteapicontrollerCreateorder( export async function siteapicontrollerBatchorders( // 叠加生成的Param类型 (非body参数swagger默认没有生成对象) params: API.siteapicontrollerBatchordersParams, - body: Record, + body: API.BatchOperationDTO, options?: { [key: string]: any }, ) { const { siteId: param0, ...queryParams } = params; - return request>(`/site-api/${param0}/orders/batch`, { - method: 'POST', - headers: { - 'Content-Type': 'text/plain', + return request( + `/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, + body: API.BatchOperationDTO, options?: { [key: string]: any }, ) { const { siteId: param0, ...queryParams } = params; - return request>(`/site-api/${param0}/products/batch`, { - method: 'POST', - headers: { - 'Content-Type': 'text/plain', + return request( + `/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 */ diff --git a/src/servers/api/typings.d.ts b/src/servers/api/typings.d.ts index 4525d65..d4ae648 100644 --- a/src/servers/api/typings.d.ts +++ b/src/servers/api/typings.d.ts @@ -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;