0609.1
Some checks failed
Next.js CI/CD 流水线 / deploy (push) Failing after 22s

This commit is contained in:
2025-06-09 01:27:51 +08:00
parent 4edd9768cc
commit d8398afa12
22 changed files with 3734 additions and 157 deletions

View 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

View 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;

View 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;

View File

@@ -1,8 +1,17 @@
//src\components\logistics\status.tsx
/**
* 物流状态组件
* @author 阿瑞
* @version 1.0.0
* @description 显示物流状态标签,支持鼠标悬停查看详细轨迹
*/
import React, { useEffect, useState, useCallback, useRef } from 'react';
import { Tag, Spin } from 'antd';
import MyTooltip from '@/components/tooltip/MyTooltip';
import { debounce } from 'lodash'; // 导入lodash的debounce函数
import { Tag, Tooltip, Spin, Typography, Timeline } from 'antd';
import { ClockCircleOutlined, EnvironmentOutlined, TruckOutlined } from '@ant-design/icons';
import { SF_OPCODE_STATUS_MAP } from '@/types/enum';
import { debounce } from 'lodash';
const { Text } = Typography;
interface LogisticsStatusProps {
recordId: string; // 关联记录的id
@@ -16,18 +25,6 @@ const detailsCache: Record<string, { details: string, number: string, timestamp:
// 缓存过期时间(毫秒)
const CACHE_EXPIRY = 5 * 60 * 1000; // 5分钟
const statusColorMapping: { [key: string]: string } = {
'已退回': 'red',
'退回中': 'orange',
'已拒收': 'volcano',
'已签收': 'green',
'派送中': 'blue',
'已发出': 'geekblue',
'已揽收': 'cyan',
'处理中': 'grey',
'待发货': 'magenta'
};
// 生成缓存键
const getCacheKey = (recordId: string, productId: string) => `${recordId}_${productId}`;
@@ -208,39 +205,436 @@ const LogisticsStatus: React.FC<LogisticsStatusProps> = React.memo(({ recordId,
[recordId, productId]
);
/**
* 检查字符串是否是有效的JSON格式
*/
const isValidJSON = (str: string): boolean => {
if (typeof str !== 'string') return false;
// 简单检查JSON应该以 { 或 [ 开头
const trimmed = str.trim();
if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) {
return false;
}
try {
JSON.parse(str);
return true;
} catch {
return false;
}
};
/**
* 解析物流详情JSON并格式化
*/
const parseLogisticsDetails = () => {
if (!logisticsDetails) return { routes: [], hasValidData: false, lastUpdateTime: '' };
try {
let detailData;
if (typeof logisticsDetails === 'string') {
// 检查是否是有效的JSON字符串
if (!isValidJSON(logisticsDetails)) {
console.log('物流详情不是JSON格式:', logisticsDetails);
return { routes: [], hasValidData: false, lastUpdateTime: '' };
}
detailData = JSON.parse(logisticsDetails);
} else {
detailData = logisticsDetails;
}
// 从SF快递API响应中提取路由信息
if (detailData?.msgData?.routeResps?.length > 0) {
const routeResp = detailData.msgData.routeResps[0];
const routes = routeResp.routes || [];
// 获取最新更新时间(从最新的路由记录中获取)
const lastUpdateTime = routes.length > 0
? routes[routes.length - 1]?.acceptTime || ''
: '';
// 如果有有效的路由数据
if (routes.length > 0) {
return {
routes: routes.map((route: any) => ({
time: route.acceptTime || '未知时间',
opCode: route.opCode || '未知',
remark: route.remark || '无备注',
location: route.acceptAddress || undefined
})).reverse(), // 反转数组,让最新的记录在前面
hasValidData: true,
lastUpdateTime
};
} else {
// 有响应但routes为空说明是已创建单但未发货的状态
return {
routes: [],
hasValidData: true, // 标记为有效数据,只是暂无物流信息
isEmpty: true,
lastUpdateTime: new Date().toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
}).replace(/\//g, '-')
};
}
}
} catch (parseError) {
console.error('解析物流详情失败:', parseError);
}
return { routes: [], hasValidData: false, lastUpdateTime: '' };
};
/**
* 渲染物流详情内容
*/
const renderLogisticsContent = () => {
if (detailsLoading) {
return (
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '24px',
minWidth: '200px'
}}>
<Spin size="small" />
<Text type="secondary" style={{ marginLeft: '8px', fontSize: '13px' }}>
...
</Text>
</div>
);
}
if (!logisticsDetails && !logisticsNumber) {
return (
<div style={{
textAlign: 'center',
padding: '16px',
minWidth: '200px'
}}>
<Text type="secondary" style={{ fontSize: '13px' }}></Text>
</div>
);
}
const { routes: routesData, hasValidData, isEmpty, lastUpdateTime } = parseLogisticsDetails();
return (
<div style={{
minWidth: '380px',
maxWidth: '450px',
background: '#fff',
borderRadius: '8px',
border: '1px solid #e8e8e8',
overflow: 'hidden'
}}>
{/* 物流单号标题区域 */}
<div style={{
background: 'linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%)',
padding: '12px 20px',
borderBottom: '1px solid #e8e8e8'
}}>
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between'
}}>
<div style={{
display: 'flex',
alignItems: 'center',
gap: '10px'
}}>
<div style={{
width: '24px',
height: '24px',
background: '#1890ff',
borderRadius: '6px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}>
<TruckOutlined style={{ color: '#fff', fontSize: '12px' }} />
</div>
<Text strong style={{
fontSize: '14px',
color: '#262626',
letterSpacing: '0.3px'
}}>
{logisticsNumber || '未知单号'}
</Text>
</div>
{lastUpdateTime && (
<div style={{
display: 'flex',
alignItems: 'center',
gap: '4px'
}}>
<ClockCircleOutlined style={{ color: '#8c8c8c', fontSize: '10px' }} />
<Text type="secondary" style={{
fontSize: '11px',
color: '#8c8c8c'
}}>
{lastUpdateTime}
</Text>
</div>
)}
</div>
</div>
{/* 物流轨迹详情 */}
<div style={{
maxHeight: '380px',
overflowY: 'auto',
padding: '12px 20px 8px'
}}>
{routesData.length > 0 ? (
<Timeline
items={routesData.map((route: any, index: number) => {
// 获取状态信息
const statusInfo = SF_OPCODE_STATUS_MAP[route.opCode];
const isLatest = index === 0;
return {
color: isLatest ? (statusInfo?.color || '#1890ff') : '#d9d9d9',
dot: isLatest ? (
<div style={{
width: '12px',
height: '12px',
background: statusInfo?.color === 'success' ? '#52c41a' : '#1890ff',
borderRadius: '50%',
border: '2px solid #fff',
boxShadow: '0 0 0 2px #e6f7ff'
}} />
) : undefined,
children: (
<div style={{
marginBottom: '12px',
paddingLeft: '4px'
}}>
{/* 第一行:状态 地点 时间 最新标签 */}
<div style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
marginBottom: '4px',
flexWrap: 'wrap'
}}>
<Text strong style={{
color: isLatest ? '#262626' : '#595959',
fontSize: '12px',
fontWeight: isLatest ? 600 : 500
}}>
{statusInfo?.status || `${route.opCode}`}
</Text>
{route.location && (
<div style={{
display: 'flex',
alignItems: 'center',
gap: '2px',
color: '#8c8c8c',
fontSize: '11px'
}}>
<EnvironmentOutlined style={{ fontSize: '9px' }} />
<span>{route.location}</span>
</div>
)}
<div style={{
color: '#bfbfbf',
fontSize: '11px',
fontFamily: 'monospace'
}}>
{route.time}
</div>
{isLatest && (
<div style={{
background: 'linear-gradient(135deg, #1890ff, #36cfc9)',
color: '#fff',
fontSize: '9px',
padding: '1px 5px',
borderRadius: '8px',
fontWeight: 500,
lineHeight: '1.2',
marginLeft: 'auto'
}}>
</div>
)}
</div>
{/* 第二行:详细备注 */}
<div style={{
color: '#595959',
lineHeight: '1.4',
fontSize: '11px',
paddingRight: '20px'
}}>
{route.remark}
</div>
</div>
),
};
})}
/>
) : (
<div style={{
textAlign: 'center',
padding: '20px',
color: '#8c8c8c'
}}>
{hasValidData && isEmpty ? (
<div>
<div style={{
fontSize: '13px',
marginBottom: '8px',
fontWeight: 500
}}>
</div>
<div style={{
fontSize: '11px',
color: '#bfbfbf'
}}>
</div>
</div>
) : (
<Text style={{
fontSize: '12px',
color: '#595959',
lineHeight: '1.5',
display: 'block'
}}>
</Text>
)}
</div>
)}
</div>
{/* 记录总数提示 */}
{routesData.length > 0 && (
<div style={{
textAlign: 'center',
marginTop: '4px',
padding: '12px 20px',
background: '#fafafa',
borderTop: '1px solid #f0f0f0'
}}>
<Text type="secondary" style={{
fontSize: '11px',
color: '#8c8c8c',
fontWeight: 500
}}>
{routesData.length}
</Text>
</div>
)}
</div>
);
};
/**
* 渲染状态标签
*/
const renderStatusTag = () => {
if (loading) {
return <Spin size="small" />;
}
if (!logisticsStatus) {
return null; // 返回null而不是div减少不必要的DOM元素
return null;
}
// 处理null值显示为待发货
if (logisticsStatus === null || logisticsStatus === undefined || logisticsStatus === '') {
return (
<MyTooltip
color="white"
title={
detailsLoading ? (
<Spin size="small" />
) : (
<div>
<span style={{ fontSize: 'larger' }}>{logisticsNumber}</span>
<p>{logisticsDetails || '暂无物流详情'}</p>
</div>
)
}
onMouseEnter={fetchLogisticsDetails}
>
<Tag
style={{
fontSize: '10px',
color: '#fa8c16',
backgroundColor: 'transparent',
borderColor: '#fa8c16'
}}
bordered={false}
color={statusColorMapping[logisticsStatus] || 'default'}
bordered={true}
>
</Tag>
);
}
// 处理API返回的特殊状态
if (logisticsStatus === '待填单') {
return (
<Tag color="volcano" style={{ fontSize: '10px' }} bordered={false}>
</Tag>
);
}
if (logisticsStatus === '待发货') {
return (
<Tag
style={{
fontSize: '10px',
color: '#fa8c16',
backgroundColor: 'transparent',
borderColor: '#fa8c16'
}}
bordered={true}
>
</Tag>
);
}
// 直接使用opCode从映射表获取状态信息
const statusInfo = SF_OPCODE_STATUS_MAP[logisticsStatus];
if (statusInfo) {
return (
<Tag color={statusInfo.color} style={{ fontSize: '10px' }} bordered={false}>
{statusInfo.status}
</Tag>
);
}
// 如果opCode不在映射表中显示原始opCode
return (
<Tag color="default" style={{ fontSize: '10px' }} bordered={false}>
{logisticsStatus}
</Tag>
</MyTooltip>
);
};
return (
<Tooltip
color="white"
title={renderLogisticsContent()}
placement="left"
trigger="hover"
onOpenChange={(visible) => {
if (visible) {
fetchLogisticsDetails();
}
}}
overlayStyle={{
maxWidth: 'none',
boxShadow: '0 12px 32px 4px rgba(0, 0, 0, 0.04), 0 8px 20px rgba(0, 0, 0, 0.08)',
borderRadius: '8px',
border: 'none',
padding: '0'
}}
>
<span className="cursor-pointer">
{renderStatusTag()}
</span>
</Tooltip>
);
});

