From d8398afa12c31e17c3863e949092871e88abd0f2 Mon Sep 17 00:00:00 2001 From: RUI <298977887@qq.com> Date: Mon, 9 Jun 2025 01:27:51 +0800 Subject: [PATCH] 0609.1 --- docs/sf-express-logistics-status-guide.md | 365 ++++++++++++ src/components/TaskController.tsx | 170 ++++++ src/components/layout/Layout.tsx | 14 +- src/components/logistics/status-tag.tsx | 373 +++++++++++++ src/components/logistics/status.tsx | 526 +++++++++++++++--- src/models/index.ts | 1 - src/models/types.ts | 1 - src/pages/api/backstage/sales/[id].ts | 42 ++ src/pages/api/backstage/sales/index.ts | 87 +++ src/pages/api/sse.ts | 52 ++ src/pages/api/test/batch-query-logistics.bak | 176 ++++++ src/pages/api/test/batch-query-logistics.ts | 227 ++++++++ src/pages/api/test/logistic-detail.ts | 137 +++++ src/pages/api/test/logistics-records.ts | 87 +++ src/pages/api/test/sf-query.ts | 201 +++++++ src/pages/api/tools/test4.ts | 73 --- .../backstage/accounts/account-modal.tsx | 162 ++++++ src/pages/backstage/accounts/index.tsx | 251 +++++++++ src/pages/team/sale/index.tsx | 15 +- src/pages/test/sf.tsx | 439 +++++++++++++++ src/pages/test/sfall.tsx | 402 +++++++++++++ src/types/enum.ts | 90 ++- 22 files changed, 3734 insertions(+), 157 deletions(-) create mode 100644 docs/sf-express-logistics-status-guide.md create mode 100644 src/components/TaskController.tsx create mode 100644 src/components/logistics/status-tag.tsx create mode 100644 src/pages/api/backstage/sales/[id].ts create mode 100644 src/pages/api/backstage/sales/index.ts create mode 100644 src/pages/api/sse.ts create mode 100644 src/pages/api/test/batch-query-logistics.bak create mode 100644 src/pages/api/test/batch-query-logistics.ts create mode 100644 src/pages/api/test/logistic-detail.ts create mode 100644 src/pages/api/test/logistics-records.ts create mode 100644 src/pages/api/test/sf-query.ts delete mode 100644 src/pages/api/tools/test4.ts create mode 100644 src/pages/backstage/accounts/account-modal.tsx create mode 100644 src/pages/backstage/accounts/index.tsx create mode 100644 src/pages/test/sf.tsx create mode 100644 src/pages/test/sfall.tsx diff --git a/docs/sf-express-logistics-status-guide.md b/docs/sf-express-logistics-status-guide.md new file mode 100644 index 0000000..a4d6e82 --- /dev/null +++ b/docs/sf-express-logistics-status-guide.md @@ -0,0 +1,365 @@ +# SF快递物流状态判断指南 + +> **作者**: 阿瑞 +> **版本**: 1.0.0 +> **更新时间**: 2025-01-28 + +## 📖 概述 + +本文档详细介绍了如何在SF快递物流查询系统中准确判断和显示物流状态。基于SF快递官方API文档和实际开发经验总结。 + +## 🔍 状态判断数据源 + +### 官方标准数据结构 + +根据SF快递官方API文档,完整的路由节点应包含以下状态信息: + +```typescript +interface RouteInfo { + acceptTime: string; // 路由节点发生时间 + acceptAddress: string; // 路由节点发生地点 + remark: string; // 路由节点具体描述 + opCode: string; // 路由节点操作码 + firstStatusCode?: string; // 一级状态编码 + firstStatusName?: string; // 一级状态名称 + secondaryStatusCode?: string; // 二级状态编码 + secondaryStatusName?: string; // 二级状态名称 +} +``` + +### 数据完整性检查 + +实际API返回的数据可能缺少状态编码字段,需要多种备用方案: + +| 字段 | 重要程度 | 描述 | +|------|----------|------| +| `firstStatusName` | ⭐⭐⭐ | 官方一级状态,最准确 | +| `secondaryStatusName` | ⭐⭐⭐ | 官方二级状态,最详细 | +| `opCode` | ⭐⭐ | 操作码,可映射到状态 | +| `remark` | ⭐ | 文本描述,可通过关键词分析 | + +## 🎯 状态判断方法 + +### 方法一:官方状态字段(推荐) + +**优先级:最高** + +```javascript +if (route.firstStatusName) { + return route.firstStatusName; // 直接使用官方状态 +} +``` + +**常见状态值:** +- `待揽收` - 快件等待收取 +- `已揽收` - 快件已被收取 +- `运输中` - 快件正在运输 +- `派送中` - 快件正在派送 +- `已签收` - 快件成功签收 +- `已拒收` - 快件被拒收 +- `退回中` - 快件正在退回 +- `已退回` - 快件已退回发件人 + +### 方法二:opCode映射表(备用) + +**优先级:中等** + +> ✅ **数据来源**: 基于真实SF快递物流数据分析,包含完整快递生命周期的opCode对应关系。 +> +> 📋 **数据样本**: 分析了从揽收到退回成功的47个物流节点,包含以下新发现的opCode: +> - `43`: 已收取(重复确认) +> - `302`: 长途运输(显示距离信息) +> - `310`: 途经中转(城市中转点) +> - `44`: 准备派送(分配快递员) +> - `204`: 派送中(具体快递员信息) +> - `70`: 派送失败(多种失败原因) +> - `33`: 待派送(约定时间) +> - `517`: 退回申请中(客户申请) +> - `99`: 退回中(实际退回过程) +> - `80`: 退回成功(最终完成) + +```javascript +const statusMap = { + // ========== 揽收相关 ========== + '54': { status: '已揽收', color: 'blue', description: '快件已被顺丰收取' }, + '43': { status: '已收取', color: 'blue', description: '顺丰速运已收取快件' }, + + // ========== 分拣运输相关 ========== + '30': { status: '分拣中', color: 'processing', description: '快件正在分拣处理' }, + '36': { status: '运输中', color: 'processing', description: '快件正在运输途中' }, + '31': { status: '到达网点', color: 'processing', description: '快件已到达转运中心' }, + '302': { status: '长途运输', color: 'processing', description: '快件正在安全运输中(长距离)' }, + '310': { status: '途经中转', color: 'processing', description: '快件途经中转城市' }, + + // ========== 派送相关 ========== + '44': { status: '准备派送', color: 'processing', description: '正在为快件分配合适的快递员' }, + '204': { status: '派送中', color: 'processing', description: '快件已交给快递员,正在派送途中' }, + '70': { status: '派送失败', color: 'warning', description: '快件派送不成功,待再次派送' }, + '33': { status: '待派送', color: 'warning', description: '已与客户约定新派送时间,待派送' }, + + // ========== 退回相关 ========== + '517': { status: '退回申请中', color: 'warning', description: '快件退回申请已提交,正在处理中' }, + '99': { status: '退回中', color: 'warning', description: '应客户要求,快件正在退回中' }, + '80': { status: '退回成功', color: 'success', description: '退回快件已派送成功' }, + + // ========== 签收相关 ========== + '81': { status: '已签收', color: 'success', description: '快件已成功签收' }, + '82': { status: '已拒收', color: 'error', description: '快件被拒收' }, + + // ========== 其他状态 ========== + '655': { status: '待揽收', color: 'default', description: '快件等待揽收' }, + '701': { status: '待揽收', color: 'default', description: '快件等待揽收' }, + '101': { status: '已揽收', color: 'blue', description: '快件已揽收' }, +}; +``` + +### 方法三:文本关键词分析(最后备用) + +**优先级:最低** + +```javascript +const analyzeStatusByRemark = (remark) => { + const text = remark.toLowerCase(); + + // 揽收相关 + if (text.includes('已收取快件') || text.includes('已揽收')) { + return { status: '已揽收', color: 'blue' }; + } + + // 分拣相关 + if (text.includes('完成分拣')) { + return { status: '分拣中', color: 'processing' }; + } + + // 运输相关 + if (text.includes('离开') || text.includes('发往') || text.includes('路上') || text.includes('安全运输中')) { + return { status: '运输中', color: 'processing' }; + } + if (text.includes('途经')) { + return { status: '途经中转', color: 'processing' }; + } + if (text.includes('到达')) { + return { status: '到达网点', color: 'processing' }; + } + + // 派送相关 + if (text.includes('分配最合适的快递员')) { + return { status: '准备派送', color: 'processing' }; + } + if (text.includes('交给') && text.includes('正在派送途中')) { + return { status: '派送中', color: 'processing' }; + } + if (text.includes('派送不成功') || text.includes('约定新派送时间')) { + return { status: '派送失败', color: 'warning' }; + } + if (text.includes('待派送')) { + return { status: '待派送', color: 'warning' }; + } + + // 签收相关 + if (text.includes('派送成功') || text.includes('签收')) { + return { status: '已签收', color: 'success' }; + } + if (text.includes('拒收') || text.includes('拒付费用')) { + return { status: '已拒收', color: 'error' }; + } + + // 退回相关 + if (text.includes('退回申请已提交')) { + return { status: '退回申请中', color: 'warning' }; + } + if (text.includes('快件正在退回中') || text.includes('应客户要求')) { + return { status: '退回中', color: 'warning' }; + } + if (text.includes('退回快件已派送成功')) { + return { status: '退回成功', color: 'success' }; + } + + return { status: '处理中', color: 'default' }; +}; +``` + +## 🔄 状态判断流程图 + +```mermaid +graph TD + A[接收路由数据] --> B{是否有firstStatusName?} + B -->|有| C[使用官方一级状态] + B -->|无| D{是否有opCode?} + D -->|有| E[查询opCode映射表] + D -->|无| F[分析remark文本] + E --> G{映射表中存在?} + G -->|是| H[返回映射状态] + G -->|否| F + F --> I[返回文本分析结果] + C --> J[状态判断完成] + H --> J + I --> J +``` + +## 🎨 UI状态显示规范 + +### 状态颜色映射 + +| 状态类型 | 颜色 | CSS类 | 含义 | +|----------|------|-------|------| +| 已签收 | 🟢 绿色 | `bg-green-100 text-green-700` | 成功完成 | +| 已拒收/已退回 | 🔴 红色 | `bg-red-100 text-red-700` | 失败/异常 | +| 退回中 | 🟡 黄色 | `bg-yellow-100 text-yellow-700` | 警告状态 | +| 运输中/派送中 | 🔵 蓝色 | `bg-blue-100 text-blue-700` | 进行中 | +| 待揽收/处理中 | ⚪ 灰色 | `bg-gray-100 text-gray-700` | 等待/默认 | + +### 时间轴节点显示 + +```jsx +// 状态标签 + + {status} + + +// 详细信息 +
+ 📍 {acceptAddress} + 🔢 opCode: {opCode} + 📊 一级状态: {firstStatusName} + 📋 二级状态: {secondaryStatusName} +
+``` + +## 📊 API数据示例 + +### 完整状态数据(理想情况) + +```json +{ + "acceptTime": "2025-06-07 00:29:27", + "acceptAddress": "广州市", + "remark": "顺丰速运 已收取快件", + "opCode": "54", + "firstStatusCode": "1", + "firstStatusName": "已揽收", + "secondaryStatusCode": "101", + "secondaryStatusName": "已揽收" +} +``` + +### 缺失状态数据(当前情况) + +```json +{ + "acceptTime": "2025-06-07 00:29:27", + "acceptAddress": "广州市", + "remark": "顺丰速运 已收取快件", + "opCode": "54" + // 缺少 firstStatusName 等字段 +} +``` + +## ⚙️ 实现代码示例 + +### 完整状态判断函数 + +```typescript +const getOverallStatus = (routes: RouteInfo[]): { status: string; color: string } => { + if (!routes || routes.length === 0) { + return { status: '暂无信息', color: 'default' }; + } + + // 取第一个(最新的)路由节点来判断当前状态 + const latestRoute = routes[0]; + + // 优先使用官方状态字段 + if (latestRoute.firstStatusName) { + const statusColorMap: Record = { + '待揽收': 'default', + '已揽收': 'blue', + '运输中': 'processing', + '派送中': 'processing', + '已签收': 'success', + '已拒收': 'error', + '退回中': 'warning', + '已退回': 'error' + }; + return { + status: latestRoute.firstStatusName, + color: statusColorMap[latestRoute.firstStatusName] || 'default' + }; + } + + // 备用方案:通过opCode判断 + const opCodeStatus = getStatusByOpCode(latestRoute.opCode); + if (opCodeStatus.status !== '处理中') { + return { status: opCodeStatus.status, color: opCodeStatus.color }; + } + + // 最后备用:通过remark分析 + return analyzeStatusByRemark(latestRoute.remark); +}; +``` + +## 🔧 故障排查 + +### 常见问题及解决方案 + +#### 1. 状态字段缺失 + +**问题**: API返回数据缺少 `firstStatusName` 等状态字段 + +**可能原因**: +- API版本过旧 +- 权限配置不足 +- 请求参数不正确 + +**解决方案**: +1. 检查API版本,使用最新版本 +2. 确认账号权限包含状态字段获取 +3. 检查 `methodType` 参数设置 +4. 使用opCode映射作为备用方案 + +#### 2. 状态显示不准确 + +**问题**: 显示的状态与实际情况不符 + +**排查步骤**: +1. 检查时间轴排序(最新的应该在前) +2. 验证opCode映射表的准确性 +3. 检查文本关键词匹配逻辑 +4. 查看原始API返回数据 + +#### 3. 状态更新延迟 + +**问题**: 状态更新不及时 + +**注意事项**: +- SF快递API数据更新可能有延迟 +- 建议设置合理的查询间隔 +- 避免频繁查询造成限流 + +## 📝 开发建议 + +### 最佳实践 + +1. **分层状态判断**: 按优先级使用多种方法 +2. **缓存机制**: 避免重复查询相同运单 +3. **错误处理**: 优雅处理API异常情况 +4. **用户体验**: 提供加载状态和错误提示 +5. **数据验证**: 验证API返回数据的完整性 + +### 性能优化 + +1. **状态映射表**: 预定义常用状态避免重复计算 +2. **条件渲染**: 只在有数据时渲染状态组件 +3. **防抖处理**: 防止用户频繁点击查询 + +## 📚 参考资料 + +- [SF快递开发者文档](https://open.sf-express.com/) +- [路由查询接口文档](https://open.sf-express.com/api/EXP_RECE_SEARCH_ROUTES) +- [Ant Design Timeline组件](https://ant.design/components/timeline) + +## 📧 联系方式 + +如有问题或建议,请联系: +- 开发者:阿瑞 +- 技术栈:TypeScript + Next.js + Ant Design +- 文档版本:v1.0.0 \ No newline at end of file diff --git a/src/components/TaskController.tsx b/src/components/TaskController.tsx new file mode 100644 index 0000000..f89db4e --- /dev/null +++ b/src/components/TaskController.tsx @@ -0,0 +1,170 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { Button, message, Popover } from 'antd'; +import { PoweroffOutlined } from '@ant-design/icons'; + +// 自定义 Hook,用于管理定时器 +function useInterval(callback: () => void, delay: number | null) { + const savedCallback = useRef<() => void>(() => {}); + + // 保存最新的回调函数,以便在定时器触发时使用 + useEffect(() => { + savedCallback.current = callback; + }, [callback]); + + // 当 delay 发生变化时,设置或清除定时器 + useEffect(() => { + if (delay !== null) { + const id = setInterval(() => savedCallback.current && savedCallback.current(), delay); + return () => clearInterval(id); // 清除定时器以防止内存泄漏 + } + }, [delay]); +} + +const TaskController: React.FC = () => { + // 剩余时间的状态 + const [remainingTime, setRemainingTime] = useState(null); + // 是否正在计时的状态 + const [isCounting, setIsCounting] = useState(false); + // 用于存储 EventSource 的引用 + const eventSourceRef = useRef(null); + const lastQueryResultRef = useRef(null); // 用于存储上次查询的结果 + + // 开始任务并启动倒计时的函数 + const startTask = async () => { + if (!isCounting) { + setIsCounting(true); + // 在计时开始时进行物流查询 + await updateLogisticsDetails(); + console.log('使用 EventSource 启动倒计时...'); + + // 启动 SSE 连接 + const eventSource = new EventSource('/api/sse'); + eventSourceRef.current = eventSource; + + // 当 SSE 连接成功时触发 + eventSource.onopen = () => { + console.log('客户端 SSE 连接已打开'); + }; + + // 当收到服务器发送的消息时触发 + eventSource.onmessage = (event) => { + const time = parseInt(event.data, 10); + setRemainingTime(time); + //console.log('接收到事件:', time); + }; + + // 当发生错误时触发 + eventSource.onerror = (error) => { + console.error('SSE 错误:', error); + eventSource.close(); + setIsCounting(false); // 关闭倒计时 + }; + } + }; + + // 本地定时器,每秒更新 UI 中的剩余时间 + useInterval(() => { + if (remainingTime !== null && remainingTime > 0) { + setRemainingTime(remainingTime - 1); + } else if (remainingTime === 0) { + // 移除或注释掉关闭 SSE 连接的代码 + //if (eventSourceRef.current) { + // eventSourceRef.current.close(); // 关闭 SSE 连接 + //} + setIsCounting(false); // 停止倒计时 + startTask(); // 计时结束后自动开始新的一轮查询 + } + }, isCounting ? 1000 : null); + + useEffect(() => { + // 页面加载时检查是否有剩余时间 + if (remainingTime === null) { + // 创建 EventSource 连接以检查服务器端是否有剩余时间 + const eventSource = new EventSource('/api/sse'); + eventSourceRef.current = eventSource; + + eventSource.onmessage = (event) => { + const time = parseInt(event.data, 10); + setRemainingTime(time); + if (time > 0) { + setIsCounting(true); // 如果有剩余时间,则开始倒计时 + } + }; + + eventSource.onerror = (error) => { + console.error('SSE 错误:', error); + eventSource.close(); + }; + + // 检查本地存储或其他持久化方式,恢复上次查询结果 + const savedLastQueryResult = localStorage.getItem('lastQueryResult'); + if (savedLastQueryResult) { + lastQueryResultRef.current = savedLastQueryResult; // 恢复查询结果 + } + + // 在组件卸载时关闭 SSE 连接,防止内存泄漏 + return () => { + if (eventSourceRef.current) { + eventSourceRef.current.close(); + } + }; + } + }, [remainingTime]); + + // 物流查询并保存结果的函数 + const updateLogisticsDetails = async () => { + try { + const response = await fetch('/api/tools/SFExpress/updateLogisticsDetails', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }); + // 处理响应数据 + const data = await response.json(); + message.success('更新物流详情成功'); + //console.log('更新物流详情成功:', data); + lastQueryResultRef.current = data.message; + // 将查询结果保存到 localStorage + localStorage.setItem('lastQueryResult', data.message); + } catch (error) { + message.error('更新物流详情失败'); + //console.error('更新物流详情失败:', error); + } + }; + + // 用于显示在 Popover 中的内容 + /*const popoverContent = ( +
+ {lastQueryResultRef.current ? ( +

{lastQueryResultRef.current}

+ ) : ( +

暂无最新物流详情

+ )} +
+ );*/ + + // 计算按钮显示的文字 + const displayText = isCounting ? `剩余: ${remainingTime} 秒` : '物流查询'; + + return ( + + + + ); +}; + +export default TaskController; diff --git a/src/components/layout/Layout.tsx b/src/components/layout/Layout.tsx index e65fcf5..d7016a1 100644 --- a/src/components/layout/Layout.tsx +++ b/src/components/layout/Layout.tsx @@ -136,13 +136,13 @@ const UserInfoDisplay: React.FC<{ userInfo: any; collapsed?: boolean; }> = React UserInfoDisplay.displayName = 'UserInfoDisplay'; // 菜单底部渲染组件 - 优化版本,使用React.memo -const MenuFooter: React.FC<{ - collapsed?: boolean; - userInfo: any; +const MenuFooter: React.FC<{ + collapsed?: boolean; + userInfo: any; onShowScriptLibrary: () => void; onShowTodoManager: () => void; -}> = React.memo(({ - collapsed, +}> = React.memo(({ + collapsed, userInfo, onShowScriptLibrary, onShowTodoManager @@ -241,7 +241,7 @@ const Layout: React.FC = ({ children }) => { const router = useRouter(); const userInfo = useUserInfo(); const userActions = useUserActions(); - + // 初始化 Socket.IO 连接 useSocket(); const [isClient, setIsClient] = useState(false); @@ -278,7 +278,7 @@ const Layout: React.FC = ({ children }) => { const handleClosePersonalInfo = useCallback(() => { setShowPersonalInfo(false); }, []); - + const handleShowScriptLibrary = useCallback(() => { setShowScriptLibrary(true); }, []); diff --git a/src/components/logistics/status-tag.tsx b/src/components/logistics/status-tag.tsx new file mode 100644 index 0000000..2589ae9 --- /dev/null +++ b/src/components/logistics/status-tag.tsx @@ -0,0 +1,373 @@ +/** + * 物流状态标签组件 + * @author 阿瑞 + * @version 1.0.0 + * @description 显示物流状态标签,支持鼠标悬停查看详细轨迹 + */ + +import React, { useState, useCallback } from 'react'; +import { Tag, Tooltip, Spin, Typography, Timeline } from 'antd'; +import { ClockCircleOutlined, EnvironmentOutlined, TruckOutlined } from '@ant-design/icons'; +import { SF_OPCODE_STATUS_MAP } from '@/types/enum'; + +const { Text } = Typography; + +// ==================== 接口定义 ==================== + +/** + * 物流轨迹节点接口 + */ +interface LogisticsRouteNode { + time: string; + opCode: string; + remark: string; + location?: string; +} + +/** + * 物流详情数据接口 + */ +interface LogisticsDetailData { + trackingNumber: string; + currentStatus: string; + routes: LogisticsRouteNode[]; + lastUpdateTime: string; +} + +/** + * 状态标签组件属性接口 + */ +interface StatusTagProps { + recordId: string; + opCode: string | null; + className?: string; +} + +// ==================== 主组件 ==================== + +/** + * 物流状态标签组件 + * @param props 组件属性 + * @returns 状态标签JSX元素 + */ +const StatusTag: React.FC = ({ recordId, opCode, className }) => { + // ==================== 状态管理 ==================== + + const [loading, setLoading] = useState(false); + const [detailData, setDetailData] = useState(null); + const [error, setError] = useState(''); + + // ==================== 业务逻辑函数 ==================== + + /** + * 获取物流详情数据 + */ + const fetchLogisticsDetail = useCallback(async () => { + if (detailData || loading) return; // 避免重复请求 + + setLoading(true); + setError(''); + + try { + const response = await fetch(`/api/test/logistic-detail?id=${recordId}`); + const result = await response.json(); + + if (result.success && result.data) { + setDetailData(result.data); + } else { + setError(result.message || '获取物流详情失败'); + } + } catch (err) { + console.error('获取物流详情失败:', err); + setError('网络请求失败'); + } finally { + setLoading(false); + } + }, [recordId, detailData, loading]); + + /** + * 渲染物流轨迹内容 + * @returns 轨迹内容JSX元素 + */ + const renderTrackingContent = () => { + if (loading) { + return ( +
+ + + 正在加载物流详情... + +
+ ); + } + + if (error) { + return ( +
+ {error} +
+ ); + } + + if (!detailData) { + return ( +
+ 鼠标悬停查看详情 +
+ ); + } + + return ( +
+ {/* 物流单号标题区域 */} +
+
+
+
+ +
+ + {detailData.trackingNumber} + +
+
+ + + {detailData.lastUpdateTime} + +
+
+
+ + {/* 物流轨迹详情 */} +
+ { + // 获取状态信息 + const statusInfo = SF_OPCODE_STATUS_MAP[route.opCode]; + const isLatest = index === 0; + + return { + color: isLatest ? (statusInfo?.color || '#1890ff') : '#d9d9d9', + dot: isLatest ? ( +
+ ) : undefined, + children: ( +
+ {/* 第一行:状态 地点 时间 最新标签 */} +
+ + {statusInfo?.status || `状态码: ${route.opCode}`} + + {route.location && ( +
+ + {route.location} +
+ )} +
+ {route.time} +
+ {isLatest && ( +
+ 最新 +
+ )} +
+ {/* 第二行:详细备注 */} +
+ {route.remark} +
+
+ ), + }; + })} + /> +
+ + {/* 记录总数提示 */} + {detailData.routes.length > 0 && ( +
+ + 共 {detailData.routes.length} 条物流记录 + +
+ )} +
+ ); + }; + + // ==================== 渲染主标签 ==================== + + /** + * 渲染状态标签 + * @returns 状态标签JSX元素 + */ + const renderStatusTag = () => { + // 处理null值:显示为待发货 + if (opCode === null || opCode === undefined || opCode === '') { + return ( + + 待发货 + + ); + } + + // 使用opCode直接从映射表获取状态信息 + const statusInfo = SF_OPCODE_STATUS_MAP[opCode]; + + if (statusInfo) { + return ( + + {statusInfo.status} + + ); + } + + // 如果opCode不在映射表中,显示原始opCode + return ( + + 状态码: {opCode} + + ); + }; + + // ==================== 渲染组件 ==================== + + return ( + { + if (visible) { + fetchLogisticsDetail(); + } + }} + overlayStyle={{ + maxWidth: 'none', + boxShadow: '0 12px 32px 4px rgba(0, 0, 0, 0.04), 0 8px 20px rgba(0, 0, 0, 0.08)', + borderRadius: '8px', + border: 'none', + padding: '0' + }} + > + + {renderStatusTag()} + + + ); +}; + +export default StatusTag; \ No newline at end of file diff --git a/src/components/logistics/status.tsx b/src/components/logistics/status.tsx index 9df3d4e..32fc7c1 100644 --- a/src/components/logistics/status.tsx +++ b/src/components/logistics/status.tsx @@ -1,8 +1,17 @@ -//src\components\logistics\status.tsx +/** + * 物流状态组件 + * @author 阿瑞 + * @version 1.0.0 + * @description 显示物流状态标签,支持鼠标悬停查看详细轨迹 + */ + import React, { useEffect, useState, useCallback, useRef } from 'react'; -import { Tag, Spin } from 'antd'; -import MyTooltip from '@/components/tooltip/MyTooltip'; -import { debounce } from 'lodash'; // 导入lodash的debounce函数 +import { Tag, Tooltip, Spin, Typography, Timeline } from 'antd'; +import { ClockCircleOutlined, EnvironmentOutlined, TruckOutlined } from '@ant-design/icons'; +import { SF_OPCODE_STATUS_MAP } from '@/types/enum'; +import { debounce } from 'lodash'; + +const { Text } = Typography; interface LogisticsStatusProps { recordId: string; // 关联记录的id @@ -16,18 +25,6 @@ const detailsCache: Record `${recordId}_${productId}`; @@ -37,10 +34,10 @@ const LogisticsStatus: React.FC = React.memo(({ recordId, const [loading, setLoading] = useState(true); const [detailsLoading, setDetailsLoading] = useState(false); const [logisticsNumber, setLogisticsNumber] = useState(null); - + // 使用 ref 来跟踪当前组件是否已挂载,避免在组件卸载后设置状态 const isMountedRef = useRef(true); - + // 使用 ref 来存储当前进行中的请求,用于取消 const currentRequestRef = useRef(null); @@ -53,7 +50,7 @@ const LogisticsStatus: React.FC = React.memo(({ recordId, isMountedRef.current = true; const cacheKey = getCacheKey(recordId, productId); const cachedStatus = statusCache[cacheKey]; - + // 如果缓存存在且未过期,使用缓存 if (cachedStatus && isCacheValid(cachedStatus.timestamp)) { if (isMountedRef.current) { @@ -65,23 +62,23 @@ const LogisticsStatus: React.FC = React.memo(({ recordId, const fetchLogisticsStatus = async () => { if (!isMountedRef.current) return; - + setLoading(true); - + // 取消之前的请求 if (currentRequestRef.current) { currentRequestRef.current.abort(); } - + // 创建新的 AbortController const abortController = new AbortController(); currentRequestRef.current = abortController; - + try { const response = await fetch(`/api/logistics/status?recordId=${recordId}&productId=${productId}`, { signal: abortController.signal }); - + if (!response.ok) { console.error('获取物流状态失败:', `HTTP ${response.status}`); if (isMountedRef.current) { @@ -89,16 +86,16 @@ const LogisticsStatus: React.FC = React.memo(({ recordId, } return; } - + const responseData = await response.json(); const status = responseData.data.物流状态; - + // 更新缓存 statusCache[cacheKey] = { status, timestamp: Date.now() }; - + if (isMountedRef.current) { setLogisticsStatus(status); } @@ -119,13 +116,13 @@ const LogisticsStatus: React.FC = React.memo(({ recordId, }; fetchLogisticsStatus(); - + // 当依赖项变化时,重置物流详情和物流单号 if (isMountedRef.current) { setLogisticsDetails(null); setLogisticsNumber(null); } - + // 清理函数 return () => { if (currentRequestRef.current) { @@ -134,7 +131,7 @@ const LogisticsStatus: React.FC = React.memo(({ recordId, } }; }, [recordId, productId]); - + // 组件卸载时的清理 useEffect(() => { return () => { @@ -149,10 +146,10 @@ const LogisticsStatus: React.FC = React.memo(({ recordId, const fetchLogisticsDetails = useCallback( debounce(async () => { if (!isMountedRef.current) return; - + const cacheKey = getCacheKey(recordId, productId); const cachedDetails = detailsCache[cacheKey]; - + // 如果缓存存在且未过期,使用缓存 if (cachedDetails && isCacheValid(cachedDetails.timestamp)) { if (isMountedRef.current) { @@ -161,14 +158,14 @@ const LogisticsStatus: React.FC = React.memo(({ recordId, } return; } - + if (isMountedRef.current) { setDetailsLoading(true); } - + try { const response = await fetch(`/api/logistics/details?recordId=${recordId}&productId=${productId}`); - + if (!response.ok) { console.error('获取物流详情失败:', `HTTP ${response.status}`); if (isMountedRef.current) { @@ -177,18 +174,18 @@ const LogisticsStatus: React.FC = React.memo(({ recordId, } return; } - + const responseData = await response.json(); const details = responseData[0]?.物流详情 || '暂无物流详情'; const number = responseData[0]?.物流单号 || ''; - + // 更新缓存 detailsCache[cacheKey] = { details, number, timestamp: Date.now() }; - + if (isMountedRef.current) { setLogisticsDetails(details); setLogisticsNumber(number); @@ -208,39 +205,436 @@ const LogisticsStatus: React.FC = React.memo(({ recordId, [recordId, productId] ); - if (loading) { - return ; - } + /** + * 检查字符串是否是有效的JSON格式 + */ + const isValidJSON = (str: string): boolean => { + if (typeof str !== 'string') return false; - if (!logisticsStatus) { - return null; // 返回null而不是div,减少不必要的DOM元素 - } + // 简单检查:JSON应该以 { 或 [ 开头 + const trimmed = str.trim(); + if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) { + return false; + } - return ( - - ) : ( -
- 物流单号:{logisticsNumber} -

