This commit is contained in:
365
docs/sf-express-logistics-status-guide.md
Normal file
365
docs/sf-express-logistics-status-guide.md
Normal file
@@ -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
|
||||
// 状态标签
|
||||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${colorClass}`}>
|
||||
{status}
|
||||
</span>
|
||||
|
||||
// 详细信息
|
||||
<div className="flex flex-wrap gap-4 text-sm text-gray-500">
|
||||
<span>📍 {acceptAddress}</span>
|
||||
<span>🔢 opCode: {opCode}</span>
|
||||
<span>📊 一级状态: {firstStatusName}</span>
|
||||
<span>📋 二级状态: {secondaryStatusName}</span>
|
||||
</div>
|
||||
```
|
||||
|
||||
## 📊 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<string, string> = {
|
||||
'待揽收': '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
|
||||
170
src/components/TaskController.tsx
Normal file
170
src/components/TaskController.tsx
Normal file
@@ -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<number | null>(null);
|
||||
// 是否正在计时的状态
|
||||
const [isCounting, setIsCounting] = useState<boolean>(false);
|
||||
// 用于存储 EventSource 的引用
|
||||
const eventSourceRef = useRef<EventSource | null>(null);
|
||||
const lastQueryResultRef = useRef<string | null>(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 = (
|
||||
<div>
|
||||
{lastQueryResultRef.current ? (
|
||||
<p>{lastQueryResultRef.current}</p>
|
||||
) : (
|
||||
<p>暂无最新物流详情</p>
|
||||
)}
|
||||
</div>
|
||||
);*/
|
||||
|
||||
// 计算按钮显示的文字
|
||||
const displayText = isCounting ? `剩余: ${remainingTime} 秒` : '物流查询';
|
||||
|
||||
return (
|
||||
<Popover
|
||||
//content={popoverContent}
|
||||
//title="最新物流详情"
|
||||
placement="top"
|
||||
>
|
||||
<Button
|
||||
className="text-white bg-blue-500 hover:bg-blue-700"
|
||||
type="primary"
|
||||
icon={<PoweroffOutlined />}
|
||||
loading={isCounting} // 按钮加载状态取决于是否在计时中
|
||||
onClick={startTask} // 点击按钮启动任务
|
||||
disabled={isCounting || (remainingTime !== null && remainingTime > 0)} // 在计时中或剩余时间大于0时禁用按钮
|
||||
>
|
||||
{displayText}
|
||||
</Button>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskController;
|
||||
373
src/components/logistics/status-tag.tsx
Normal file
373
src/components/logistics/status-tag.tsx
Normal file
@@ -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<StatusTagProps> = ({ recordId, opCode, className }) => {
|
||||
// ==================== 状态管理 ====================
|
||||
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [detailData, setDetailData] = useState<LogisticsDetailData | null>(null);
|
||||
const [error, setError] = useState<string>('');
|
||||
|
||||
// ==================== 业务逻辑函数 ====================
|
||||
|
||||
/**
|
||||
* 获取物流详情数据
|
||||
*/
|
||||
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 (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '24px',
|
||||
minWidth: '200px'
|
||||
}}>
|
||||
<Spin size="small" />
|
||||
<Text type="secondary" style={{ marginLeft: '8px', fontSize: '13px' }}>
|
||||
正在加载物流详情...
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
padding: '16px',
|
||||
minWidth: '200px'
|
||||
}}>
|
||||
<Text type="danger" style={{ fontSize: '13px' }}>{error}</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!detailData) {
|
||||
return (
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
padding: '16px',
|
||||
minWidth: '200px'
|
||||
}}>
|
||||
<Text type="secondary" style={{ fontSize: '13px' }}>鼠标悬停查看详情</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
minWidth: '380px',
|
||||
maxWidth: '450px',
|
||||
background: '#fff',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #e8e8e8',
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
{/* 物流单号标题区域 */}
|
||||
<div style={{
|
||||
background: 'linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%)',
|
||||
padding: '12px 20px',
|
||||
borderBottom: '1px solid #e8e8e8'
|
||||
}}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between'
|
||||
}}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '10px'
|
||||
}}>
|
||||
<div style={{
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
background: '#1890ff',
|
||||
borderRadius: '6px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
<TruckOutlined style={{ color: '#fff', fontSize: '12px' }} />
|
||||
</div>
|
||||
<Text strong style={{
|
||||
fontSize: '14px',
|
||||
color: '#262626',
|
||||
letterSpacing: '0.3px'
|
||||
}}>
|
||||
{detailData.trackingNumber}
|
||||
</Text>
|
||||
</div>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px'
|
||||
}}>
|
||||
<ClockCircleOutlined style={{ color: '#8c8c8c', fontSize: '10px' }} />
|
||||
<Text type="secondary" style={{
|
||||
fontSize: '11px',
|
||||
color: '#8c8c8c'
|
||||
}}>
|
||||
{detailData.lastUpdateTime}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 物流轨迹详情 */}
|
||||
<div style={{
|
||||
maxHeight: '380px',
|
||||
overflowY: 'auto',
|
||||
padding: '12px 20px 8px'
|
||||
}}>
|
||||
<Timeline
|
||||
items={detailData.routes.map((route, index) => {
|
||||
// 获取状态信息
|
||||
const statusInfo = SF_OPCODE_STATUS_MAP[route.opCode];
|
||||
const isLatest = index === 0;
|
||||
|
||||
return {
|
||||
color: isLatest ? (statusInfo?.color || '#1890ff') : '#d9d9d9',
|
||||
dot: isLatest ? (
|
||||
<div style={{
|
||||
width: '12px',
|
||||
height: '12px',
|
||||
background: statusInfo?.color === 'success' ? '#52c41a' : '#1890ff',
|
||||
borderRadius: '50%',
|
||||
border: '2px solid #fff',
|
||||
boxShadow: '0 0 0 2px #e6f7ff'
|
||||
}} />
|
||||
) : undefined,
|
||||
children: (
|
||||
<div style={{
|
||||
marginBottom: '12px',
|
||||
paddingLeft: '4px'
|
||||
}}>
|
||||
{/* 第一行:状态 地点 时间 最新标签 */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
marginBottom: '4px',
|
||||
flexWrap: 'wrap'
|
||||
}}>
|
||||
<Text strong style={{
|
||||
color: isLatest ? '#262626' : '#595959',
|
||||
fontSize: '12px',
|
||||
fontWeight: isLatest ? 600 : 500
|
||||
}}>
|
||||
{statusInfo?.status || `状态码: ${route.opCode}`}
|
||||
</Text>
|
||||
{route.location && (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '2px',
|
||||
color: '#8c8c8c',
|
||||
fontSize: '11px'
|
||||
}}>
|
||||
<EnvironmentOutlined style={{ fontSize: '9px' }} />
|
||||
<span>{route.location}</span>
|
||||
</div>
|
||||
)}
|
||||
<div style={{
|
||||
color: '#bfbfbf',
|
||||
fontSize: '11px',
|
||||
fontFamily: 'monospace'
|
||||
}}>
|
||||
{route.time}
|
||||
</div>
|
||||
{isLatest && (
|
||||
<div style={{
|
||||
background: 'linear-gradient(135deg, #1890ff, #36cfc9)',
|
||||
color: '#fff',
|
||||
fontSize: '9px',
|
||||
padding: '1px 5px',
|
||||
borderRadius: '8px',
|
||||
fontWeight: 500,
|
||||
lineHeight: '1.2',
|
||||
marginLeft: 'auto'
|
||||
}}>
|
||||
最新
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* 第二行:详细备注 */}
|
||||
<div style={{
|
||||
color: '#595959',
|
||||
lineHeight: '1.4',
|
||||
fontSize: '11px',
|
||||
paddingRight: '20px'
|
||||
}}>
|
||||
{route.remark}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 记录总数提示 */}
|
||||
{detailData.routes.length > 0 && (
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
marginTop: '4px',
|
||||
padding: '12px 20px',
|
||||
background: '#fafafa',
|
||||
borderTop: '1px solid #f0f0f0'
|
||||
}}>
|
||||
<Text type="secondary" style={{
|
||||
fontSize: '11px',
|
||||
color: '#8c8c8c',
|
||||
fontWeight: 500
|
||||
}}>
|
||||
共 {detailData.routes.length} 条物流记录
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// ==================== 渲染主标签 ====================
|
||||
|
||||
/**
|
||||
* 渲染状态标签
|
||||
* @returns 状态标签JSX元素
|
||||
*/
|
||||
const renderStatusTag = () => {
|
||||
// 处理null值:显示为待发货
|
||||
if (opCode === null || opCode === undefined || opCode === '') {
|
||||
return (
|
||||
<Tag color="default" className={className}>
|
||||
待发货
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
|
||||
// 使用opCode直接从映射表获取状态信息
|
||||
const statusInfo = SF_OPCODE_STATUS_MAP[opCode];
|
||||
|
||||
if (statusInfo) {
|
||||
return (
|
||||
<Tag color={statusInfo.color} className={className}>
|
||||
{statusInfo.status}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
|
||||
// 如果opCode不在映射表中,显示原始opCode
|
||||
return (
|
||||
<Tag color="default" className={className}>
|
||||
状态码: {opCode}
|
||||
</Tag>
|
||||
);
|
||||
};
|
||||
|
||||
// ==================== 渲染组件 ====================
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
color="white"
|
||||
title={renderTrackingContent()}
|
||||
placement="left"
|
||||
trigger="hover"
|
||||
onOpenChange={(visible) => {
|
||||
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'
|
||||
}}
|
||||
>
|
||||
<span className="cursor-pointer">
|
||||
{renderStatusTag()}
|
||||
</span>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatusTag;
|
||||
@@ -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<string, { details: string, number: string, timestamp:
|
||||
// 缓存过期时间(毫秒)
|
||||
const CACHE_EXPIRY = 5 * 60 * 1000; // 5分钟
|
||||
|
||||
const statusColorMapping: { [key: string]: string } = {
|
||||
'已退回': 'red',
|
||||
'退回中': 'orange',
|
||||
'已拒收': 'volcano',
|
||||
'已签收': 'green',
|
||||
'派送中': 'blue',
|
||||
'已发出': 'geekblue',
|
||||
'已揽收': 'cyan',
|
||||
'处理中': 'grey',
|
||||
'待发货': 'magenta'
|
||||
};
|
||||
|
||||
// 生成缓存键
|
||||
const getCacheKey = (recordId: string, productId: string) => `${recordId}_${productId}`;
|
||||
|
||||
@@ -208,39 +205,436 @@ const LogisticsStatus: React.FC<LogisticsStatusProps> = React.memo(({ recordId,
|
||||
[recordId, productId]
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
return <Spin size="small" />;
|
||||
}
|
||||
/**
|
||||
* 检查字符串是否是有效的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 (
|
||||
<MyTooltip
|
||||
color="white"
|
||||
title={
|
||||
detailsLoading ? (
|
||||
<Spin size="small" />
|
||||
) : (
|
||||
<div>
|
||||
<span style={{ fontSize: 'larger' }}>物流单号:{logisticsNumber}</span>
|
||||
<p>{logisticsDetails || '暂无物流详情'}</p>
|
||||
</div>
|
||||
)
|
||||
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}
|
||||
>
|
||||
<Tag
|
||||
style={{
|
||||
fontSize: '10px',
|
||||
}}
|
||||
bordered={false}
|
||||
color={statusColorMapping[logisticsStatus] || 'default'}
|
||||
>
|
||||
|
||||
// 从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 (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '24px',
|
||||
minWidth: '200px'
|
||||
}}>
|
||||
<Spin size="small" />
|
||||
<Text type="secondary" style={{ marginLeft: '8px', fontSize: '13px' }}>
|
||||
正在加载物流详情...
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!logisticsDetails && !logisticsNumber) {
|
||||
return (
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
padding: '16px',
|
||||
minWidth: '200px'
|
||||
}}>
|
||||
<Text type="secondary" style={{ fontSize: '13px' }}>鼠标悬停查看详情</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const { routes: routesData, hasValidData, isEmpty, lastUpdateTime } = parseLogisticsDetails();
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
minWidth: '380px',
|
||||
maxWidth: '450px',
|
||||
background: '#fff',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #e8e8e8',
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
{/* 物流单号标题区域 */}
|
||||
<div style={{
|
||||
background: 'linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%)',
|
||||
padding: '12px 20px',
|
||||
borderBottom: '1px solid #e8e8e8'
|
||||
}}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between'
|
||||
}}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '10px'
|
||||
}}>
|
||||
<div style={{
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
background: '#1890ff',
|
||||
borderRadius: '6px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
<TruckOutlined style={{ color: '#fff', fontSize: '12px' }} />
|
||||
</div>
|
||||
<Text strong style={{
|
||||
fontSize: '14px',
|
||||
color: '#262626',
|
||||
letterSpacing: '0.3px'
|
||||
}}>
|
||||
{logisticsNumber || '未知单号'}
|
||||
</Text>
|
||||
</div>
|
||||
{lastUpdateTime && (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px'
|
||||
}}>
|
||||
<ClockCircleOutlined style={{ color: '#8c8c8c', fontSize: '10px' }} />
|
||||
<Text type="secondary" style={{
|
||||
fontSize: '11px',
|
||||
color: '#8c8c8c'
|
||||
}}>
|
||||
{lastUpdateTime}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 物流轨迹详情 */}
|
||||
<div style={{
|
||||
maxHeight: '380px',
|
||||
overflowY: 'auto',
|
||||
padding: '12px 20px 8px'
|
||||
}}>
|
||||
{routesData.length > 0 ? (
|
||||
<Timeline
|
||||
items={routesData.map((route: any, index: number) => {
|
||||
// 获取状态信息
|
||||
const statusInfo = SF_OPCODE_STATUS_MAP[route.opCode];
|
||||
const isLatest = index === 0;
|
||||
|
||||
return {
|
||||
color: isLatest ? (statusInfo?.color || '#1890ff') : '#d9d9d9',
|
||||
dot: isLatest ? (
|
||||
<div style={{
|
||||
width: '12px',
|
||||
height: '12px',
|
||||
background: statusInfo?.color === 'success' ? '#52c41a' : '#1890ff',
|
||||
borderRadius: '50%',
|
||||
border: '2px solid #fff',
|
||||
boxShadow: '0 0 0 2px #e6f7ff'
|
||||
}} />
|
||||
) : undefined,
|
||||
children: (
|
||||
<div style={{
|
||||
marginBottom: '12px',
|
||||
paddingLeft: '4px'
|
||||
}}>
|
||||
{/* 第一行:状态 地点 时间 最新标签 */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
marginBottom: '4px',
|
||||
flexWrap: 'wrap'
|
||||
}}>
|
||||
<Text strong style={{
|
||||
color: isLatest ? '#262626' : '#595959',
|
||||
fontSize: '12px',
|
||||
fontWeight: isLatest ? 600 : 500
|
||||
}}>
|
||||
{statusInfo?.status || `${route.opCode}`}
|
||||
</Text>
|
||||
{route.location && (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '2px',
|
||||
color: '#8c8c8c',
|
||||
fontSize: '11px'
|
||||
}}>
|
||||
<EnvironmentOutlined style={{ fontSize: '9px' }} />
|
||||
<span>{route.location}</span>
|
||||
</div>
|
||||
)}
|
||||
<div style={{
|
||||
color: '#bfbfbf',
|
||||
fontSize: '11px',
|
||||
fontFamily: 'monospace'
|
||||
}}>
|
||||
{route.time}
|
||||
</div>
|
||||
{isLatest && (
|
||||
<div style={{
|
||||
background: 'linear-gradient(135deg, #1890ff, #36cfc9)',
|
||||
color: '#fff',
|
||||
fontSize: '9px',
|
||||
padding: '1px 5px',
|
||||
borderRadius: '8px',
|
||||
fontWeight: 500,
|
||||
lineHeight: '1.2',
|
||||
marginLeft: 'auto'
|
||||
}}>
|
||||
最新
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* 第二行:详细备注 */}
|
||||
<div style={{
|
||||
color: '#595959',
|
||||
lineHeight: '1.4',
|
||||
fontSize: '11px',
|
||||
paddingRight: '20px'
|
||||
}}>
|
||||
{route.remark}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
})}
|
||||
/>
|
||||
) : (
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
padding: '20px',
|
||||
color: '#8c8c8c'
|
||||
}}>
|
||||
{hasValidData && isEmpty ? (
|
||||
<div>
|
||||
<div style={{
|
||||
fontSize: '13px',
|
||||
marginBottom: '8px',
|
||||
fontWeight: 500
|
||||
}}>
|
||||
待发货
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: '11px',
|
||||
color: '#bfbfbf'
|
||||
}}>
|
||||
暂无详细物流信息
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Text style={{
|
||||
fontSize: '12px',
|
||||
color: '#595959',
|
||||
lineHeight: '1.5',
|
||||
display: 'block'
|
||||
}}>
|
||||
暂无物流详情
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 记录总数提示 */}
|
||||
{routesData.length > 0 && (
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
marginTop: '4px',
|
||||
padding: '12px 20px',
|
||||
background: '#fafafa',
|
||||
borderTop: '1px solid #f0f0f0'
|
||||
}}>
|
||||
<Text type="secondary" style={{
|
||||
fontSize: '11px',
|
||||
color: '#8c8c8c',
|
||||
fontWeight: 500
|
||||
}}>
|
||||
共 {routesData.length} 条物流记录
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 渲染状态标签
|
||||
*/
|
||||
const renderStatusTag = () => {
|
||||
if (loading) {
|
||||
return <Spin size="small" />;
|
||||
}
|
||||
|
||||
if (!logisticsStatus) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 处理null值:显示为待发货
|
||||
if (logisticsStatus === null || logisticsStatus === undefined || logisticsStatus === '') {
|
||||
return (
|
||||
<Tag
|
||||
style={{
|
||||
fontSize: '10px',
|
||||
color: '#fa8c16',
|
||||
backgroundColor: 'transparent',
|
||||
borderColor: '#fa8c16'
|
||||
}}
|
||||
bordered={true}
|
||||
>
|
||||
待发货
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
|
||||
// 处理API返回的特殊状态
|
||||
if (logisticsStatus === '待填单') {
|
||||
return (
|
||||
<Tag color="volcano" style={{ fontSize: '10px' }} bordered={false}>
|
||||
待填单
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
|
||||
if (logisticsStatus === '待发货') {
|
||||
return (
|
||||
<Tag
|
||||
style={{
|
||||
fontSize: '10px',
|
||||
color: '#fa8c16',
|
||||
backgroundColor: 'transparent',
|
||||
borderColor: '#fa8c16'
|
||||
}}
|
||||
bordered={true}
|
||||
>
|
||||
待发货
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
|
||||
// 直接使用opCode从映射表获取状态信息
|
||||
const statusInfo = SF_OPCODE_STATUS_MAP[logisticsStatus];
|
||||
|
||||
if (statusInfo) {
|
||||
return (
|
||||
<Tag color={statusInfo.color} style={{ fontSize: '10px' }} bordered={false}>
|
||||
{statusInfo.status}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
|
||||
// 如果opCode不在映射表中,显示原始opCode
|
||||
return (
|
||||
<Tag color="default" style={{ fontSize: '10px' }} bordered={false}>
|
||||
{logisticsStatus}
|
||||
</Tag>
|
||||
</MyTooltip>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
color="white"
|
||||
title={renderLogisticsContent()}
|
||||
placement="left"
|
||||
trigger="hover"
|
||||
onOpenChange={(visible) => {
|
||||
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'
|
||||
}}
|
||||
>
|
||||
<span className="cursor-pointer">
|
||||
{renderStatusTag()}
|
||||
</span>
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -332,7 +332,6 @@ export interface ILogisticsRecord {
|
||||
物流单号: string;
|
||||
是否查询: boolean;
|
||||
客户尾号: string;
|
||||
物流公司: string;
|
||||
物流详情: string;
|
||||
更新时间: Date;
|
||||
物流状态: string;
|
||||
|
||||
42
src/pages/api/backstage/sales/[id].ts
Normal file
42
src/pages/api/backstage/sales/[id].ts
Normal file
@@ -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);
|
||||
87
src/pages/api/backstage/sales/index.ts
Normal file
87
src/pages/api/backstage/sales/index.ts
Normal file
@@ -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);
|
||||
52
src/pages/api/sse.ts
Normal file
52
src/pages/api/sse.ts
Normal file
@@ -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 连接已关闭(客户端断开连接)");
|
||||
});
|
||||
}
|
||||
176
src/pages/api/test/batch-query-logistics.bak
Normal file
176
src/pages/api/test/batch-query-logistics.bak
Normal file
@@ -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<BatchQueryResponse>
|
||||
) => {
|
||||
// 只允许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);
|
||||
227
src/pages/api/test/batch-query-logistics.ts
Normal file
227
src/pages/api/test/batch-query-logistics.ts
Normal file
@@ -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<BatchQueryResponse>
|
||||
) => {
|
||||
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);
|
||||
137
src/pages/api/test/logistic-detail.ts
Normal file
137
src/pages/api/test/logistic-detail.ts
Normal file
@@ -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<LogisticsDetailResponse>
|
||||
) => {
|
||||
// 只允许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);
|
||||
87
src/pages/api/test/logistics-records.ts
Normal file
87
src/pages/api/test/logistics-records.ts
Normal file
@@ -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<LogisticsRecordResponse>
|
||||
) => {
|
||||
// 只允许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);
|
||||
201
src/pages/api/test/sf-query.ts
Normal file
201
src/pages/api/test/sf-query.ts
Normal file
@@ -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;
|
||||
@@ -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);
|
||||
162
src/pages/backstage/accounts/account-modal.tsx
Normal file
162
src/pages/backstage/accounts/account-modal.tsx
Normal file
@@ -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<AccountModalProps> = ({ visible, onOk, onCancel, account }) => {
|
||||
const [form] = Form.useForm();
|
||||
const [users, setUsers] = useState<IUser[]>([]);
|
||||
const [categories, setCategories] = useState<ICategory[]>([]);
|
||||
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 (
|
||||
<Modal
|
||||
title={account ? '编辑账号' : '添加账号'}
|
||||
open={visible}
|
||||
onOk={handleSubmit}
|
||||
onCancel={() => {
|
||||
form.resetFields();
|
||||
onCancel();
|
||||
}}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item name="账号编号" label="账号编号" rules={[{ required: true, message: '请输入账号编号!' }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="微信号" label="微信号">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="微信昵称" label="微信昵称">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="手机编号" label="手机编号">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="账号状态" label="账号状态" rules={[{ required: true, message: '请选择账号状态!' }]}>
|
||||
<Select>
|
||||
<Select.Option value={1}>正常</Select.Option>
|
||||
<Select.Option value={0}>停用</Select.Option>
|
||||
<Select.Option value={2}>异常</Select.Option>
|
||||
<Select.Option value={3}>备用</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item name="账号负责人" label="账号负责人" rules={[{ required: true, message: '请选择账号负责人!' }]}>
|
||||
<Select placeholder="请选择账号负责人">
|
||||
{users.map(user => (
|
||||
<Select.Option key={user._id} value={user._id}>
|
||||
{user.姓名}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item name="前端引流人员" label="前端引流人员">
|
||||
<Select placeholder="请选择前端引流人员">
|
||||
{users.map(user => (
|
||||
<Select.Option key={user._id} value={user._id}>
|
||||
{user.姓名}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item name="账号类型" label="账号类型">
|
||||
<Select mode="multiple" placeholder="请选择账号类型">
|
||||
{categories.map(category => (
|
||||
<Select.Option key={category._id} value={category._id}>
|
||||
{category.name}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item name="备注" label="备注">
|
||||
<Input.TextArea />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccountModal;
|
||||
251
src/pages/backstage/accounts/index.tsx
Normal file
251
src/pages/backstage/accounts/index.tsx
Normal file
@@ -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<IAccount[]>([]);
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
const [currentAccount, setCurrentAccount] = useState<IAccount | null>(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<IAccount> = 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 strong style={{
|
||||
fontWeight: 'bold',
|
||||
fontSize: '1.2em',
|
||||
}}>
|
||||
<span style={{
|
||||
display: 'inline-block',
|
||||
width: '32px',
|
||||
height: '32px',
|
||||
borderRadius: '50%',
|
||||
border: '2px solid #000',
|
||||
textAlign: 'center',
|
||||
lineHeight: '26px',
|
||||
fontWeight: 'bold',
|
||||
}}>
|
||||
{text}
|
||||
</span>
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '账号资料',
|
||||
dataIndex: 'name',
|
||||
width: 160,
|
||||
render: (_, record) => {
|
||||
return (
|
||||
<div className="flex">
|
||||
<img alt="" src={record.头像} className="h-10 w-10 rounded-full" />
|
||||
<div className="ml-2 flex flex-col">
|
||||
<span className="text-sm">
|
||||
<ShopOutlined /> {record.微信昵称}
|
||||
</span>
|
||||
<span className="text-sm">
|
||||
<Tag icon={<WechatOutlined />} color="success">{record.微信号}</Tag>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '账号类型',
|
||||
dataIndex: '账号类型',
|
||||
filters: categoryFilters,
|
||||
onFilter: (value: any, record: IAccount) => record.账号类型?.some((cat: ICategory) => cat.name === value) ?? false,
|
||||
render: (categories: ICategory[]) => (
|
||||
<>
|
||||
{categories.map((category: ICategory) => (
|
||||
<Tag color="blue" key={category._id}>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
{category.icon && (
|
||||
<Icon icon={category.icon} width={16} height={16} style={{ marginRight: 8 }} />
|
||||
)}
|
||||
{category.name}
|
||||
</div>
|
||||
</Tag>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '手机编号',
|
||||
dataIndex: '手机编号',
|
||||
key: 'phoneNumber',
|
||||
},
|
||||
{
|
||||
title: '账号状态',
|
||||
dataIndex: '账号状态',
|
||||
key: 'status',
|
||||
render: (status: number) => (
|
||||
<Tag color={status === 1 ? 'green' : status === 0 ? 'red' : 'orange'}>
|
||||
{status === 1 ? '正常' : status === 0 ? '停用' : '异常'}
|
||||
</Tag>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '账号负责人',
|
||||
dataIndex: '账号负责人',
|
||||
align: 'center',
|
||||
width: 100, // 定义列宽度
|
||||
key: '账号负责人',
|
||||
render: (账号负责人: any) => (
|
||||
<div>{账号负责人?.姓名 ?? 账号负责人}</div> // 如果归属人是个对象,则显示realname,否则直接显示归属人(ID)
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '前端引流人员',
|
||||
dataIndex: '前端引流人员',
|
||||
align: 'center',
|
||||
width: 120, // 定义列宽度
|
||||
key: '前端引流人员',
|
||||
//filters: frontendHandlersFilters,
|
||||
//onFilter: (value, record) => record.前端引流人员?.realname === value,
|
||||
render: (前端引流人员: any) => (
|
||||
<div>{前端引流人员?.姓名 ?? '未知'}</div>
|
||||
),
|
||||
},
|
||||
//备注
|
||||
{
|
||||
title: '备注',
|
||||
dataIndex: '备注',
|
||||
key: 'remark',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
/*render: (_: any, record: IAccount) => (
|
||||
<Space size="middle">
|
||||
<Button onClick={() => handleEdit(record)}>编辑</Button>
|
||||
<Button onClick={() => handleDelete(record._id)}>删除</Button>
|
||||
</Space>
|
||||
),*/
|
||||
width: 90,
|
||||
align: 'center',
|
||||
render: (_: any, record: IAccount) => (
|
||||
<div className="flex w-full justify-center">
|
||||
<IconButton onClick={() => handleEdit(record)}>
|
||||
<Iconify icon="solar:pen-bold-duotone" size={18} />
|
||||
</IconButton>
|
||||
<Popconfirm
|
||||
title="确定要删除吗?"
|
||||
onConfirm={() => handleDelete(record._id)}
|
||||
okText="是"
|
||||
cancelText="否"
|
||||
placement="left"
|
||||
>
|
||||
<IconButton>
|
||||
<Iconify icon="mingcute:delete-2-fill" size={18} className="text-error" />
|
||||
</IconButton>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
], [accounts]);
|
||||
|
||||
return (
|
||||
<Card
|
||||
title="店铺账号列表"
|
||||
extra={
|
||||
<Button type="primary" onClick={() => { setIsModalVisible(true); setCurrentAccount(null); }}>
|
||||
添加账号
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Table
|
||||
sticky
|
||||
scroll={{
|
||||
y: `calc(100vh - 320px)`,
|
||||
x: 'max-content',
|
||||
}}
|
||||
pagination={false}
|
||||
size='small'
|
||||
columns={columns}
|
||||
dataSource={accounts}
|
||||
rowKey="_id"
|
||||
/>
|
||||
{isModalVisible && (
|
||||
<AccountModal
|
||||
visible={isModalVisible}
|
||||
onOk={handleModalOk}
|
||||
onCancel={() => setIsModalVisible(false)}
|
||||
account={currentAccount}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccountsPage;
|
||||
@@ -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<SalesRecordPageProps> = ({ 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 => (
|
||||
|
||||
439
src/pages/test/sf.tsx
Normal file
439
src/pages/test/sf.tsx
Normal file
@@ -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<boolean>(false);
|
||||
const [queryResult, setQueryResult] = useState<QueryResponse | null>(null);
|
||||
const [error, setError] = useState<string>('');
|
||||
|
||||
// ==================== 业务逻辑函数 ====================
|
||||
|
||||
/**
|
||||
* 根据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: (
|
||||
<div className="mb-4">
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Tag color={statusInfo.color}>
|
||||
{statusInfo.status}
|
||||
</Tag>
|
||||
|
||||
<Text type="secondary" className="text-xs">
|
||||
opCode: {route.opCode}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<Text className="text-base block mb-2">
|
||||
{route.remark}
|
||||
</Text>
|
||||
|
||||
{route.acceptAddress && (
|
||||
<Text type="secondary" className="text-sm">
|
||||
📍 {route.acceptAddress}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="ml-4 text-right">
|
||||
<Text type="secondary" className="text-sm">
|
||||
{route.acceptTime}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen px-4 py-6">
|
||||
<div className="w-full">
|
||||
{/* ==================== 页面标题 ==================== */}
|
||||
<div className="mb-6">
|
||||
<div className="text-center">
|
||||
<Title level={2} className="mb-2 text-gray-900 font-semibold">
|
||||
<TruckOutlined className="mr-2 text-blue-600" />
|
||||
SF快递物流查询
|
||||
</Title>
|
||||
<Text type="secondary" className="text-base">
|
||||
输入运单号和手机号后4位查询物流信息
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ==================== 上半部分:左1/3查询表单+运单信息,右2/3开发者数据 ==================== */}
|
||||
<div className="flex gap-4 mb-6 h-auto">
|
||||
{/* 左侧1/3:查询表单和运单信息 */}
|
||||
<div className="w-1/3 flex flex-col">
|
||||
{/* 查询表单 */}
|
||||
<Card className="mb-4 flex-shrink-0">
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={handleQuery}
|
||||
>
|
||||
<Form.Item
|
||||
label="运单号"
|
||||
name="trackingNumber"
|
||||
rules={[
|
||||
{ required: true, message: '请输入运单号' },
|
||||
{ min: 8, message: '运单号至少8位' },
|
||||
{ max: 20, message: '运单号不能超过20位' }
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
size="large"
|
||||
placeholder="请输入SF快递运单号"
|
||||
prefix={<TruckOutlined className="text-gray-400" />}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="手机号后4位"
|
||||
name="phoneLast4Digits"
|
||||
rules={[
|
||||
{ required: true, message: '请输入手机号后4位' },
|
||||
{ len: 4, message: '请输入4位数字' },
|
||||
{ pattern: /^\d{4}$/, message: '请输入4位数字' }
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
size="large"
|
||||
placeholder="请输入手机号后4位"
|
||||
prefix={<PhoneOutlined className="text-gray-400" />}
|
||||
maxLength={4}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item className="mb-0">
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
loading={loading}
|
||||
size="large"
|
||||
icon={loading ? <SyncOutlined spin /> : <SearchOutlined />}
|
||||
className="flex-1"
|
||||
>
|
||||
{loading ? '查询中...' : '查询物流'}
|
||||
</Button>
|
||||
<Button
|
||||
size="large"
|
||||
onClick={handleReset}
|
||||
className="px-4"
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
</div>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
{/* 运单信息和当前状态 */}
|
||||
{queryResult?.success && queryResult.msgData?.msgData?.routeResps?.length && queryResult.msgData.msgData.routeResps.length > 0 && (
|
||||
<Card className="shadow-sm border-gray-200 flex-1">
|
||||
{queryResult.msgData?.msgData?.routeResps?.map((routeResp: any, index: number) => {
|
||||
const overallStatus = getOverallStatus(routeResp.routes?.slice().reverse() || []);
|
||||
|
||||
return (
|
||||
<div key={index}>
|
||||
<div className="text-center mb-4">
|
||||
<Text strong className="text-lg block mb-2">
|
||||
{routeResp.mailNo}
|
||||
</Text>
|
||||
<Text type="secondary" className="text-sm">
|
||||
SF快递运单
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className="text-center mb-4">
|
||||
<Text type="secondary" className="text-sm block mb-2">当前状态</Text>
|
||||
<Tag color={overallStatus.color} className="text-base px-4 py-2">
|
||||
{overallStatus.status}
|
||||
</Tag>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-50 rounded-lg p-4">
|
||||
<div className="text-center">
|
||||
<Text strong className="text-2xl block mb-1">
|
||||
{routeResp.routes?.length || 0}
|
||||
</Text>
|
||||
<Text type="secondary" className="text-sm">
|
||||
物流节点
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 错误信息显示 */}
|
||||
{error && (
|
||||
<Alert
|
||||
message="查询失败"
|
||||
description={error}
|
||||
type="error"
|
||||
showIcon
|
||||
className="mt-4"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 右侧2/3:开发者数据 */}
|
||||
<div className="w-2/3">
|
||||
{queryResult && !loading && (
|
||||
<Card className="shadow-sm border-gray-200 h-full">
|
||||
<div className="mb-4">
|
||||
<Text strong className="text-lg">
|
||||
<CodeOutlined className="mr-2" />
|
||||
开发者数据 ({JSON.stringify(queryResult).length} 字符)
|
||||
</Text>
|
||||
</div>
|
||||
<div className="bg-gray-900 rounded p-4 h-96 overflow-auto">
|
||||
<pre className="text-green-400 font-mono text-xs whitespace-pre-wrap break-words">
|
||||
{JSON.stringify(queryResult, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 加载状态 */}
|
||||
{loading && (
|
||||
<Card className="text-center shadow-sm border-gray-200 h-full flex items-center justify-center">
|
||||
<div className="py-8">
|
||||
<Spin size="large" className="mb-4" />
|
||||
<div>
|
||||
<Title level={4} className="text-gray-700 mb-2">
|
||||
正在查询物流信息
|
||||
</Title>
|
||||
<Text type="secondary">
|
||||
请稍等片刻...
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 右侧占位内容 */}
|
||||
{!queryResult && !loading && (
|
||||
<Card className="shadow-sm border-gray-200 h-full flex items-center justify-center">
|
||||
<div className="text-center text-gray-400">
|
||||
<CodeOutlined style={{ fontSize: '48px' }} className="mb-4 block" />
|
||||
<Text type="secondary">查询后将在此处显示开发者数据</Text>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ==================== 下半部分:物流轨迹 ==================== */}
|
||||
{queryResult?.success && queryResult.msgData?.msgData?.routeResps?.length && queryResult.msgData.msgData.routeResps.length > 0 && !loading && (
|
||||
<div className="w-full">
|
||||
<Card
|
||||
title="物流轨迹"
|
||||
className="shadow-sm border-gray-200"
|
||||
>
|
||||
{queryResult.msgData?.msgData?.routeResps?.map((routeResp: any, index: number) => (
|
||||
<div key={index}>
|
||||
{routeResp.routes?.length > 0 ? (
|
||||
<Timeline
|
||||
items={renderTimelineItems(routeResp.routes.slice().reverse())}
|
||||
/>
|
||||
) : (
|
||||
<Alert
|
||||
message="暂无物流信息"
|
||||
description="该运单暂无物流轨迹信息,请稍后重试"
|
||||
type="info"
|
||||
showIcon
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 查询失败时的错误信息 */}
|
||||
{queryResult && !loading && !queryResult.success && (
|
||||
<Card className="shadow-sm border-gray-200">
|
||||
<Alert
|
||||
message="查询失败"
|
||||
description={queryResult.errorMsg || queryResult.apiErrorMsg || '未知错误'}
|
||||
type="error"
|
||||
showIcon
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 无查询结果 */}
|
||||
{queryResult?.success && !queryResult.msgData?.msgData?.routeResps?.length && !loading && (
|
||||
<Card className="shadow-sm border-gray-200">
|
||||
<Alert
|
||||
message="无查询结果"
|
||||
description="未找到该运单的物流信息,请检查运单号和手机号是否正确"
|
||||
type="warning"
|
||||
showIcon
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SFExpressQueryPage;
|
||||
402
src/pages/test/sfall.tsx
Normal file
402
src/pages/test/sfall.tsx
Normal file
@@ -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<boolean>(false);
|
||||
const [records, setRecords] = useState<LogisticsRecordData[]>([]);
|
||||
const [total, setTotal] = useState<number>(0);
|
||||
const [error, setError] = useState<string>('');
|
||||
const [pagination, setPagination] = useState({
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
});
|
||||
|
||||
// 批量查询相关状态
|
||||
const [batchLoading, setBatchLoading] = useState<boolean>(false);
|
||||
const [batchModalVisible, setBatchModalVisible] = useState<boolean>(false);
|
||||
const [batchResults, setBatchResults] = useState<any>(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<LogisticsRecordData> = [
|
||||
{
|
||||
title: '物流单号',
|
||||
dataIndex: '物流单号',
|
||||
key: '物流单号',
|
||||
width: 180,
|
||||
render: (text: string) => (
|
||||
<Text strong className="font-mono">
|
||||
{text || '未填写'}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '客户尾号',
|
||||
dataIndex: '客户尾号',
|
||||
key: '客户尾号',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
render: (text: string) => (
|
||||
<Text className="font-mono">
|
||||
{text || '-'}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '是否查询',
|
||||
dataIndex: '是否查询',
|
||||
key: '是否查询',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
render: (isQueried: boolean) => (
|
||||
<Tag color={isQueried ? 'success' : 'default'}>
|
||||
{isQueried ? '已查询' : '未查询'}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '物流状态',
|
||||
dataIndex: '物流状态',
|
||||
key: '物流状态',
|
||||
width: 120,
|
||||
render: (opCode: string | null, record: LogisticsRecordData) => {
|
||||
return (
|
||||
<StatusTag
|
||||
recordId={record._id}
|
||||
opCode={opCode}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// ==================== 生命周期 ====================
|
||||
|
||||
useEffect(() => {
|
||||
fetchLogisticsRecords();
|
||||
}, []);
|
||||
|
||||
// ==================== 渲染组件 ====================
|
||||
|
||||
return (
|
||||
<div className="min-h-screen px-4 py-6">
|
||||
<div className="w-full">
|
||||
{/* ==================== 页面标题 ==================== */}
|
||||
<div className="mb-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<Title level={2} className="mb-2 text-gray-900 font-semibold">
|
||||
<TruckOutlined className="mr-2 text-blue-600" />
|
||||
物流记录管理
|
||||
</Title>
|
||||
<Text type="secondary" className="text-base">
|
||||
查看和管理所有物流记录信息
|
||||
</Text>
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={handleRefresh}
|
||||
loading={loading}
|
||||
>
|
||||
刷新数据
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ==================== 主要内容区域 ==================== */}
|
||||
<div className="flex gap-4">
|
||||
{/* 左侧2/3:物流记录列表 */}
|
||||
<div className="w-2/3">
|
||||
<Card className="shadow-sm border-gray-200">
|
||||
{/* 错误信息显示 */}
|
||||
{error && (
|
||||
<Alert
|
||||
message="数据加载失败"
|
||||
description={error}
|
||||
type="error"
|
||||
showIcon
|
||||
className="mb-4"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 数据统计 */}
|
||||
<div className="mb-4 flex justify-between items-center">
|
||||
<div>
|
||||
<Text type="secondary">
|
||||
共 <Text strong>{total}</Text> 条记录
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 物流记录表格 */}
|
||||
<Table<LogisticsRecordData>
|
||||
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"
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 右侧1/3:批量查询功能 */}
|
||||
<div className="w-1/3">
|
||||
<Card className="shadow-sm border-gray-200 h-full">
|
||||
<div className="text-center">
|
||||
<div className="mb-6">
|
||||
<ThunderboltOutlined style={{ fontSize: '48px', color: '#1890ff' }} className="mb-4 block" />
|
||||
<Title level={4} className="mb-2">批量查询物流</Title>
|
||||
<Text type="secondary" className="text-sm">
|
||||
自动查询所有物流记录的最新状态
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="bg-blue-50 p-4 rounded-lg">
|
||||
<Text strong className="block mb-2">查询规则:</Text>
|
||||
<div className="text-left text-sm text-gray-600 space-y-1">
|
||||
<div>• 只查询有物流单号的记录</div>
|
||||
<div>• 只查询有客户尾号的记录</div>
|
||||
<div>• 只查询标记为"需要查询"的记录</div>
|
||||
<div>• 逐个查询,避免并发请求</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
icon={<ThunderboltOutlined />}
|
||||
loading={batchLoading}
|
||||
onClick={handleBatchQuery}
|
||||
block
|
||||
>
|
||||
{batchLoading ? '正在批量查询...' : '开始批量查询'}
|
||||
</Button>
|
||||
|
||||
{batchLoading && (
|
||||
<div className="mt-4">
|
||||
<Text type="secondary" className="text-sm block mb-2">
|
||||
正在查询中,请耐心等待...
|
||||
</Text>
|
||||
<Progress
|
||||
percent={0}
|
||||
showInfo={false}
|
||||
status="active"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 批量查询结果弹窗 */}
|
||||
<Modal
|
||||
title="批量查询结果"
|
||||
open={batchModalVisible}
|
||||
onCancel={() => setBatchModalVisible(false)}
|
||||
footer={[
|
||||
<Button key="close" onClick={() => setBatchModalVisible(false)}>
|
||||
关闭
|
||||
</Button>
|
||||
]}
|
||||
width={600}
|
||||
>
|
||||
{batchResults && (
|
||||
<div>
|
||||
<div className="mb-4 p-4 bg-gray-50 rounded-lg">
|
||||
<div className="grid grid-cols-3 gap-4 text-center">
|
||||
<div>
|
||||
<Text strong className="text-lg block">{batchResults.total}</Text>
|
||||
<Text type="secondary">总数</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text strong className="text-lg block text-green-600">{batchResults.successCount}</Text>
|
||||
<Text type="secondary">成功</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text strong className="text-lg block text-red-600">{batchResults.failedCount}</Text>
|
||||
<Text type="secondary">失败</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<List
|
||||
dataSource={batchResults.details}
|
||||
renderItem={(item: any) => (
|
||||
<List.Item>
|
||||
<div className="w-full flex justify-between items-center">
|
||||
<div>
|
||||
<Text strong>{item.trackingNumber}</Text>
|
||||
<div className="text-sm text-gray-500">{item.message}</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{item.newStatus && (
|
||||
<Tag color="blue">{item.newStatus}</Tag>
|
||||
)}
|
||||
<Tag color={item.status === 'success' ? 'success' : 'error'}>
|
||||
{item.status === 'success' ? '成功' : '失败'}
|
||||
</Tag>
|
||||
</div>
|
||||
</div>
|
||||
</List.Item>
|
||||
)}
|
||||
size="small"
|
||||
style={{ maxHeight: '400px', overflow: 'auto' }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LogisticsRecordsPage;
|
||||
@@ -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<string, SFExpressStatusInfo> = {
|
||||
// ========== 揽收相关 ==========
|
||||
'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<string, string> = {
|
||||
'待揽收': '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<string, string> = {
|
||||
'success': '✅',
|
||||
'error': '❌',
|
||||
'warning': '⚠️',
|
||||
'processing': '🔄',
|
||||
'blue': '📦',
|
||||
'default': '⏳',
|
||||
} as const;
|
||||
|
||||
Reference in New Issue
Block a user