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 React, { useEffect, useState, useCallback, useRef } from 'react';
|
||||||
import { Tag, Spin } from 'antd';
|
import { Tag, Tooltip, Spin, Typography, Timeline } from 'antd';
|
||||||
import MyTooltip from '@/components/tooltip/MyTooltip';
|
import { ClockCircleOutlined, EnvironmentOutlined, TruckOutlined } from '@ant-design/icons';
|
||||||
import { debounce } from 'lodash'; // 导入lodash的debounce函数
|
import { SF_OPCODE_STATUS_MAP } from '@/types/enum';
|
||||||
|
import { debounce } from 'lodash';
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
interface LogisticsStatusProps {
|
interface LogisticsStatusProps {
|
||||||
recordId: string; // 关联记录的id
|
recordId: string; // 关联记录的id
|
||||||
@@ -16,18 +25,6 @@ const detailsCache: Record<string, { details: string, number: string, timestamp:
|
|||||||
// 缓存过期时间(毫秒)
|
// 缓存过期时间(毫秒)
|
||||||
const CACHE_EXPIRY = 5 * 60 * 1000; // 5分钟
|
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}`;
|
const getCacheKey = (recordId: string, productId: string) => `${recordId}_${productId}`;
|
||||||
|
|
||||||
@@ -208,39 +205,436 @@ const LogisticsStatus: React.FC<LogisticsStatusProps> = React.memo(({ recordId,
|
|||||||
[recordId, productId]
|
[recordId, productId]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (loading) {
|
/**
|
||||||
return <Spin size="small" />;
|
* 检查字符串是否是有效的JSON格式
|
||||||
}
|
*/
|
||||||
|
const isValidJSON = (str: string): boolean => {
|
||||||
|
if (typeof str !== 'string') return false;
|
||||||
|
|
||||||
if (!logisticsStatus) {
|
// 简单检查:JSON应该以 { 或 [ 开头
|
||||||
return null; // 返回null而不是div,减少不必要的DOM元素
|
const trimmed = str.trim();
|
||||||
}
|
if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
try {
|
||||||
<MyTooltip
|
JSON.parse(str);
|
||||||
color="white"
|
return true;
|
||||||
title={
|
} catch {
|
||||||
detailsLoading ? (
|
return false;
|
||||||
<Spin size="small" />
|
}
|
||||||
) : (
|
};
|
||||||
<div>
|
|
||||||
<span style={{ fontSize: 'larger' }}>物流单号:{logisticsNumber}</span>
|
/**
|
||||||
<p>{logisticsDetails || '暂无物流详情'}</p>
|
* 解析物流详情JSON并格式化
|
||||||
</div>
|
*/
|
||||||
)
|
const parseLogisticsDetails = () => {
|
||||||
|
if (!logisticsDetails) return { routes: [], hasValidData: false, lastUpdateTime: '' };
|
||||||
|
|
||||||
|
try {
|
||||||
|
let detailData;
|
||||||
|
|
||||||
|
if (typeof logisticsDetails === 'string') {
|
||||||
|
// 检查是否是有效的JSON字符串
|
||||||
|
if (!isValidJSON(logisticsDetails)) {
|
||||||
|
console.log('物流详情不是JSON格式:', logisticsDetails);
|
||||||
|
return { routes: [], hasValidData: false, lastUpdateTime: '' };
|
||||||
|
}
|
||||||
|
detailData = JSON.parse(logisticsDetails);
|
||||||
|
} else {
|
||||||
|
detailData = logisticsDetails;
|
||||||
}
|
}
|
||||||
onMouseEnter={fetchLogisticsDetails}
|
|
||||||
>
|
// 从SF快递API响应中提取路由信息
|
||||||
<Tag
|
if (detailData?.msgData?.routeResps?.length > 0) {
|
||||||
style={{
|
const routeResp = detailData.msgData.routeResps[0];
|
||||||
fontSize: '10px',
|
const routes = routeResp.routes || [];
|
||||||
}}
|
|
||||||
bordered={false}
|
// 获取最新更新时间(从最新的路由记录中获取)
|
||||||
color={statusColorMapping[logisticsStatus] || 'default'}
|
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}
|
{logisticsStatus}
|
||||||
</Tag>
|
</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: String },
|
||||||
是否查询: { type: Boolean, default: true }, // 设置默认值为true
|
是否查询: { type: Boolean, default: true }, // 设置默认值为true
|
||||||
客户尾号: { type: String },
|
客户尾号: { type: String },
|
||||||
物流公司: { type: String },
|
|
||||||
物流详情: { type: String },
|
物流详情: { type: String },
|
||||||
物流状态: { type: String },
|
物流状态: { type: String },
|
||||||
更新时间: { type: Date, default: Date.now },
|
更新时间: { type: Date, default: Date.now },
|
||||||
|
|||||||
@@ -332,7 +332,6 @@ export interface ILogisticsRecord {
|
|||||||
物流单号: string;
|
物流单号: string;
|
||||||
是否查询: boolean;
|
是否查询: boolean;
|
||||||
客户尾号: string;
|
客户尾号: string;
|
||||||
物流公司: string;
|
|
||||||
物流详情: string;
|
物流详情: string;
|
||||||
更新时间: Date;
|
更新时间: Date;
|
||||||
物流状态: string;
|
物流状态: 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 React, { useEffect, useState } from 'react';
|
||||||
import { Form, Input, Select, DatePicker, Button, Row, Col, Card, Divider, Tag, App } from 'antd';
|
import { Form, Input, Select, DatePicker, Button, Row, Col, Card, Divider, Tag, App } from 'antd';
|
||||||
import { ICustomer, ICustomerCoupon, IProduct, ISalesRecord } from '@/models/types';
|
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);
|
const account = accounts.find(acc => acc._id === option?.value);
|
||||||
if (!account) return false;
|
if (!account) return false;
|
||||||
const { 微信号, 微信昵称 } = account;
|
const { 微信号, 微信昵称 } = account;
|
||||||
return (
|
const inputLower = input.toLowerCase();
|
||||||
微信号.toLowerCase().includes(input.toLowerCase()) ||
|
const wechatIdMatch = 微信号 ? 微信号.toLowerCase().includes(inputLower) : false;
|
||||||
微信昵称.toLowerCase().includes(input.toLowerCase())
|
const wechatNameMatch = 微信昵称 ? 微信昵称.toLowerCase().includes(inputLower) : false;
|
||||||
);
|
return wechatIdMatch || wechatNameMatch;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{accounts.map(account => (
|
{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 {
|
export enum PermissionType {
|
||||||
CATALOGUE,
|
CATALOGUE,
|
||||||
MENU,
|
MENU,
|
||||||
BUTTON,
|
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