View File

@@ -363,7 +363,6 @@ const LogisticsRecordSchema: Schema = new Schema({
: { type: String },
: { type: Boolean, default: true }, // 设置默认值为true
: { type: String },
: { type: String },
: { type: String },
: { type: String },
: { type: Date, default: Date.now },

View File

@@ -332,7 +332,6 @@ export interface ILogisticsRecord {
物流单号: string;
是否查询: boolean;
客户尾号: string;
物流公司: string;
物流详情: string;
更新时间: Date;
物流状态: string;

View 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);

View 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
View 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 连接已关闭(客户端断开连接)");
});
}

View 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) {
// 获取最新的opCoderoutes数组中的最后一个元素是最新的
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);

View 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) {
// 获取最新的opCoderoutes数组中的最后一个元素是最新的记录
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);

View 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);

View 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);

View 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;

View File

@@ -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);

View 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;

View 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;

View File

@@ -1,4 +1,9 @@
//src\pages\team\sale\index.tsx
/**
* 文件: team/sale/index.tsx
* 作者: 阿瑞
* 功能: 销售记录页面 - 创建和管理销售记录
* 版本: v2.0.0 - 使用 fetch 替换 axios修复搜索过滤错误
*/
import React, { useEffect, useState } from 'react';
import { Form, Input, Select, DatePicker, Button, Row, Col, Card, Divider, Tag, App } from 'antd';
import { ICustomer, ICustomerCoupon, IProduct, ISalesRecord } from '@/models/types';
@@ -482,10 +487,10 @@ const SalesRecordPage: React.FC<SalesRecordPageProps> = ({ salesRecord, onCancel
const account = accounts.find(acc => acc._id === option?.value);
if (!account) return false;
const { , } = account;
return (
.toLowerCase().includes(input.toLowerCase()) ||
.toLowerCase().includes(input.toLowerCase())
);
const inputLower = input.toLowerCase();
const wechatIdMatch = ? .toLowerCase().includes(inputLower) : false;
const wechatNameMatch = ? .toLowerCase().includes(inputLower) : false;
return wechatIdMatch || wechatNameMatch;
}}
>
{accounts.map(account => (

439
src/pages/test/sf.tsx Normal file
View 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
View 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;

View File

@@ -56,5 +56,87 @@ export enum BasicStatus {
CATALOGUE,
MENU,
BUTTON,
}
}
// ==================== SF快递物流状态映射 ====================
/**
* SF快递状态信息接口
* @author 阿瑞
* @version 1.0.0
*/
export interface SFExpressStatusInfo {
status: string;
color: string;
description: string;
}
/**
* SF快递opCode状态映射表
* @description 基于真实SF快递物流数据分析得出的opCode对应状态
* @author 阿瑞
* @version 1.0.0
*/
export const SF_OPCODE_STATUS_MAP: Record<string, SFExpressStatusInfo> = {
// ========== 揽收相关 ==========
'54': { status: '已揽收', color: 'blue', description: '快件已被顺丰收取' },
'43': { status: '已收取', color: 'blue', description: '顺丰速运已收取快件' },
// ========== 分拣运输相关 ==========
'30': { status: '分拣中', color: 'processing', description: '快件正在分拣处理' },
'36': { status: '运输中', color: 'processing', description: '快件正在运输途中' },
'31': { status: '到达网点', color: 'processing', description: '快件已到达转运中心' },
'302': { status: '长途运输', color: 'processing', description: '快件正在安全运输中(长距离)' },
'310': { status: '途经中转', color: 'processing', description: '快件途经中转城市' },
'570': { status: '铁路运输', color: 'processing', description: '快件通过铁路运输,已发车' },
// ========== 派送相关 ==========
'44': { status: '准备派送', color: 'processing', description: '正在为快件分配合适的快递员' },
'204': { status: '派送中', color: 'processing', description: '快件已交给快递员,正在派送途中' },
'70': { status: '派送失败', color: 'warning', description: '快件派送不成功,待再次派送' },
'33': { status: '待派送', color: 'warning', description: '已与客户约定新派送时间,待派送' },
// ========== 退回相关 ==========
'517': { status: '退回申请中', color: 'warning', description: '快件退回申请已提交,正在处理中' },
'99': { status: '退回中', color: 'warning', description: '应客户要求,快件正在退回中' },
// ========== 签收相关 ==========
'80': { status: '已签收', color: 'success', description: '快件已成功签收(含正常签收和退回签收)' },
} as const;
/**
* SF快递状态颜色映射表
* @description 将状态字符串映射到对应的UI颜色
*/
export const SF_STATUS_COLOR_MAP: Record<string, string> = {
'待揽收': 'default',
'已揽收': 'blue',
'已收取': 'blue',
'分拣中': 'processing',
'运输中': 'processing',
'铁路运输': 'processing',
'到达网点': 'processing',
'长途运输': 'processing',
'途经中转': 'processing',
'准备派送': 'processing',
'派送中': 'processing',
'派送失败': 'warning',
'待派送': 'warning',
'退回申请中': 'warning',
'退回中': 'warning',
'已签收': 'success',
} as const;
/**
* SF快递状态图标映射表
* @description 将状态颜色映射到对应的emoji图标
*/
export const SF_STATUS_ICON_MAP: Record<string, string> = {
'success': '✅',
'error': '❌',
'warning': '⚠️',
'processing': '🔄',
'blue': '📦',
'default': '⏳',
} as const;