管理端开发日记②:订单详情、流程与数据回显

列表解决“找到订单”,详情页解决“理解订单”。一个订单详情并不是一张表,而是订单资料、业务凭证、审核记录、状态流转和飞行任务的聚合视图。
1. 页面结构
src/views/manage/delivery-order/detail.vue 采用左侧地图、右侧信息区的布局。右侧按内容拆分为基础信息、图片凭证、流程记录和操作区。
| 数据 | 来源 | 用途 |
|---|---|---|
| 订单详情 | 订单详情接口 | 基础字段和当前状态 |
| 订单凭证 | 凭证接口 | 订单图、货品图、签收图 |
| 历史版本 | 订单历史接口 | 驳回再提交前后的变化 |
| 流程日志 | progress 接口 | 审核、调度、起飞、完成节点 |
| 飞行任务 | 飞行任务接口 | 航线、起降点和飞行状态 |
2. 多数据源回显
详情页不能把所有数据硬塞进一个超大接口。订单与飞行任务的更新频率不同,图片也需要鉴权下载,因此页面分别请求,再汇总成视图模型。
async function loadDetail() { loading.value = true try { const [orderResult, proofResult, historyResult, logResult] = await Promise.allSettled([ getDeliveryOrder(orderId.value), listDeliveryProof({ orderId: orderId.value }), listDeliveryOrderHistory({ orderId: orderId.value }), getDeliveryOrderProcessLogs(orderId.value) ])
detail.value = valueOf(orderResult, {}) proofList.value = rowsOf(proofResult) orderHistoryList.value = rowsOf(historyResult) processLogs.value = rowsOf(logResult) await loadWayline() } finally { loading.value = false }}这里使用 Promise.allSettled 而不是 Promise.all,因为某一组非核心数据失败时,不应让整个详情页变成空白。例如签收图暂时读取失败,订单基本资料与流程日志仍然可以展示。
3. 图片凭证按业务类型分组
凭证并不是普通附件,它带有明确语义:
const proofGroups = { ORDER_PHOTO: '订单截图', GOODS_PHOTO: '货品照片', SIGN_PHOTO: '签收凭证'}页面通过 AuthenticatedImage 获取受保护图片,而不是直接拼接服务器路径。这样既携带登录凭证,又可以统一处理加载失败、临时地址和缓存。
图片分组后,审核员可以快速判断“订单来源是否可信”“货品是否满足飞行条件”;订单完成后又能独立查看签收凭证,不会把所有图片混成一排。
4. 流程日志比当前状态更有解释力
当前状态只能说明现在在哪里,流程日志才能解释为什么走到这里。页面将日志按时间排序并构造成时间线:
const sortedLogs = computed(() => [...processLogs.value].sort((a, b) => { return new Date(a.operateTime || a.createTime) - new Date(b.operateTime || b.createTime)}))每个节点显示操作名称、操作人、时间和备注。对于被驳回后重新提交的订单,还会从历史记录中读取 rejectReason、rejectExplain,让新版本和旧版本之间的关系可追溯。
5. 状态驱动底部操作区
详情页不允许用户随意触发任何接口,而是由订单状态确定按钮:
| 当前状态 | 可执行动作 |
|---|---|
PENDING_AUDIT | 审核通过、驳回 |
WAIT_DISPATCH | 取消订单、开始飞行 |
DELIVERING | 标记配送成功、配送失败 |
| 终态 | 只读查看 |
<template v-if="detail.orderStatus === 'PENDING_AUDIT'"> <el-button @click="rejectOrder">驳回</el-button> <el-button type="primary" @click="approveOrder">审核通过</el-button></template>每次操作成功后重新执行 loadDetail(),而不是在前端手工猜测新状态。服务端才是状态机的最终裁判,重新拉取可以同步流程日志、任务编号和更新时间。
6. 数据回显中的空值和异步顺序
详情数据返回前,模板中的 detail.orderStatus、坐标和图片列表都可能为空。页面默认使用空对象和空数组,并用 -- 处理缺失字段。地图必须在容器完成渲染后初始化,航线又依赖订单坐标和任务数据,因此加载顺序大致为:
读取订单与关联数据 ↓等待 DOM 完成渲染 ↓初始化地图实例 ↓解析航点并绘制航线 ↓放置起点、终点和无人机这类顺序问题不会总是报错,却会表现为“偶尔没有线”“切换页面后地图空白”,所以比普通表单更需要明确生命周期。
7. 本篇小结
操作幂等与二次确认
详情页的写操作都增加提交锁,高风险动作增加确认弹窗。前端提交锁用于避免双击,后端仍需依据当前状态做幂等校验。例如两名管理员同时审核同一订单,后提交的一方应该收到“状态已变化”,而不是重复生成流程日志。
if (submitting.value) returnsubmitting.value = truetry { await approveDeliveryOrder(detail.value.orderId) await loadDetail()} finally { submitting.value = false}详情页的降级顺序
我把信息分为三个等级:订单基础信息是页面成立的前提;流程日志和任务信息失败时显示局部错误;图片加载失败只影响对应缩略图。这样网络不稳定时,页面不是“全有或全无”。
另外,切换订单 ID 时先停止旧的地图动画、清空凭证临时 URL,再加载新数据。否则组件复用可能让上一张订单的无人机和下一张订单的资料短暂出现在同一屏,堪称跨订单串门。
订单详情是典型的聚合页面:接口多、状态多、展示层次也多。把数据源分开请求、在视图层重新编排,比追求一个万能接口更容易维护。我写到最后的感受是,详情页像侦探桌上的线索板——每根线都要接对,故事才讲得通。
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!





