{logisticsDetails || '暂无物流详情'}

-
- ) + try { + JSON.parse(str); + return true; + } catch { + return false; + } + }; + + /** + * 解析物流详情JSON并格式化 + */ + const parseLogisticsDetails = () => { + if (!logisticsDetails) return { routes: [], hasValidData: false, lastUpdateTime: '' }; + + try { + let detailData; + + if (typeof logisticsDetails === 'string') { + // 检查是否是有效的JSON字符串 + if (!isValidJSON(logisticsDetails)) { + console.log('物流详情不是JSON格式:', logisticsDetails); + return { routes: [], hasValidData: false, lastUpdateTime: '' }; + } + detailData = JSON.parse(logisticsDetails); + } else { + detailData = logisticsDetails; } - onMouseEnter={fetchLogisticsDetails} - > - + + // 从SF快递API响应中提取路由信息 + if (detailData?.msgData?.routeResps?.length > 0) { + const routeResp = detailData.msgData.routeResps[0]; + const routes = routeResp.routes || []; + + // 获取最新更新时间(从最新的路由记录中获取) + const lastUpdateTime = routes.length > 0 + ? routes[routes.length - 1]?.acceptTime || '' + : ''; + + // 如果有有效的路由数据 + if (routes.length > 0) { + return { + routes: routes.map((route: any) => ({ + time: route.acceptTime || '未知时间', + opCode: route.opCode || '未知', + remark: route.remark || '无备注', + location: route.acceptAddress || undefined + })).reverse(), // 反转数组,让最新的记录在前面 + hasValidData: true, + lastUpdateTime + }; + } else { + // 有响应但routes为空,说明是已创建单但未发货的状态 + return { + routes: [], + hasValidData: true, // 标记为有效数据,只是暂无物流信息 + isEmpty: true, + lastUpdateTime: new Date().toLocaleString('zh-CN', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }).replace(/\//g, '-') + }; + } + } + } catch (parseError) { + console.error('解析物流详情失败:', parseError); + } + + return { routes: [], hasValidData: false, lastUpdateTime: '' }; + }; + + /** + * 渲染物流详情内容 + */ + const renderLogisticsContent = () => { + if (detailsLoading) { + return ( +
+ + + 正在加载物流详情... + +
+ ); + } + + if (!logisticsDetails && !logisticsNumber) { + return ( +
+ 鼠标悬停查看详情 +
+ ); + } + + const { routes: routesData, hasValidData, isEmpty, lastUpdateTime } = parseLogisticsDetails(); + + return ( +
+ {/* 物流单号标题区域 */} +
+
+
+
+ +
+ + {logisticsNumber || '未知单号'} + +
+ {lastUpdateTime && ( +
+ + + {lastUpdateTime} + +
+ )} +
+
+ + {/* 物流轨迹详情 */} +
+ {routesData.length > 0 ? ( + { + // 获取状态信息 + const statusInfo = SF_OPCODE_STATUS_MAP[route.opCode]; + const isLatest = index === 0; + + return { + color: isLatest ? (statusInfo?.color || '#1890ff') : '#d9d9d9', + dot: isLatest ? ( +
+ ) : undefined, + children: ( +
+ {/* 第一行:状态 地点 时间 最新标签 */} +
+ + {statusInfo?.status || `${route.opCode}`} + + {route.location && ( +
+ + {route.location} +
+ )} +
+ {route.time} +
+ {isLatest && ( +
+ 最新 +
+ )} +
+ {/* 第二行:详细备注 */} +
+ {route.remark} +
+
+ ), + }; + })} + /> + ) : ( +
+ {hasValidData && isEmpty ? ( +
+
+ 待发货 +
+
+ 暂无详细物流信息 +
+
+ ) : ( + + 暂无物流详情 + + )} +
+ )} +
+ + {/* 记录总数提示 */} + {routesData.length > 0 && ( +
+ + 共 {routesData.length} 条物流记录 + +
+ )} +
+ ); + }; + + /** + * 渲染状态标签 + */ + const renderStatusTag = () => { + if (loading) { + return ; + } + + if (!logisticsStatus) { + return null; + } + + // 处理null值:显示为待发货 + if (logisticsStatus === null || logisticsStatus === undefined || logisticsStatus === '') { + return ( + + 待发货 + + ); + } + + // 处理API返回的特殊状态 + if (logisticsStatus === '待填单') { + return ( + + 待填单 + + ); + } + + if (logisticsStatus === '待发货') { + return ( + + 待发货 + + ); + } + + // 直接使用opCode从映射表获取状态信息 + const statusInfo = SF_OPCODE_STATUS_MAP[logisticsStatus]; + + if (statusInfo) { + return ( + + {statusInfo.status} + + ); + } + + // 如果opCode不在映射表中,显示原始opCode + return ( + {logisticsStatus} - + ); + }; + + return ( + { + if (visible) { + fetchLogisticsDetails(); + } + }} + overlayStyle={{ + maxWidth: 'none', + boxShadow: '0 12px 32px 4px rgba(0, 0, 0, 0.04), 0 8px 20px rgba(0, 0, 0, 0.08)', + borderRadius: '8px', + border: 'none', + padding: '0' + }} + > + + {renderStatusTag()} + + ); }); diff --git a/src/models/index.ts b/src/models/index.ts index 7fda40b..0081b25 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -363,7 +363,6 @@ const LogisticsRecordSchema: Schema = new Schema({ 物流单号: { type: String }, 是否查询: { type: Boolean, default: true }, // 设置默认值为true 客户尾号: { type: String }, - 物流公司: { type: String }, 物流详情: { type: String }, 物流状态: { type: String }, 更新时间: { type: Date, default: Date.now }, diff --git a/src/models/types.ts b/src/models/types.ts index 032119f..c55ed9d 100644 --- a/src/models/types.ts +++ b/src/models/types.ts @@ -332,7 +332,6 @@ export interface ILogisticsRecord { 物流单号: string; 是否查询: boolean; 客户尾号: string; - 物流公司: string; 物流详情: string; 更新时间: Date; 物流状态: string; diff --git a/src/pages/api/backstage/sales/[id].ts b/src/pages/api/backstage/sales/[id].ts new file mode 100644 index 0000000..7b6105f --- /dev/null +++ b/src/pages/api/backstage/sales/[id].ts @@ -0,0 +1,42 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { SalesRecord } from '@/models'; +import connectDB from '@/utils/connectDB'; + +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + const { query: { id }, method } = req; + + switch (method) { + case 'GET': + try { + const salesRecord = await SalesRecord.findById(id) + .populate('客户') + .populate('产品') + .populate('订单来源') + .populate('导购') + .populate('付款平台'); + if (!salesRecord) { + return res.status(404).json({ message: '未找到销售记录' }); + } + res.status(200).json(salesRecord); + } catch (error) { + res.status(500).json({ message: '服务器错误' }); + } + break; + case 'DELETE': + try { + const deletedSalesRecord = await SalesRecord.findByIdAndDelete(id); + if (!deletedSalesRecord) { + return res.status(404).json({ message: '未找到销售记录' }); + } + res.status(200).json({ message: '销售记录删除成功' }); + } catch (error) { + res.status(500).json({ message: '服务器错误' }); + } + break; + default: + res.setHeader('Allow', ['GET', 'DELETE']); + res.status(405).end(`不允许 ${method} 方法`); + } +}; + +export default connectDB(handler); diff --git a/src/pages/api/backstage/sales/index.ts b/src/pages/api/backstage/sales/index.ts new file mode 100644 index 0000000..c01dc53 --- /dev/null +++ b/src/pages/api/backstage/sales/index.ts @@ -0,0 +1,87 @@ +//src\pages\api\backstage\sales\index.ts +import type { NextApiRequest, NextApiResponse } from 'next'; +import { SalesRecord, Customer } from '@/models'; +import connectDB from '@/utils/connectDB'; +interface CouponUsage { + _id: string; + 券码: string; + 已使用: boolean; + 使用日期: Date | null; +} +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + if (req.method === 'GET') { + try { + const { teamId } = req.query; + const salesRecords = await SalesRecord.find({ 团队: teamId }) + .populate('客户') + .populate('产品') + .populate('订单来源') + .populate('导购') + .populate('付款平台'); + res.status(200).json({ salesRecords }); + } catch (error) { + res.status(500).json({ message: '服务器错误' }); + } + } else if (req.method === 'POST') { + //打印接收到的请求体 + console.log(req.body); + try { + const { 团队, 客户, 产品, 订单来源, 导购, + 应收金额, 收款金额, 待收款, + 成交日期, 收款状态, 货款状态, 收款平台, 备注, + 优惠券, // 从请求中获取优惠券信息 + 余额抵用 // 从请求中获取优惠券信息和余额抵用信息 + } = req.body; + const newSalesRecord = new SalesRecord({ + 团队, + 客户, + 产品, + 订单来源, + 导购, + 应收金额, + 收款金额, + 待收款, + 成交日期, + 收款状态, + 货款状态, + 收款平台, + 备注, + 优惠券: [], // 初始化优惠券字段 + 余额抵用: 余额抵用 || null, // 将余额抵用信息记录到销售记录中 + }); + + // 处理优惠券的使用 + if (优惠券 && 优惠券.length > 0) { + const customer = await Customer.findById(客户); + if (!customer) { + return res.status(404).json({ message: '客户未找到' }); + } + + 优惠券.forEach((couponCode: string) => { + const customerCoupon = customer.优惠券.find((c: CouponUsage) => c.券码 === couponCode); + if (customerCoupon && !customerCoupon.已使用) { + customerCoupon.已使用 = true; + customerCoupon.使用日期 = new Date(); + + // 确保 newSalesRecord.优惠券 是一个有效的数组 + newSalesRecord.优惠券 = newSalesRecord.优惠券 || []; + newSalesRecord.优惠券.push(customerCoupon); // 将已使用的优惠券添加到销售记录中 + } + }); + + await customer.save(); // 保存客户的优惠券状态更新 + } + + await newSalesRecord.save(); + res.status(201).json({ message: '销售记录创建成功', salesRecord: newSalesRecord }); + } catch (error) { + console.error('创建销售记录失败:', error); + res.status(400).json({ message: '创建销售记录失败' }); + } + } else { + res.setHeader('Allow', ['GET', 'POST']); + res.status(405).end(`不允许 ${req.method} 方法`); + } +}; + +export default connectDB(handler); diff --git a/src/pages/api/sse.ts b/src/pages/api/sse.ts new file mode 100644 index 0000000..0cec577 --- /dev/null +++ b/src/pages/api/sse.ts @@ -0,0 +1,52 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; + +// 全局变量,用于存储剩余时间 +let remainingTimeGlobal: number | null = null; +// 全局变量,用于存储计时器的 ID +let intervalIdGlobal: NodeJS.Timeout | null = null; + +export default function handler(req: NextApiRequest, res: NextApiResponse) { + console.log("SSE 连接已打开"); + + // 设置响应头,指定为 SSE 流式传输 + res.setHeader('Content-Type', 'text/event-stream'); + res.setHeader('Cache-Control', 'no-cache'); + res.setHeader('Connection', 'keep-alive'); + res.flushHeaders(); // 立即刷新响应头 + + // 如果全局倒计时未开始,则初始化 + if (remainingTimeGlobal === null) { + remainingTimeGlobal = 600; // 默认30秒倒计时 + } + + // 发送时间更新的函数 + const sendTimeUpdate = () => { + if (remainingTimeGlobal !== null && remainingTimeGlobal >= 0) { + //console.log(`发送更新: ${remainingTimeGlobal}`); + res.write(`data: ${remainingTimeGlobal}\n\n`); // 发送剩余时间给客户端 + remainingTimeGlobal -= 1; + } else { + // 当倒计时结束时清除计时器并关闭 SSE 连接 + /* + if (intervalIdGlobal) clearInterval(intervalIdGlobal); + remainingTimeGlobal = null; + intervalIdGlobal = null; + console.log("SSE 连接已关闭(倒计时结束)"); + res.end(); // 结束 SSE 响应*/ + // 当倒计时结束时重置倒计时,并继续保持连接 + remainingTimeGlobal = 600; // 重置倒计时时间 + } + }; + + // 如果计时器未启动,则启动 + if (!intervalIdGlobal) { + intervalIdGlobal = setInterval(sendTimeUpdate, 1000); + } + + // 立即发送当前的剩余时间 + sendTimeUpdate(); + + req.on('close', () => { + console.log("SSE 连接已关闭(客户端断开连接)"); + }); +} diff --git a/src/pages/api/test/batch-query-logistics.bak b/src/pages/api/test/batch-query-logistics.bak new file mode 100644 index 0000000..eb49ffa --- /dev/null +++ b/src/pages/api/test/batch-query-logistics.bak @@ -0,0 +1,176 @@ +/** + * 批量物流查询API + * @author 阿瑞 + * @version 1.0.0 + * @description 批量查询物流信息并更新数据库记录 + */ + +import type { NextApiRequest, NextApiResponse } from 'next'; +import connectDB from '@/utils/connectDB'; +import { LogisticsRecord } from '@/models'; + +/** + * 批量查询响应数据接口 + */ +interface BatchQueryResponse { + success: boolean; + message?: string; + results?: { + total: number; + successCount: number; + failedCount: number; + details: Array<{ + _id: string; + 物流单号: string; + status: 'success' | 'failed'; + message: string; + newStatus?: string; + }>; + }; + error?: string; +} + +/** + * 处理批量物流查询请求 + * @param req HTTP请求对象 + * @param res HTTP响应对象 + */ +const handler = async ( + req: NextApiRequest, + res: NextApiResponse +) => { + // 只允许POST请求 + if (req.method !== 'POST') { + return res.status(405).json({ + success: false, + message: '只支持POST请求' + }); + } + + try { + // 获取需要查询的记录 + const recordsToQuery = await LogisticsRecord.find({ + 物流单号: { $exists: true, $ne: null, $ne: '' }, + 客户尾号: { $exists: true, $ne: null, $ne: '' }, + 是否查询: true + }).lean(); + + if (!recordsToQuery || recordsToQuery.length === 0) { + return res.status(200).json({ + success: true, + message: '没有需要查询的物流记录', + results: { + total: 0, + successCount: 0, + failedCount: 0, + details: [] + } + }); + } + + console.log(`开始批量查询 ${recordsToQuery.length} 条物流记录`); + + const results = { + total: recordsToQuery.length, + successCount: 0, + failedCount: 0, + details: [] as Array<{ + _id: string; + 物流单号: string; + status: 'success' | 'failed'; + message: string; + newStatus?: string; + }> + }; + + // 逐个查询物流信息 + for (const record of recordsToQuery) { + try { + console.log(`正在查询物流单号: ${record.物流单号}`); + + // 调用SF快递查询API + const queryResponse = await fetch(`${req.headers.origin}/api/test/sf-query`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + trackingNumber: record.物流单号, + phoneLast4Digits: record.客户尾号, + }), + }); + + const queryResult: any = await queryResponse.json(); + + if (queryResult.success && queryResult.msgData?.msgData?.routeResps?.length > 0) { + // 获取最新的opCode(routes数组中的最后一个元素是最新的) + const routes = queryResult.msgData.msgData.routeResps[0].routes; + const latestRoute = routes && routes.length > 0 ? routes[routes.length - 1] : null; + const newStatus = latestRoute?.opCode || '未知'; + + // 更新数据库记录 + await LogisticsRecord.findByIdAndUpdate(record._id, { + 物流状态: newStatus, + 物流详情: JSON.stringify(queryResult.msgData), + 更新时间: new Date(), + }); + + results.successCount++; + results.details.push({ + _id: String(record._id), + 物流单号: String(record.物流单号), + status: 'success', + message: '查询成功并更新', + newStatus: newStatus + }); + + console.log(`✅ ${record.物流单号} 查询成功,状态: ${newStatus}`); + } else { + // 查询失败 + results.failedCount++; + results.details.push({ + _id: String(record._id), + 物流单号: String(record.物流单号), + status: 'failed', + message: queryResult.errorMsg || queryResult.msgData?.errorMsg || '查询失败' + }); + + console.log(`❌ ${record.物流单号} 查询失败`); + } + + // 添加延迟以避免请求过于频繁 + await new Promise(resolve => setTimeout(resolve, 1000)); + + } catch (error) { + console.error(`查询 ${record.物流单号} 时发生错误:`, error); + + results.failedCount++; + results.details.push({ + _id: String(record._id), + 物流单号: String(record.物流单号), + status: 'failed', + message: '网络请求失败' + }); + } + } + + console.log(`批量查询完成,成功: ${results.successCount}, 失败: ${results.failedCount}`); + + return res.status(200).json({ + success: true, + message: `批量查询完成,成功 ${results.successCount} 条,失败 ${results.failedCount} 条`, + results + }); + + } catch (error) { + console.error('批量查询物流信息失败:', error); + + return res.status(500).json({ + success: false, + error: '服务器内部错误', + message: '批量查询失败,请稍后重试' + }); + } +}; + +export default connectDB(handler); \ No newline at end of file diff --git a/src/pages/api/test/batch-query-logistics.ts b/src/pages/api/test/batch-query-logistics.ts new file mode 100644 index 0000000..b94cda0 --- /dev/null +++ b/src/pages/api/test/batch-query-logistics.ts @@ -0,0 +1,227 @@ +/** + * 批量物流查询API + * @author 阿瑞 + * @version 1.0.0 + * @description 批量查询物流信息并更新数据库记录 + */ + +import type { NextApiRequest, NextApiResponse } from 'next'; +import connectDB from '@/utils/connectDB'; +import { LogisticsRecord } from '@/models'; + +/** + * 批量查询响应数据接口 + */ +interface BatchQueryResponse { + success: boolean; + message?: string; + results?: { + total: number; + successCount: number; + failedCount: number; + details: Array<{ + _id: string; + trackingNumber: string; + status: 'success' | 'failed'; + message: string; + newStatus?: string | null; + }>; + }; + error?: string; +} + +/** + * 处理批量物流查询请求 + */ +const handler = async ( + req: NextApiRequest, + res: NextApiResponse +) => { + if (req.method !== 'POST') { + return res.status(405).json({ + success: false, + message: '只支持POST请求' + }); + } + + try { + // 首先处理历史数据:将所有状态为"80"但仍标记为需要查询的记录设置为不需要查询 + const updateResult = await LogisticsRecord.updateMany( + { + 物流状态: '80', + 是否查询: true + }, + { + 是否查询: false, + 更新时间: new Date() + } + ); + + if (updateResult.modifiedCount > 0) { + console.log(`🔧 清理历史数据:${updateResult.modifiedCount} 条已签收记录已设置为不需要查询`); + } + + // 获取需要查询的记录 + const recordsToQuery = await LogisticsRecord.find({ + $and: [ + { 物流单号: { $exists: true, $nin: [null, ''] } }, + { 客户尾号: { $exists: true, $nin: [null, ''] } }, + { 是否查询: true }, + { 物流状态: { $ne: '80' } } // 排除已签收的包裹 + ] + }).lean(); + + if (!recordsToQuery || recordsToQuery.length === 0) { + return res.status(200).json({ + success: true, + message: '没有需要查询的物流记录', + results: { + total: 0, + successCount: 0, + failedCount: 0, + details: [] + } + }); + } + + console.log(`开始批量查询 ${recordsToQuery.length} 条物流记录`); + + // 初始化结果统计 + const batchResults = { + total: recordsToQuery.length, + successCount: 0, + failedCount: 0, + details: [] as Array<{ + _id: string; + trackingNumber: string; + status: 'success' | 'failed'; + message: string; + newStatus?: string | null; + }> + }; + + // 逐个查询物流信息 + for (const record of recordsToQuery) { + try { + const trackingNumber = String(record.物流单号); + const phoneLast4 = String(record.客户尾号); + + console.log(`正在查询物流单号: ${trackingNumber}`); + + // 调用SF快递查询API + const queryResponse = await fetch(`${req.headers.origin}/api/test/sf-query`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + trackingNumber: trackingNumber, + phoneLast4Digits: phoneLast4, + }), + }); + + const queryResult: any = await queryResponse.json(); + + if (queryResult.success && queryResult.msgData?.msgData?.routeResps?.length > 0) { + // 获取最新的opCode(routes数组中的最后一个元素是最新的记录) + const routeResp = queryResult.msgData.msgData.routeResps[0]; + const routes = routeResp.routes; + + if (routes && routes.length > 0) { + // 取最后一个路由记录(最新的) + const latestRoute = routes[routes.length - 1]; + const newOpCode = latestRoute?.opCode || '未知'; + + // 更新数据库记录 + const updateData: any = { + 物流状态: newOpCode, + 物流详情: JSON.stringify(queryResult.msgData), + 更新时间: new Date(), + }; + + // 如果状态为80(已签收),则停止后续查询 + if (newOpCode === '80') { + updateData.是否查询 = false; + } + + await LogisticsRecord.findByIdAndUpdate(record._id, updateData); + + batchResults.successCount++; + batchResults.details.push({ + _id: String(record._id), + trackingNumber: trackingNumber, + status: 'success', + message: '查询成功并更新', + newStatus: newOpCode + }); + + console.log(`✅ ${trackingNumber} 查询成功,最新状态: ${newOpCode}${newOpCode === '80' ? ' [已签收,停止查询]' : ''}`); + } else { + // 查询成功但没有路由信息(可能是无效单号或暂无物流更新) + await LogisticsRecord.findByIdAndUpdate(record._id, { + 物流状态: null, + 物流详情: JSON.stringify(queryResult.msgData), + 更新时间: new Date(), + }); + + batchResults.successCount++; + batchResults.details.push({ + _id: String(record._id), + trackingNumber: trackingNumber, + status: 'success', + message: '查询成功,但暂无物流信息', + newStatus: null + }); + + console.log(`⚠️ ${trackingNumber} 查询成功,但暂无物流信息`); + } + } else { + // 查询失败 + batchResults.failedCount++; + batchResults.details.push({ + _id: String(record._id), + trackingNumber: trackingNumber, + status: 'failed', + message: queryResult.errorMsg || queryResult.msgData?.errorMsg || '查询失败' + }); + + console.log(`❌ ${trackingNumber} 查询失败`); + } + + // 添加延迟以避免请求过于频繁(每次查询间隔1秒) + await new Promise(resolve => setTimeout(resolve, 1000)); + + } catch (error) { + const trackingNumber = String(record.物流单号); + console.error(`查询 ${trackingNumber} 时发生错误:`, error); + + batchResults.failedCount++; + batchResults.details.push({ + _id: String(record._id), + trackingNumber: trackingNumber, + status: 'failed', + message: '网络请求失败' + }); + } + } + + console.log(`批量查询完成,成功: ${batchResults.successCount}, 失败: ${batchResults.failedCount}`); + + return res.status(200).json({ + success: true, + message: `批量查询完成,成功 ${batchResults.successCount} 条,失败 ${batchResults.failedCount} 条`, + results: batchResults + }); + + } catch (error) { + console.error('批量查询物流信息失败:', error); + + return res.status(500).json({ + success: false, + error: '服务器内部错误', + message: '批量查询失败,请稍后重试' + }); + } +}; + +export default connectDB(handler); \ No newline at end of file diff --git a/src/pages/api/test/logistic-detail.ts b/src/pages/api/test/logistic-detail.ts new file mode 100644 index 0000000..f0c2ef8 --- /dev/null +++ b/src/pages/api/test/logistic-detail.ts @@ -0,0 +1,137 @@ +/** + * 物流详情查询API + * @author 阿瑞 + * @version 1.0.0 + * @description 根据记录ID查询物流详细轨迹信息 + */ + +import type { NextApiRequest, NextApiResponse } from 'next'; +import connectDB from '@/utils/connectDB'; +import { LogisticsRecord } from '@/models'; + +/** + * 物流轨迹节点接口 + */ +interface LogisticsRouteNode { + time: string; + opCode: string; + remark: string; + location?: string; +} + +/** + * 物流详情响应接口 + */ +interface LogisticsDetailResponse { + success: boolean; + data?: { + trackingNumber: string; + currentStatus: string; + routes: LogisticsRouteNode[]; + lastUpdateTime: string; + }; + message?: string; + error?: string; +} + +/** + * 处理物流详情查询请求 + * @param req HTTP请求对象 + * @param res HTTP响应对象 + */ +const handler = async ( + req: NextApiRequest, + res: NextApiResponse +) => { + // 只允许GET请求 + if (req.method !== 'GET') { + return res.status(405).json({ + success: false, + message: '只支持GET请求' + }); + } + + try { + const { id } = req.query; + + // 参数验证 + if (!id || typeof id !== 'string') { + return res.status(400).json({ + success: false, + message: '缺少必要参数:记录ID' + }); + } + + // 查询物流记录 + const record = await LogisticsRecord.findById(id) + .select('物流单号 物流状态 物流详情 更新时间') + .lean(); + + if (!record) { + return res.status(404).json({ + success: false, + message: '未找到对应的物流记录' + }); + } + + // 解析物流详情 + let routesData: LogisticsRouteNode[] = []; + const recordData = record as any; + let currentStatus = recordData.物流状态 || '未知'; + + if (recordData.物流详情) { + try { + const logisticsDetail = typeof recordData.物流详情 === 'string' + ? JSON.parse(recordData.物流详情) + : recordData.物流详情; + + // 从SF快递API响应中提取路由信息 + if (logisticsDetail?.msgData?.routeResps?.length > 0) { + const routeResp = logisticsDetail.msgData.routeResps[0]; + const routes = routeResp.routes || []; + + routesData = routes.map((route: any) => ({ + time: route.acceptTime || '未知时间', + opCode: route.opCode || '未知', + remark: route.remark || '无备注', + location: route.acceptAddress || undefined + })).reverse(); // 反转数组,让最新的记录在前面 + } + } catch (parseError) { + console.error('解析物流详情失败:', parseError); + } + } + + // 如果没有路由数据,返回基本信息 + if (routesData.length === 0) { + routesData = [{ + time: recordData.更新时间 ? new Date(recordData.更新时间).toLocaleString('zh-CN') : '未知时间', + opCode: currentStatus, + remark: currentStatus === null ? '包裹尚未发出' : '暂无详细物流信息', + location: undefined + }]; + } + + return res.status(200).json({ + success: true, + data: { + trackingNumber: recordData.物流单号 || '未知', + currentStatus: currentStatus, + routes: routesData, + lastUpdateTime: recordData.更新时间 ? new Date(recordData.更新时间).toLocaleString('zh-CN') : '未知' + }, + message: '获取物流详情成功' + }); + + } catch (error) { + console.error('查询物流详情失败:', error); + + return res.status(500).json({ + success: false, + error: '服务器内部错误', + message: '查询物流详情失败,请稍后重试' + }); + } +}; + +export default connectDB(handler); \ No newline at end of file diff --git a/src/pages/api/test/logistics-records.ts b/src/pages/api/test/logistics-records.ts new file mode 100644 index 0000000..47aef2d --- /dev/null +++ b/src/pages/api/test/logistics-records.ts @@ -0,0 +1,87 @@ +/** + * 物流记录查询API + * @author 阿瑞 + * @version 1.0.0 + * @description 获取所有物流记录数据的API接口 + */ + +import type { NextApiRequest, NextApiResponse } from 'next'; +import connectDB from '@/utils/connectDB'; +import { LogisticsRecord } from '@/models'; + +/** + * 物流记录响应数据接口 + */ +interface LogisticsRecordResponse { + success: boolean; + data?: any[]; + total?: number; + message?: string; + error?: string; +} + +/** + * 处理物流记录查询请求 + * @param req HTTP请求对象 + * @param res HTTP响应对象 + */ +const handler = async ( + req: NextApiRequest, + res: NextApiResponse +) => { + // 只允许GET请求 + if (req.method !== 'GET') { + return res.status(405).json({ + success: false, + message: '只支持GET请求' + }); + } + + try { + // 获取查询参数 + const { page = 1, pageSize = 20 } = req.query; + const skip = (Number(page) - 1) * Number(pageSize); + + // 查询物流记录数据 + const records = await LogisticsRecord.find({}) + .select('物流单号 客户尾号 是否查询 物流状态 物流详情 更新时间 类型 createdAt') // 只选择需要的字段 + .sort({ 更新时间: -1 }) // 按更新时间倒序 + .skip(skip) + .limit(Number(pageSize)) + .lean(); // 使用lean()提高查询性能 + + // 获取总记录数 + const total = await LogisticsRecord.countDocuments({}); + + // 格式化返回数据 + const formattedRecords = records.map((record: any) => ({ + _id: record._id, + 物流单号: record.物流单号, + 客户尾号: record.客户尾号, + 是否查询: record.是否查询, + 物流状态: record.物流状态, + 物流详情: record.物流详情, + 更新时间: record.更新时间, + 类型: record.类型, + createdAt: record.createdAt, + })); + + return res.status(200).json({ + success: true, + data: formattedRecords, + total, + message: '获取物流记录成功' + }); + + } catch (error) { + console.error('获取物流记录失败:', error); + + return res.status(500).json({ + success: false, + error: '服务器内部错误', + message: '获取物流记录失败,请稍后重试' + }); + } +}; + +export default connectDB(handler); \ No newline at end of file diff --git a/src/pages/api/test/sf-query.ts b/src/pages/api/test/sf-query.ts new file mode 100644 index 0000000..af47c7c --- /dev/null +++ b/src/pages/api/test/sf-query.ts @@ -0,0 +1,201 @@ +/** + * SF快递物流查询测试API + * @author 阿瑞 + * @version 1.0.0 + * @description 专门用于测试SF快递物流查询功能的API接口 + */ + +import type { NextApiRequest, NextApiResponse } from 'next'; +import querystring from 'querystring'; +import { getAccessToken } from '@/utils/getAccessToken'; + +// ==================== 接口定义 ==================== + +/** + * 查询请求参数接口 + */ +interface QueryRequest { + trackingNumber: string; + phoneLast4Digits: string; +} + +/** + * SF快递API响应接口 + */ +interface SFApiResponse { + success: boolean; + errorCode?: string; + errorMsg?: string; + apiResultCode?: string; + apiErrorMsg?: string; + apiResponseID?: string; + msgData?: any; +} + +// ==================== 主处理函数 ==================== + +/** + * API请求处理函数 + * @param req NextJS API请求对象 + * @param res NextJS API响应对象 + */ +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + // 只允许POST请求 + if (req.method !== 'POST') { + return res.status(405).json({ + success: false, + errorMsg: '只支持POST请求方法' + }); + } + + try { + // ==================== 参数验证 ==================== + + const { trackingNumber, phoneLast4Digits }: QueryRequest = req.body; + + // 验证必需参数 + if (!trackingNumber || !phoneLast4Digits) { + return res.status(400).json({ + success: false, + errorMsg: '缺少必需参数:trackingNumber 和 phoneLast4Digits 都不能为空' + }); + } + + // 验证运单号格式 + if (typeof trackingNumber !== 'string' || trackingNumber.trim().length < 8) { + return res.status(400).json({ + success: false, + errorMsg: '运单号格式错误:长度至少为8位' + }); + } + + // 验证手机号后4位格式 + if (typeof phoneLast4Digits !== 'string' || !/^\d{4}$/.test(phoneLast4Digits)) { + return res.status(400).json({ + success: false, + errorMsg: '手机号后4位格式错误:必须是4位数字' + }); + } + + console.log('开始查询SF快递物流信息:', { trackingNumber, phoneLast4Digits }); + + // ==================== 获取访问令牌 ==================== + + let accessToken: string; + try { + accessToken = await getAccessToken(); + console.log('成功获取访问令牌'); + } catch (tokenError) { + console.error('获取访问令牌失败:', tokenError); + return res.status(500).json({ + success: false, + errorMsg: '获取访问令牌失败,请检查API配置' + }); + } + + // ==================== 构建查询请求 ==================== + + const partnerID = process.env.PARTNER_ID || 'YPD607MO'; + const reqUrl = 'https://bspgw.sf-express.com/std/service'; + const requestID = crypto.randomUUID(); + const serviceCode = "EXP_RECE_SEARCH_ROUTES"; + const timestamp = Date.now().toString(); + + // 构建消息数据对象 + const msgData = { + language: "zh-CN", + trackingType: "1", + trackingNumber: [trackingNumber.trim()], + methodType: "1", + checkPhoneNo: phoneLast4Digits.trim() + }; + + console.log('构建的查询参数:', { + partnerID, + requestID, + serviceCode, + timestamp, + msgData + }); + + // 构建请求体数据 + const requestData = querystring.stringify({ + partnerID, + requestID, + serviceCode, + timestamp, + accessToken, + msgData: JSON.stringify(msgData) + }); + + // ==================== 调用SF快递API ==================== + + const response = await fetch(reqUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'User-Agent': 'SF-Express-Query-Test' + }, + body: requestData + }); + + if (!response.ok) { + console.error('SF API HTTP错误:', response.status, response.statusText); + return res.status(500).json({ + success: false, + errorMsg: `SF快递API请求失败: HTTP ${response.status}` + }); + } + + const responseData = await response.json(); + console.log('SF API原始响应:', JSON.stringify(responseData, null, 2)); + + // ==================== 处理API响应 ==================== + + // 检查API级别的错误 + if (responseData.apiResultCode && responseData.apiResultCode !== 'A1000') { + console.error('SF API业务错误:', responseData); + return res.status(400).json({ + success: false, + errorCode: responseData.apiResultCode, + errorMsg: responseData.apiErrorMsg || '未知的SF快递API错误', + apiResponseID: responseData.apiResponseID, + rawResponse: responseData + }); + } + + // 解析业务数据 + let parsedMsgData = null; + if (responseData.apiResultData) { + try { + parsedMsgData = JSON.parse(responseData.apiResultData); + } catch (parseError) { + console.error('解析msgData失败:', parseError); + return res.status(500).json({ + success: false, + errorMsg: '解析物流数据失败' + }); + } + } + + // ==================== 格式化响应数据 ==================== + + const result: SFApiResponse = { + success: true, + msgData: parsedMsgData, + apiResponseID: responseData.apiResponseID + }; + + console.log('查询成功,返回结果'); + res.status(200).json(result); + + } catch (error: any) { + console.error('SF快递查询过程中发生错误:', error); + res.status(500).json({ + success: false, + errorMsg: error.message || '服务器内部错误' + }); + } +}; + +export default handler; \ No newline at end of file diff --git a/src/pages/api/tools/test4.ts b/src/pages/api/tools/test4.ts deleted file mode 100644 index 7b9aed7..0000000 --- a/src/pages/api/tools/test4.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { NextApiRequest, NextApiResponse } from 'next'; -import connectDB from '@/utils/connectDB'; - -//const extractInfoAPI = 'http://192.168.1.8:8006/extract_info/'; -//const parseLocationAPI = 'http://192.168.1.8:8000/parse_location/'; -// 从环境变量获取 API 的 URL -const extractInfoAPI = process.env.EXTRACT_INFO_API_URL || ''; -const parseLocationAPI = process.env.PARSE_LOCATION_API_URL || ''; - -const handler = async (req: NextApiRequest, res: NextApiResponse) => { - if (req.method !== 'POST') { - return res.status(405).json({ error: 'Method not allowed' }); - } - - try { - const { text } = req.body; - - // 1. 使用 extract_info API 提取信息 - const extractResponse = await fetch(extractInfoAPI, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ text }) - }); - - if (!extractResponse.ok) { - throw new Error(`Extract info API failed: ${extractResponse.status}`); - } - - const extractData = await extractResponse.json(); - const extractedInfo = extractData.extracted_info[0]; - - // 提取信息失败时抛出异常 - if (!extractedInfo) { - throw new Error('Failed to extract information'); - } - - // 2. 将城市、县区、详细地址拼接为完整地址 - const city = extractedInfo['城市'][0]?.text || ''; - const county = extractedInfo['县区'][0]?.text || ''; - const detailAddress = extractedInfo['详细地址'][0]?.text || ''; - const fullAddress = `${city}${county}${detailAddress}`; - - // 3. 使用 parse_location API 解析完整地址 - const parseResponse = await fetch(parseLocationAPI, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ text: fullAddress }) - }); - - if (!parseResponse.ok) { - throw new Error(`Parse location API failed: ${parseResponse.status}`); - } - - const parsedLocation = await parseResponse.json(); - - // 4. 返回组合后的结果 - const result = { - ...extractedInfo, - 解析地址: parsedLocation, - }; - - res.status(200).json(result); - } catch (error) { - console.error('Error in processing request:', error); - res.status(500).json({ error: 'Internal Server Error' }); - } -}; - -export default connectDB(handler); diff --git a/src/pages/backstage/accounts/account-modal.tsx b/src/pages/backstage/accounts/account-modal.tsx new file mode 100644 index 0000000..229222c --- /dev/null +++ b/src/pages/backstage/accounts/account-modal.tsx @@ -0,0 +1,162 @@ +import React, { useEffect, useState } from 'react'; +import { Modal, Form, Input, message, Select } from 'antd'; +import axios from 'axios'; +import { useUserInfo } from '@/store/userStore'; // 使用 Zustand 获取用户信息 +import { IAccount, IUser, ICategory } from '@/models/types'; + +interface AccountModalProps { + visible: boolean; + onCancel: () => void; + onOk: () => void; + account: IAccount | null; +} + +const AccountModal: React.FC = ({ visible, onOk, onCancel, account }) => { + const [form] = Form.useForm(); + const [users, setUsers] = useState([]); + const [categories, setCategories] = useState([]); + const userInfo = useUserInfo(); // 获取当前用户信息 + + /*useEffect(() => { + fetchUsersAndCategories(userInfo.团队?._id); + if (account) { + form.setFieldsValue({ + 账号编号: account.账号编号, + 微信号: account.微信号, + 微信昵称: account.微信昵称, + 手机编号: account.手机编号, + 账号状态: account.账号状态, + 备注: account.备注, + 账号负责人: account.账号负责人?._id, + 前端引流人员: account.前端引流人员?._id, + 账号类型: account.账号类型?.map(type => type._id), + }); + } else { + form.resetFields(); + } + }, [account, form, userInfo.团队]);*/ + useEffect(() => { + const teamId = userInfo.团队?._id; + if (teamId) { + fetchUsersAndCategories(teamId); + } + if (account) { + form.setFieldsValue({ + 账号编号: account.账号编号, + 微信号: account.微信号, + 微信昵称: account.微信昵称, + 手机编号: account.手机编号, + 账号状态: account.账号状态, + 备注: account.备注, + 账号负责人: account.账号负责人?._id, + 前端引流人员: account.前端引流人员?._id, + 账号类型: account.账号类型?.map(type => type._id), + }); + } else { + form.resetFields(); + } + }, [account, form, userInfo.团队]); + + const fetchUsersAndCategories = async (teamId: string) => { + try { + const [usersResponse, categoriesResponse] = await Promise.all([ + axios.get(`/api/backstage/users?teamId=${teamId}`), + axios.get(`/api/backstage/categories?teamId=${teamId}`) + ]); + setUsers(usersResponse.data.users); + setCategories(categoriesResponse.data.categories); + } catch (error) { + message.error('加载用户或品类数据失败'); + } + }; + + const handleSubmit = async () => { + try { + const values = await form.validateFields(); + const method = account ? 'PUT' : 'POST'; + const url = account ? `/api/backstage/accounts/${account._id}` : '/api/backstage/accounts'; + + await axios({ + method, + url, + data: { + ...values, + 团队: userInfo.团队?._id, + }, + }); + + message.success('店铺账号操作成功'); + onOk(); + } catch (error) { + console.error('Validate Failed:', error); + message.error('店铺账号操作失败'); + } + }; + + return ( + { + form.resetFields(); + onCancel(); + }} + > +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ ); +}; + +export default AccountModal; diff --git a/src/pages/backstage/accounts/index.tsx b/src/pages/backstage/accounts/index.tsx new file mode 100644 index 0000000..0055c32 --- /dev/null +++ b/src/pages/backstage/accounts/index.tsx @@ -0,0 +1,251 @@ +import React, { useState, useEffect, useMemo } from 'react'; +import { Table, Button, message, Card, Tag, Typography, Popconfirm } from 'antd'; +import axios from 'axios'; +import AccountModal from './account-modal'; // 引入店铺账号模态框组件 +import { IAccount, ICategory } from '@/models/types'; // 确保有正确的类型定义 +import { useUserInfo } from '@/store/userStore'; // 使用 Zustand 获取用户信息 +import { ColumnsType } from 'antd/es/table'; +import { ShopOutlined, WechatOutlined } from '@ant-design/icons'; +import { Icon } from '@iconify/react'; +import { IconButton, Iconify } from '@/components/icon'; +const { Text } = Typography; + +const AccountsPage = () => { + const [accounts, setAccounts] = useState([]); + const [isModalVisible, setIsModalVisible] = useState(false); + const [currentAccount, setCurrentAccount] = useState(null); + const userInfo = useUserInfo(); // 获取当前用户信息 + + useEffect(() => { + if (userInfo.团队?._id) { + fetchAccounts(userInfo.团队._id); + } + }, [userInfo]); + + const fetchAccounts = async (teamId: string) => { + try { + const { data } = await axios.get(`/api/backstage/accounts?teamId=${teamId}`); + setAccounts(data.accounts); + } catch (error) { + message.error('加载店铺账号数据失败'); + } + }; + + const handleModalOk = () => { + setIsModalVisible(false); + if (userInfo.团队?._id) { + fetchAccounts(userInfo.团队._id); + } + }; + + const handleEdit = (account: IAccount) => { + setCurrentAccount(account); + setIsModalVisible(true); + }; + + const handleDelete = async (id: string) => { + try { + await axios.delete(`/api/backstage/accounts/${id}`); + if (userInfo.团队?._id) { + fetchAccounts(userInfo.团队._id); + } + message.success('店铺账号删除成功'); + } catch (error) { + message.error('删除店铺账号失败'); + } + }; + + // 从所有账户中提取所有唯一的类别名称 + const uniqueCategories = Array.from(new Set( + accounts.flatMap(acc => acc.账号类型?.map(cat => cat.name) ?? []) + )); + // 将这些类别转换成筛选所需的格式 + const categoryFilters = uniqueCategories.map(cat => ({ + text: cat, + value: cat, + })); + + const columns: ColumnsType = useMemo(() => [ + //序号 + { + title: '序号', + dataIndex: '序号', + width: 60, + key: 'index', + render: (_text: any, _record: any, index: number) => index + 1, + }, + { + title: '账号编号', + dataIndex: '账号编号', + width: 100, // 定义列宽度 + align: 'center', + key: '账号编号', + render: (text: string) => ( + + + {text} + + + ), + }, + { + title: '账号资料', + dataIndex: 'name', + width: 160, + render: (_, record) => { + return ( +
+ +
+ + {record.微信昵称} + + + } color="success">{record.微信号} + +
+
+ ); + }, + }, + { + title: '账号类型', + dataIndex: '账号类型', + filters: categoryFilters, + onFilter: (value: any, record: IAccount) => record.账号类型?.some((cat: ICategory) => cat.name === value) ?? false, + render: (categories: ICategory[]) => ( + <> + {categories.map((category: ICategory) => ( + +
+ {category.icon && ( + + )} + {category.name} +
+
+ ))} + + ) + }, + { + title: '手机编号', + dataIndex: '手机编号', + key: 'phoneNumber', + }, + { + title: '账号状态', + dataIndex: '账号状态', + key: 'status', + render: (status: number) => ( + + {status === 1 ? '正常' : status === 0 ? '停用' : '异常'} + + ) + }, + { + title: '账号负责人', + dataIndex: '账号负责人', + align: 'center', + width: 100, // 定义列宽度 + key: '账号负责人', + render: (账号负责人: any) => ( +
{账号负责人?.姓名 ?? 账号负责人}
// 如果归属人是个对象,则显示realname,否则直接显示归属人(ID) + ), + }, + { + title: '前端引流人员', + dataIndex: '前端引流人员', + align: 'center', + width: 120, // 定义列宽度 + key: '前端引流人员', + //filters: frontendHandlersFilters, + //onFilter: (value, record) => record.前端引流人员?.realname === value, + render: (前端引流人员: any) => ( +
{前端引流人员?.姓名 ?? '未知'}
+ ), + }, + //备注 + { + title: '备注', + dataIndex: '备注', + key: 'remark', + }, + { + title: '操作', + key: 'action', + /*render: (_: any, record: IAccount) => ( + + + + + ),*/ + width: 90, + align: 'center', + render: (_: any, record: IAccount) => ( +
+ handleEdit(record)}> + + + handleDelete(record._id)} + okText="是" + cancelText="否" + placement="left" + > + + + + +
+ ), + }, + ], [accounts]); + + return ( + { setIsModalVisible(true); setCurrentAccount(null); }}> + 添加账号 + + } + > + + {isModalVisible && ( + setIsModalVisible(false)} + account={currentAccount} + /> + )} + + ); +}; + +export default AccountsPage; diff --git a/src/pages/team/sale/index.tsx b/src/pages/team/sale/index.tsx index 8dec1df..af60318 100644 --- a/src/pages/team/sale/index.tsx +++ b/src/pages/team/sale/index.tsx @@ -1,4 +1,9 @@ -//src\pages\team\sale\index.tsx +/** + * 文件: team/sale/index.tsx + * 作者: 阿瑞 + * 功能: 销售记录页面 - 创建和管理销售记录 + * 版本: v2.0.0 - 使用 fetch 替换 axios,修复搜索过滤错误 + */ import React, { useEffect, useState } from 'react'; import { Form, Input, Select, DatePicker, Button, Row, Col, Card, Divider, Tag, App } from 'antd'; import { ICustomer, ICustomerCoupon, IProduct, ISalesRecord } from '@/models/types'; @@ -482,10 +487,10 @@ const SalesRecordPage: React.FC = ({ salesRecord, onCancel const account = accounts.find(acc => acc._id === option?.value); if (!account) return false; const { 微信号, 微信昵称 } = account; - return ( - 微信号.toLowerCase().includes(input.toLowerCase()) || - 微信昵称.toLowerCase().includes(input.toLowerCase()) - ); + const inputLower = input.toLowerCase(); + const wechatIdMatch = 微信号 ? 微信号.toLowerCase().includes(inputLower) : false; + const wechatNameMatch = 微信昵称 ? 微信昵称.toLowerCase().includes(inputLower) : false; + return wechatIdMatch || wechatNameMatch; }} > {accounts.map(account => ( diff --git a/src/pages/test/sf.tsx b/src/pages/test/sf.tsx new file mode 100644 index 0000000..d7fa463 --- /dev/null +++ b/src/pages/test/sf.tsx @@ -0,0 +1,439 @@ +/** + * 物流查询测试页面 + * @author 阿瑞 + * @version 1.0.0 + * @description SF快递物流查询功能测试页面,用于测试运单号查询功能 + */ + +import { useState } from 'react'; +import { Card, Form, Input, Button, Alert, Timeline, Spin, Typography, Tag } from 'antd'; +import { SearchOutlined, TruckOutlined, PhoneOutlined, CodeOutlined, SyncOutlined } from '@ant-design/icons'; +import { SF_OPCODE_STATUS_MAP, type SFExpressStatusInfo } from '@/types/enum'; + +const { Title, Text } = Typography; + +// ==================== 接口定义 ==================== + +/** + * 物流查询表单数据接口 + */ +interface QueryFormData { + trackingNumber: string; + phoneLast4Digits: string; +} + +/** + * 物流路由信息接口 + */ +interface RouteInfo { + opCode: string; + remark: string; + acceptTime: string; + acceptAddress?: string; +} + +/** + * 查询响应数据接口 + */ +interface QueryResponse { + success: boolean; + errorCode?: string; + errorMsg?: string; + apiErrorMsg?: string; + apiResponseID?: string; + msgData?: { + success?: boolean; + errorCode?: string; + errorMsg?: string; + msgData?: { + routeResps?: Array<{ + mailNo: string; + routes?: RouteInfo[]; + }>; + }; + }; +} + +// ==================== 主组件 ==================== + +/** + * SF快递物流查询测试页面组件 + * @returns 物流查询页面JSX元素 + */ +const SFExpressQueryPage: React.FC = () => { + // ==================== 状态管理 ==================== + + const [form] = Form.useForm(); + const [loading, setLoading] = useState(false); + const [queryResult, setQueryResult] = useState(null); + const [error, setError] = useState(''); + + // ==================== 业务逻辑函数 ==================== + + /** + * 根据opCode获取状态信息 + * @param opCode SF快递opCode + * @returns 状态信息对象 + */ + const getStatusByOpCode = (opCode: string): SFExpressStatusInfo => { + return SF_OPCODE_STATUS_MAP[opCode] || { status: '处理中', color: 'default', description: '快件正在处理中' }; + }; + + /** + * 获取当前物流的整体状态 + * @param routes 物流路由信息数组 + * @returns 状态信息对象 + */ + const getOverallStatus = (routes: RouteInfo[]): { status: string; color: string } => { + if (!routes || routes.length === 0) { + return { status: '暂无信息', color: 'default' }; + } + + // 取最新的路由节点状态 + const latestRoute = routes[0]; + const statusInfo = getStatusByOpCode(latestRoute.opCode); + + return { status: statusInfo.status, color: statusInfo.color }; + }; + + /** + * 处理物流查询提交 + * @param values 表单数据 + */ + const handleQuery = async (values: QueryFormData) => { + setLoading(true); + setError(''); + setQueryResult(null); + + try { + const response = await fetch('/api/test/sf-query', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + trackingNumber: values.trackingNumber, + phoneLast4Digits: values.phoneLast4Digits, + }), + }); + + const result = await response.json(); + console.log('查询结果:', result); + + setQueryResult(result); + + if (!result.success) { + setError(result.errorMsg || '查询失败,请稍后重试'); + } + } catch (err) { + console.error('查询过程中发生错误:', err); + setError('网络请求失败,请检查网络连接或稍后重试'); + } finally { + setLoading(false); + } + }; + + /** + * 重置查询表单和结果 + */ + const handleReset = () => { + form.resetFields(); + setQueryResult(null); + setError(''); + }; + + // ==================== 渲染函数 ==================== + + /** + * 渲染物流时间轴组件 + * @param routes 物流路由信息数组 + * @returns 时间轴JSX元素 + */ + const renderTimelineItems = (routes: RouteInfo[]) => { + return routes.map((route, index) => { + const statusInfo = getStatusByOpCode(route.opCode); + + // 时间轴节点颜色 + const timelineColor = index === 0 ? 'green' : + statusInfo.color === 'success' ? 'green' : + statusInfo.color === 'error' || statusInfo.color === 'red' ? 'red' : + statusInfo.color === 'warning' ? 'orange' : 'blue'; + + return { + color: timelineColor, + children: ( +
+
+
+
+ + {statusInfo.status} + + + + opCode: {route.opCode} + +
+ + + {route.remark} + + + {route.acceptAddress && ( + + 📍 {route.acceptAddress} + + )} +
+ +
+ + {route.acceptTime} + +
+
+
+ ), + }; + }); + }; + + return ( +
+
+ {/* ==================== 页面标题 ==================== */} +
+
+ + <TruckOutlined className="mr-2 text-blue-600" /> + SF快递物流查询 + + + 输入运单号和手机号后4位查询物流信息 + +
+
+ + {/* ==================== 上半部分:左1/3查询表单+运单信息,右2/3开发者数据 ==================== */} +
+ {/* 左侧1/3:查询表单和运单信息 */} +
+ {/* 查询表单 */} + +
+ + } + /> + + + + } + maxLength={4} + /> + + + +
+ + +
+
+ +
+ + {/* 运单信息和当前状态 */} + {queryResult?.success && queryResult.msgData?.msgData?.routeResps?.length && queryResult.msgData.msgData.routeResps.length > 0 && ( + + {queryResult.msgData?.msgData?.routeResps?.map((routeResp: any, index: number) => { + const overallStatus = getOverallStatus(routeResp.routes?.slice().reverse() || []); + + return ( +
+
+ + {routeResp.mailNo} + + + SF快递运单 + +
+ +
+ 当前状态 + + {overallStatus.status} + +
+ +
+
+ + {routeResp.routes?.length || 0} + + + 物流节点 + +
+
+
+ ); + })} +
+ )} + + {/* 错误信息显示 */} + {error && ( + + )} +
+ + {/* 右侧2/3:开发者数据 */} +
+ {queryResult && !loading && ( + +
+ + + 开发者数据 ({JSON.stringify(queryResult).length} 字符) + +
+
+
+                    {JSON.stringify(queryResult, null, 2)}
+                  
+
+
+ )} + + {/* 加载状态 */} + {loading && ( + +
+ +
+ + 正在查询物流信息 + + + 请稍等片刻... + +
+
+
+ )} + + {/* 右侧占位内容 */} + {!queryResult && !loading && ( + +
+ + 查询后将在此处显示开发者数据 +
+
+ )} +
+
+ + {/* ==================== 下半部分:物流轨迹 ==================== */} + {queryResult?.success && queryResult.msgData?.msgData?.routeResps?.length && queryResult.msgData.msgData.routeResps.length > 0 && !loading && ( +
+ + {queryResult.msgData?.msgData?.routeResps?.map((routeResp: any, index: number) => ( +
+ {routeResp.routes?.length > 0 ? ( + + ) : ( + + )} +
+ ))} +
+
+ )} + + {/* 查询失败时的错误信息 */} + {queryResult && !loading && !queryResult.success && ( + + + + )} + + {/* 无查询结果 */} + {queryResult?.success && !queryResult.msgData?.msgData?.routeResps?.length && !loading && ( + + + + )} +
+
+ ); +}; + +export default SFExpressQueryPage; diff --git a/src/pages/test/sfall.tsx b/src/pages/test/sfall.tsx new file mode 100644 index 0000000..c588a9d --- /dev/null +++ b/src/pages/test/sfall.tsx @@ -0,0 +1,402 @@ +/** + * 物流记录管理页面 + * @author 阿瑞 + * @version 1.0.0 + * @description 显示所有物流记录的管理页面,包含物流单号、客户尾号、查询状态、物流状态等信息 + */ + +import { useState, useEffect } from 'react'; +import { Card, Table, Tag, Typography, Alert, Button, Progress, Modal, List } from 'antd'; +import { TruckOutlined, ReloadOutlined, ThunderboltOutlined } from '@ant-design/icons'; +import StatusTag from '@/components/logistics/status-tag'; +import type { ColumnsType } from 'antd/es/table'; + +const { Title, Text } = Typography; + +// ==================== 接口定义 ==================== + +/** + * 物流记录数据接口 + */ +interface LogisticsRecordData { + _id: string; + 物流单号: string; + 客户尾号: string; + 是否查询: boolean; + 物流状态: string; + 物流详情: any; + 更新时间: string; + 类型: string; + createdAt: string; +} + +/** + * API响应数据接口 + */ +interface ApiResponse { + success: boolean; + data?: LogisticsRecordData[]; + total?: number; + message?: string; + error?: string; +} + +// ==================== 主组件 ==================== + +/** + * 物流记录管理页面组件 + * @returns 物流记录管理页面JSX元素 + */ +const LogisticsRecordsPage: React.FC = () => { + // ==================== 状态管理 ==================== + + const [loading, setLoading] = useState(false); + const [records, setRecords] = useState([]); + const [total, setTotal] = useState(0); + const [error, setError] = useState(''); + const [pagination, setPagination] = useState({ + current: 1, + pageSize: 20, + }); + + // 批量查询相关状态 + const [batchLoading, setBatchLoading] = useState(false); + const [batchModalVisible, setBatchModalVisible] = useState(false); + const [batchResults, setBatchResults] = useState(null); + + // ==================== 业务逻辑函数 ==================== + + /** + * 获取物流记录数据 + * @param page 页码 + * @param pageSize 每页数量 + */ + const fetchLogisticsRecords = async (page: number = 1, pageSize: number = 20) => { + setLoading(true); + setError(''); + + try { + const response = await fetch(`/api/test/logistics-records?page=${page}&pageSize=${pageSize}`); + const result: ApiResponse = await response.json(); + + if (result.success && result.data) { + setRecords(result.data); + setTotal(result.total || 0); + } else { + setError(result.message || result.error || '获取数据失败'); + } + } catch (err) { + console.error('获取物流记录失败:', err); + setError('网络请求失败,请检查网络连接'); + } finally { + setLoading(false); + } + }; + + /** + * 处理表格分页变化 + * @param page 页码 + * @param pageSize 每页数量 + */ + const handleTableChange = (page: number, pageSize?: number) => { + const newPagination = { + current: page, + pageSize: pageSize || pagination.pageSize, + }; + setPagination(newPagination); + fetchLogisticsRecords(page, pageSize || pagination.pageSize); + }; + + /** + * 刷新数据 + */ + const handleRefresh = () => { + fetchLogisticsRecords(pagination.current, pagination.pageSize); + }; + + + + /** + * 批量查询物流信息 + */ + const handleBatchQuery = async () => { + setBatchLoading(true); + setBatchResults(null); + + try { + const response = await fetch('/api/test/batch-query-logistics', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }); + + const result = await response.json(); + + if (result.success) { + setBatchResults(result.results); + setBatchModalVisible(true); + // 批量查询完成后刷新列表 + fetchLogisticsRecords(pagination.current, pagination.pageSize); + } else { + setError(result.message || '批量查询失败'); + } + } catch (err) { + console.error('批量查询失败:', err); + setError('网络请求失败,请检查网络连接'); + } finally { + setBatchLoading(false); + } + }; + + // ==================== 表格列定义 ==================== + + const columns: ColumnsType = [ + { + title: '物流单号', + dataIndex: '物流单号', + key: '物流单号', + width: 180, + render: (text: string) => ( + + {text || '未填写'} + + ), + }, + { + title: '客户尾号', + dataIndex: '客户尾号', + key: '客户尾号', + width: 100, + align: 'center', + render: (text: string) => ( + + {text || '-'} + + ), + }, + { + title: '是否查询', + dataIndex: '是否查询', + key: '是否查询', + width: 100, + align: 'center', + render: (isQueried: boolean) => ( + + {isQueried ? '已查询' : '未查询'} + + ), + }, + { + title: '物流状态', + dataIndex: '物流状态', + key: '物流状态', + width: 120, + render: (opCode: string | null, record: LogisticsRecordData) => { + return ( + + ); + }, + }, + ]; + + // ==================== 生命周期 ==================== + + useEffect(() => { + fetchLogisticsRecords(); + }, []); + + // ==================== 渲染组件 ==================== + + return ( +
+
+ {/* ==================== 页面标题 ==================== */} +
+
+
+ + <TruckOutlined className="mr-2 text-blue-600" /> + 物流记录管理 + + + 查看和管理所有物流记录信息 + +
+ +
+
+ + {/* ==================== 主要内容区域 ==================== */} +
+ {/* 左侧2/3:物流记录列表 */} +
+ + {/* 错误信息显示 */} + {error && ( + + )} + + {/* 数据统计 */} +
+
+ + 共 {total} 条记录 + +
+
+ + {/* 物流记录表格 */} + + columns={columns} + dataSource={records} + rowKey="_id" + loading={loading} + pagination={{ + current: pagination.current, + pageSize: pagination.pageSize, + total: total, + showSizeChanger: true, + showQuickJumper: true, + showTotal: (total, range) => + `第 ${range[0]}-${range[1]} 条,共 ${total} 条`, + onChange: handleTableChange, + onShowSizeChange: handleTableChange, + }} + scroll={{ x: 600 }} + size="middle" + /> +
+
+ + {/* 右侧1/3:批量查询功能 */} +
+ +
+
+ + 批量查询物流 + + 自动查询所有物流记录的最新状态 + +
+ +
+
+ 查询规则: +
+
• 只查询有物流单号的记录
+
• 只查询有客户尾号的记录
+
• 只查询标记为"需要查询"的记录
+
• 逐个查询,避免并发请求
+
+
+ + + + {batchLoading && ( +
+ + 正在查询中,请耐心等待... + + +
+ )} +
+
+
+
+
+ + {/* 批量查询结果弹窗 */} + setBatchModalVisible(false)} + footer={[ + + ]} + width={600} + > + {batchResults && ( +
+
+
+
+ {batchResults.total} + 总数 +
+
+ {batchResults.successCount} + 成功 +
+
+ {batchResults.failedCount} + 失败 +
+
+
+ + ( + +
+
+ {item.trackingNumber} +
{item.message}
+
+
+ {item.newStatus && ( + {item.newStatus} + )} + + {item.status === 'success' ? '成功' : '失败'} + +
+
+
+ )} + size="small" + style={{ maxHeight: '400px', overflow: 'auto' }} + /> +
+ )} +
+
+
+ ); +}; + +export default LogisticsRecordsPage; diff --git a/src/types/enum.ts b/src/types/enum.ts index e204798..43e4c54 100644 --- a/src/types/enum.ts +++ b/src/types/enum.ts @@ -53,8 +53,90 @@ export enum BasicStatus { } export enum PermissionType { - CATALOGUE, - MENU, - BUTTON, - } + CATALOGUE, + MENU, + BUTTON, +} + +// ==================== SF快递物流状态映射 ==================== + +/** + * SF快递状态信息接口 + * @author 阿瑞 + * @version 1.0.0 + */ +export interface SFExpressStatusInfo { + status: string; + color: string; + description: string; +} + +/** + * SF快递opCode状态映射表 + * @description 基于真实SF快递物流数据分析得出的opCode对应状态 + * @author 阿瑞 + * @version 1.0.0 + */ +export const SF_OPCODE_STATUS_MAP: Record = { + // ========== 揽收相关 ========== + '54': { status: '已揽收', color: 'blue', description: '快件已被顺丰收取' }, + '43': { status: '已收取', color: 'blue', description: '顺丰速运已收取快件' }, + + // ========== 分拣运输相关 ========== + '30': { status: '分拣中', color: 'processing', description: '快件正在分拣处理' }, + '36': { status: '运输中', color: 'processing', description: '快件正在运输途中' }, + '31': { status: '到达网点', color: 'processing', description: '快件已到达转运中心' }, + '302': { status: '长途运输', color: 'processing', description: '快件正在安全运输中(长距离)' }, + '310': { status: '途经中转', color: 'processing', description: '快件途经中转城市' }, + '570': { status: '铁路运输', color: 'processing', description: '快件通过铁路运输,已发车' }, + + // ========== 派送相关 ========== + '44': { status: '准备派送', color: 'processing', description: '正在为快件分配合适的快递员' }, + '204': { status: '派送中', color: 'processing', description: '快件已交给快递员,正在派送途中' }, + '70': { status: '派送失败', color: 'warning', description: '快件派送不成功,待再次派送' }, + '33': { status: '待派送', color: 'warning', description: '已与客户约定新派送时间,待派送' }, + + // ========== 退回相关 ========== + '517': { status: '退回申请中', color: 'warning', description: '快件退回申请已提交,正在处理中' }, + '99': { status: '退回中', color: 'warning', description: '应客户要求,快件正在退回中' }, + + // ========== 签收相关 ========== + '80': { status: '已签收', color: 'success', description: '快件已成功签收(含正常签收和退回签收)' }, +} as const; + +/** + * SF快递状态颜色映射表 + * @description 将状态字符串映射到对应的UI颜色 + */ +export const SF_STATUS_COLOR_MAP: Record = { + '待揽收': 'default', + '已揽收': 'blue', + '已收取': 'blue', + '分拣中': 'processing', + '运输中': 'processing', + '铁路运输': 'processing', + '到达网点': 'processing', + '长途运输': 'processing', + '途经中转': 'processing', + '准备派送': 'processing', + '派送中': 'processing', + '派送失败': 'warning', + '待派送': 'warning', + '退回申请中': 'warning', + '退回中': 'warning', + '已签收': 'success', +} as const; + +/** + * SF快递状态图标映射表 + * @description 将状态颜色映射到对应的emoji图标 + */ +export const SF_STATUS_ICON_MAP: Record = { + 'success': '✅', + 'error': '❌', + 'warning': '⚠️', + 'processing': '🔄', + 'blue': '📦', + 'default': '⏳', +} as const; \ No newline at end of file