半年前我们开始把物流端的配送App往鸿蒙上迁移,项目里最不起眼却让我花时间最多的,不是组件适配,也不是地图SDK,而是三个TypeScript类型定义:LargeItem、DeliveryOrder、DeliveryTeam。没错,就是几个interface和enum。但整个大件配送业务的稳定性、订单全生命周期跟踪的可维护性,几乎都压在它们身上。
这个项目简单说,是用React Native跑在鸿蒙设备上,专门给大件物流的配送员和调度员用的订单管理端。大件商品(冰箱、洗衣机、家具)和普通小件在配送流程上差距极大,订单字段多、状态节点多、异常情况也多。如果类型定义写得含糊,后面接口联调、状态流转、多端一致性全都会出问题。这篇我就把这三个核心类型的设计思路、订单生命周期跟踪的落地方式,以及我们踩过的几个坑完整聊一遍,给正在做鸿蒙跨平台、尤其是做物流订单类项目的朋友做个参考。
1. 项目背景:鸿蒙上的配送App,为什么先写类型
1.1 业务现状:大件订单和普通小件不是一个难度
先说业务场景。我们做的这个大件配送App,核心用户是配送员和调度员,处理的是冰箱、洗衣机、床垫、沙发这类大件商品。这类订单有几个普通小件完全不具备的特点:
- SKU属性复杂:长宽高、重量、是否易碎、是否需要安装、是否需要两个人搬运、能否堆叠装车,每一条都会影响配送执行。
- 时间窗口严格:用户往往预约了上午/下午/晚上某个时间段,早到没人、晚到投诉。
- 异常分支多:联系不上客户、拒收、货损、用户要求改期、地址错误,每个异常都要记录原因凭证。
- 团队协作强:大件经常是两人一组、带车配送,不是单人小电驴能搞定的。
这些特点反映到代码里,就是订单数据Model必须足够“厚”。如果一个订单对象只有单号、客户名、手机号这几个字段,那后面所有业务逻辑都没法写。可如果Model设计得乱,字段命名不统一、类型随意用any,那跨端开发时就是灾难。
我在项目启动时定的规矩是:先定义类型,再写页面,再联接口。业务规则能写进类型系统的,绝对不留在组件里用if-else硬凑。
1.2 为什么用React Native做鸿蒙端
团队预算有限,Android、iOS两端已经用React Native开发了两年,货架上的业务代码基本都是JS/TS。鸿蒙端如果从零用ArkUI另写一套,等于要养三套代码,产品迭代速度完全跟不上。所以我们从一开始就锁定了 react-native-openharmony,也就是社区里说的RNOH方案。
RNOH的大致原理是把React Native的运行时和渲染管线适配到鸿蒙的ArkUI框架上,让JS业务代码不用大改就能跑在鸿蒙设备里。这个方案在2024年到2025年已经比较成熟了,核心组件覆盖面够用,但离“完全无缝”还有距离。最明显的问题是:底层原生能力差异大,比如定位SDK、推送、地图组件,在iOS和Android上各自有原生实现,鸿蒙侧很多第三库没有现成适配,需要自己封装原生模块。
这种情况下,类型定义就成了三端共同的“语言公约”。业务侧不关心底层是Android View还是ArkUI组件,只要拿到的是一个结构稳定、字段明确、状态流转合法的订单对象,UI层就能正确渲染,交互逻辑就能正确执行。
1.3 先定义类型,相当于把业务规则内聚在一起
很多人觉得TypeScript类型定义就是写几个interface给编辑器做提示,这远远不够。在一个订单全生命周期跟踪的App里,类型定义承担的是“业务契约”的职责:
- 编译期约束:状态枚举值写错了、字段名拼错了,编译直接报错,而不是运行时白屏。
- 边界隔离:API返回的数据经过解析层之后,类型已经收敛,UI层拿到的都是可信数据。
- 状态机显式化:订单从创建到完成,哪些状态能流转到哪些状态,用类型系统定义好,防止非法跳转。
我们的经验是,类型定义阶段多花三天,后面联调能省三周。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 类型定义设计:把业务规则写进数据类型
2.1 LargeItem:大件商品的数据画像
第一个核心类型是 LargeItem,代表一个大件商品。它不是简单的商品名+价格,而是承载了配送执行所需的全部物理属性。我们的设计长这样:
typescript复制// 大件商品基础信息
export interface LargeItem {
/** 商品唯一编码 */
skuId: string;
/** 商品名称 */
name: string;
/** 品牌 */
brand?: string;
/** 商品分类,如:冰箱/洗衣机/家具 */
category: string;
/** 长宽高,单位cm */
dimensions: {
length: number;
width: number;
height: number;
};
/** 毛重,单位kg */
weight: number;
/** 体积,单位m³,可以由dimensions自动计算 */
readonly volume: number;
// 配送执行相关标志
/** 是否易碎 */
fragile: boolean;
/** 是否能堆叠装车 */
stackable: boolean;
/** 是否需要安装服务 */
needInstall: boolean;
/** 需要几个人搬运,默认1 */
requiredCrew: number;
/** 是否走楼梯,电梯无法进入时用 */
stairRequired?: boolean;
/** 商品图片URL列表 */
images: string[];
/** 商品附加说明 */
remark?: string;
}
这里有两个设计点我特别想强调。
第一个是 readonly volume。 体积是长宽高算出来的,不应该允许外部直接赋值。用 readonly 加计算属性,既统一了单位,又避免了有人把m³和cm³混着传。实际计算放在构造函数或者工厂函数里:
typescript复制export function createLargeItem(input: Omit<LargeItem, 'volume'>): LargeItem {
const { length, width, height } = input.dimensions;
return {
...input,
// 转成m³:cm * cm * cm / 1_000_000
volume: Number(((length * width * height) / 1_000_000).toFixed(3)),
};
}
第二个是 requiredCrew 和 stairRequired。 这两个字段看起来不起眼,但在调度派车的时候非常关键。一个3人才能搬的钢琴和一个1人就能扛的洗衣机,占用的人力资源完全不同。我们曾经因为没有这两个字段,调度系统把一个需要3人的订单派给了只有2人的小团队,结果现场临时加人,成本直接翻倍。这些字段不是给UI展示用的,是给业务规则计算用的。
2.2 DeliveryOrder:状态机驱动的订单模型
第二个核心类型是 DeliveryOrder,也就是配送订单。这是整个生命周期跟踪的中心。它不能只是一个“字段集合”,而必须是一个能感知自己状态的模型。
状态我们用字符串字面量联合类型定义,同时提供英文枚举做代码提示:
typescript复制// 订单核心状态,按配送执行顺序排列
export type OrderStatus =
| 'PENDING' // 待派单
| 'ASSIGNED' // 已派单(分配了配送团队)
| 'PICKED' // 已拣货/装车
| 'DELIVERING' // 配送中
| 'COMPLETED' // 已完成(客户签收)
| 'EXCEPTION' // 异常中
| 'CANCELLED'; // 已取消
订单主接口:
typescript复制export interface DeliveryOrder {
/** 订单号 */
orderId: string;
/** 当前状态 */
status: OrderStatus;
/** 客户信息 */
customer: {
name: string;
phone: string;
address: string;
/** 经纬度,用于地图导航 */
location?: {
lat: number;
lng: number;
};
};
/** 预约配送时间窗 */
timeWindow: {
start: string; // ISO时间戳
end: string; // ISO时间戳
};
/** 关联的大件商品列表 */
items: LargeItem[];
/** 关联的配送团队ID,未派单时为null */
teamId: string | null;
/** 订单备注 */
remark?: string;
/** 状态变更历史,用于全生命周期回溯 */
statusHistory: Array<{
status: OrderStatus;
/** 变更时间 */
changedAt: string;
/** 操作人ID */
operatorId: string;
/** 备注,比如拒收原因 */
remark?: string;
}>;
/** 签收照片,完成时填写 */
proofUrls?: string[];
/** 异常原因,异常时填写 */
exceptionReason?: string;
/** 创建时间 */
createdAt: string;
/** 最后更新时间 */
updatedAt: string;
}
statusHistory 是整个生命周期跟踪的基石。我们要求后端在每次状态变更时都追加一条历史记录,客户端只读展示,不自己伪造。这样订单从创建到签收,所有时间节点都有据可查,客户投诉时能快速定位“卡在哪个环节、操作人是谁、间隔了多久”。
我不建议用纯字符串 status: string 来定义状态。字符串太开放,任何拼写错误都会变成一个新的“状态”,而且IDE没有任何提示。用联合类型之后,写 order.status === 'PICKED' 时是有代码补全的,拼错了直接编译报错。
2.3 DeliveryTeam:运力数据与任务负载
第三个核心类型是 DeliveryTeam,代表一个配送小组。大件配送很少是一个配送员单独行动,通常是一辆车配两到三个人。这块数据的类型设计直接决定了派单计算和任务负载均衡能不能做。
typescript复制export interface DeliveryTeam {
/** 团队ID */
teamId: string;
/** 团队名称 */
name: string;
/** 成员列表 */
members: Array<{
userId: string;
name: string;
/** 角色:司机/搬运工/安装工 */
role: 'DRIVER' | 'CARRIER' | 'INSTALLER';
phone: string;
}>;
/** 当前可用的运输车辆 */
vehicle?: {
plate: string;
/** 车厢容积 m³ */
capacityVolume: number;
/** 最大载重 kg */
capacityWeight: number;
};
/** 今天已分配的任务列表 */
assignments: DeliveryOrder[];
}
// 计算团队当前负载率:已分配订单总体积 / 车辆容积
export function calcTeamLoadRate(team: DeliveryTeam): number {
if (!team.vehicle) return 0;
const totalVolume = team.assignments.reduce(
(sum, order) =>
sum + order.items.reduce((s, item) => s + item.volume, 0),
0
);
return Number((totalVolume / team.vehicle.capacityVolume).toFixed(2));
}
我特意把 vehicle.capacityVolume 和 LargeItem.volume 都用m³做单位,就是为了在派单计算时直接做数值比较,不用来回换算。类型设计最怕的就是单位不统一,一个用cm³一个用m³,表面上编译能过,运行时算出来全是错的。
负载率的计算放进了类型文件里,而不是放在业务组件里。理由是:凡是和实体强绑定的计算逻辑,都应该和类型定义放在一起,组件里只做展示。这样以后调度算法换成后端实时计算,前端也能用同一套函数模型做本地预校验。
2.4 数据安全性:运行时校验与不可变更新
类型定义在编译期有效,但到了运行时,从接口拿到的数据不受TypeScript保护。API返回的JSON里有可能是 null、多字段、少字段,甚至类型不对。所以我们在解析层加了运行时校验,核心思路是:
- 先用TypeScript定义好期望结构
- 运行时拿到数据后,通过一个轻量解析函数校验关键字段
- 校验不通过就进异常队列,不直接渲染
以订单为例:
typescript复制function parseOrder(raw: unknown): DeliveryOrder {
if (!raw || typeof raw !== 'object') {
throw new Error('Invalid order payload');
}
const obj = raw as Record<string, unknown>;
if (typeof obj.orderId !== 'string' || !obj.orderId) {
throw new Error(`Missing orderId: ${JSON.stringify(obj)}`);
}
const status = obj.status as OrderStatus;
const validStatuses: OrderStatus[] = [
'PENDING', 'ASSIGNED', 'PICKED', 'DELIVERING',
'COMPLETED', 'EXCEPTION', 'CANCELLED'
];
if (!validStatuses.includes(status)) {
throw new Error(`Invalid order status: ${String(obj.status)}`);
}
// 其他字段逐个校验...
return obj as DeliveryOrder;
}
有些团队图省事,用 as DeliveryOrder 直接把整个JSON断言过去。我强烈建议不要这么做。后端改了个字段名、把 timeWindow.start 从时间字符串改成了数字时间戳,客户端拿到的就是一个“看似成功但渲染时全坏”的对象。运行时校验相当于给数据加了一道安检门,宁可提前报错,也不能让脏数据流到UI层。
此外,我们还要求状态更新遵循不可变更新原则。每次更新订单状态,不直接 order.status = 'COMPLETED',而是返回一个新对象:
typescript复制export function transitionOrder(
order: DeliveryOrder,
nextStatus: OrderStatus,
operatorId: string,
remark?: string
): DeliveryOrder {
return {
...order,
status: nextStatus,
statusHistory: [
...order.statusHistory,
{
status: nextStatus,
changedAt: new Date().toISOString(),
operatorId,
remark,
},
],
updatedAt: new Date().toISOString(),
};
}
这样做的好处是配合React的状态管理(比如 useReducer 或 zustand)时,引用变化清晰,组件重新渲染逻辑简单,而且历史记录不会被篡改。在这个项目里,我们把这个函数用在了所有订单操作入口,下单、派单、装车、配送、签收、异常标记全部走同一个函数。
3. 订单全生命周期跟踪的落地实现
3.1 状态机定义:什么状态可以流转到什么状态
有了类型还不够,订单生命周期跟踪还要求定义清楚状态之间能不能跳转。比如一个 COMPLETED 的订单不应该被改成 PENDING;一个 CANCELLED 的订单不应该再进入 DELIVERING。
我们用一张流转表来约束:
typescript复制const TRANSITION_MAP: Record<OrderStatus, OrderStatus[]> = {
PENDING: ['ASSIGNED', 'CANCELLED'],
ASSIGNED: ['PICKED', 'CANCELLED', 'EXCEPTION'],
PICKED: ['DELIVERING', 'EXCEPTION'],
DELIVERING: ['COMPLETED', 'EXCEPTION'],
EXCEPTION: ['DELIVERING', 'CANCELLED'],
COMPLETED: [],
CANCELLED: [],
};
export function canTransit(from: OrderStatus, to: OrderStatus): boolean {
return TRANSITION_MAP[from]?.includes(to) ?? false;
}
这个映射表在Web端和鸿蒙端共用。在鸿蒙的React Native端,按钮的可用性直接由它控制。比如当前是 PICKED,UI上就只允许出现“开始配送”和“标记异常”两个操作,其他按钮一律禁用。这样从源头上避免非法操作。
这里补充一个细节:异常状态是一个中转态,不是终态。现实业务中,配送途中联系不上客户,订单会进入 EXCEPTION,但之后团队可能会再次联系上客户,重新进入 DELIVERING,或者最终确认无法配送,转为 CANCELLED。如果设计时把 EXCEPTION 当终态,后面运营就会非常被动。类型定义要把这些业务细节提前想清楚。
3.2 判别联合:让每个状态携带专属数据
单纯用枚举值加可选字段会出现一个问题:订单在 COMPLETED 时才有 proofUrls,在 EXCEPTION 时才有 exceptionReason,如果都用可选字段,那UI层每次都判空,类型系统也没法约束。
我们后来引入了TypeScript判别联合(Discriminated Union),把同一订单在不同状态下的数据差异显式建模:
typescript复制interface OrderBase {
orderId: string;
customer: DeliveryOrder['customer'];
items: LargeItem[];
timeWindow: DeliveryOrder['timeWindow'];
teamId: string | null;
statusHistory: DeliveryOrder['statusHistory'];
}
export interface OrderPending extends OrderBase {
status: 'PENDING';
}
export interface OrderDelivering extends OrderBase {
status: 'DELIVERING';
/** 当前司机位置,用于轨迹跟踪 */
currentLocation?: { lat: number; lng: number };
}
export interface OrderCompleted extends OrderBase {
status: 'COMPLETED';
/** 签收照片 */
proofUrls: string[];
/** 完成时间 */
completedAt: string;
}
export interface OrderException extends OrderBase {
status: 'EXCEPTION';
exceptionReason: string;
/** 异常现场照片 */
exceptionPhotos?: string[];
}
export type TrackableOrder =
| OrderPending
| OrderDelivering
| OrderCompleted
| OrderException;
使用判别联合之后,组件里通过 switch 判断 status,TypeScript会自动收窄类型,在 OrderCompleted 分支里访问 proofUrls 不需要任何类型断言,直接就是安全可用的。
在订单详情页,我们的处理方式是这样的:
tsx复制function OrderDetail({ order }: { order: TrackableOrder }) {
switch (order.status) {
case 'PENDING':
return <PendingView order={order} />;
case 'DELIVERING':
return <DeliveringView order={order} location={order.currentLocation} />;
case 'COMPLETED':
return <CompletedView order={order} proofs={order.proofUrls} />;
case 'EXCEPTION':
return <ExceptionView order={order} reason={order.exceptionReason} />;
default:
return null;
}
}
这段代码在鸿蒙端的React Native里跑得很稳,原因就是每个状态的数据都被类型系统约束得死死的,组件拿到的永远是合理的数据组合。
3.3 从接口响应到类型安全的数据管线
订单列表页的数据流我简单画一下流程(不涉及具体画图,就文字描述):页面加载时调后端接口,拿到的是 unknown 原始JSON,通过上一节讲的 parseOrder 解析成 DeliveryOrder,再放进状态管理Store。任何解析失败都进入错误日志上报,而不是直接崩溃。
我这里要强调一个容易被忽略的点:在React Native鸿蒙端,网络层返回的数据格式可能和Web端有细微差别。比如某些HTTP头、时间格式、数字精度问题,在原生WebView里和RN环境下表现不完全一致。所以解析层放一个健壮的校验函数,不只是防后端问题,还能兜住不同端的兼容差异。
我们实际用了一套轻量级组件来管理整个数据管线:
typescript复制// 简化版:订单Store
import { create } from 'zustand';
interface OrderStore {
orders: Record<string, DeliveryOrder>;
loading: boolean;
error: string | null;
fetchOrders: (teamId: string) => Promise<void>;
updateStatus: (
orderId: string,
nextStatus: OrderStatus,
operatorId: string
) => void;
}
export const useOrderStore = create<OrderStore>((set, get) => ({
orders: {},
loading: false,
error: null,
fetchOrders: async (teamId) => {
set({ loading: true, error: null });
try {
const response = await fetch(`/api/teams/${teamId}/orders`);
const json: unknown = await response.json();
const list = Array.isArray(json) ? json : [];
const orders: Record<string, DeliveryOrder> = {};
for (const item of list) {
try {
const order = parseOrder(item);
orders[order.orderId] = order;
} catch (err) {
// 单条解析失败不影响整体,但需要上报
console.error('parse order failed', err);
}
}
set({ orders, loading: false });
} catch (err) {
set({ error: (err as Error).message, loading: false });
}
},
updateStatus: (orderId, nextStatus, operatorId) => {
const current = get().orders[orderId];
if (!current) return;
if (!canTransit(current.status, nextStatus)) {
console.warn(`Cannot transit ${current.status} -> ${nextStatus}`);
return;
}
const updated = transitionOrder(current, nextStatus, operatorId);
set((state) => ({
orders: { ...state.orders, [orderId]: updated },
}));
},
}));
这里选择了 zustand 而不是 Redux,主要是为了减少样板代码。在鸿蒙的RNOH环境下,zustand依赖少、无额外原生模块,接入成本最低,实测很稳。
3.4 UI联动:用类型驱动订单卡片渲染
订单列表的每个卡片,本质上就是 DeliveryOrder 在某种状态下的“投影”。我们写了一个 OrderCard 组件,通过 status 驱动显示不同的状态色、操作按钮和信息块:
tsx复制export const OrderCard = ({ order }: { order: DeliveryOrder }) => {
return (
<View style={styles.card}>
<Text style={styles.orderId}>单号:{order.orderId}</Text>
<Text>客户:{order.customer.name}</Text>
<Text>地址:{order.customer.address}</Text>
{/* 状态标签 */}
<StatusBadge status={order.status} />
{/* 商品信息 */}
{order.items.map((item) => (
<View key={item.skuId} style={styles.itemRow}>
<Text>{item.name}</Text>
<Text>{(item.volume).toFixed(2)}m³</Text>
{item.fragile && <Text style={styles.fragile}>易碎</Text>}
</View>
))}
{/* 操作区 */}
<View style={styles.actions}>
<ActionButton
title="开始配送"
disabled={!canTransit(order.status, 'DELIVERING')}
/>
<ActionButton
title="标记异常"
disabled={!canTransit(order.status, 'EXCEPTION')}
/>
</View>
</View>
);
};
等列表渲染到一定规模之后,我建议把每个核心卡片拆成独立组件,不要像我上面这样所有信息堆在一起。大件订单字段太多,一屏放不下,卡片层级做深一点,详情页单独成为一个页面栈,性能更好,代码也更容易维护。
4. 常见问题与排坑实录
4.1 React Native 启动白屏问题
刚把RNOH环境搭起来时,我们第一个遇到的问题就是:App启动之后白屏,要等好几秒才有内容。这个问题在鸿蒙上比Android上更明显,因为HarmonyOS NEXT应用启动时,React Native运行时和JS Bundle的加载都在冷启动路径上。
排查思路三步走:
- 先确认是JS Bundle问题还是渲染问题。我们在鸿蒙侧入口Log里加打印,看
AppRegistry.runApplication有没有执行到,如果执行了但白屏,就是渲染层问题。 - 检查是否加载了本地Bundle。开发阶段RNOH会默认走本地调试器,但发布版本必须把Bundle内置到应用资源里,否则网络慢或者调试服务没启动,页面必然白屏。
- 关闭不必要的调试面板。Debug模式下的远程JS加载和红屏提示在Release包一定要关掉,否则会影响启动性能。
我们最终的解法是:构建时把JS Bundle打进鸿蒙应用的 rawfile 目录,启动时优先从本地读取;同时设置了原生启动页,让用户打开App的瞬间看到品牌Logo,避免“白屏焦虑”。启动时间从最初的3秒降到了1秒以内。
4.2 类型漂移:后端改了字段,客户端不知道
项目跑起来之后,最让人头疼的是后端接口字段悄悄变化。有一次后端把 timeWindow 里的 start 和 end 从字符串改成时间戳,客户端解析订单列表时 parseOrder 直接大面积抛错,好在因为我们有运行时校验,没有让脏数据渲染到页面上,但整个列表还是空了。
后来团队在 parseOrder 的校验逻辑里加了更宽容的兼容处理:
typescript复制function parseTimeWindow(raw: unknown): DeliveryOrder['timeWindow'] {
if (!raw || typeof raw !== 'object') {
throw new Error('Invalid timeWindow');
}
const obj = raw as Record<string, unknown>;
const start = typeof obj.start === 'number'
? new Date(obj.start).toISOString()
: String(obj.start);
const end = typeof obj.end === 'number'
? new Date(obj.end).toISOString()
: String(obj.end);
return { start, end };
}
同时和后端约定,任何字段变更都要提前在接口文档里同步。我们内部还做了一个小工具,联调阶段自动抓取接口返回的JSON结构,跟本地TypeScript类型做Diff比对,一旦发现字段不一致就在CI上报提醒。这个工具虽然没有完全自动化,但大大减少了联调时“跑起来才发现挂了”的频率。
4.3 模拟器平台不兼容:arm64与x86的坑
项目过程中有同事想用电脑模拟器调试,结果鸿蒙模拟器在x86架构的机器上运行经常报“运行设备不兼容”,部分依赖底层 jsvm 的功能直接不可用。鸿蒙模拟器对arm64架构的支持更完整,如果手头是M系列芯片的Mac或者arm64的Windows设备,体验会好很多。
如果你用的是x86电脑,我的建议是尽快换真机调试,或者用云真机服务。我们在项目里给每位开发都配了一台HarmonyOS NEXT的测试机,专门跑RNOH包。真机调试虽然麻烦一点,但性能数据和模拟器的差异非常大,定位问题也更准确。
4.4 ArkTS互操作与har封装
RNOH里如果要用鸿蒙原生能力,比如调用系统扫码、蓝牙、推送,需要自己封装原生模块,并把代码编译成 .har 包给RN侧调用。这里最常出的问题就是类型边界不匹配:RN侧的TypeScript对象传到ArkTS侧时,如果字段不是基本类型,ArkTS经常会报类型警告甚至崩溃。
我们的解法是在TurboModule接口处传递扁平化的JSON字符串:
typescript复制// RN侧
const result = await NativeModules.NativeScanner.scan(JSON.stringify(order));
然后在ArkTS侧二次解析。这样避免了两套类型系统的直接冲突,虽然多了一次序列化开销,但胜在稳定。
另外,封装的har包要挂到工程的 oh_modules 下,首次集成时别忘了同步Graph配置。我们的经验是:原生模块封装完成之后,先在纯ArkTS的测试页面里验证一遍,确认没问题再接RN侧,否则排查起来很麻烦。
4.5 记住类型定义是给团队用的,不是给后端交差的
最后聊一个容易忽略的点:类型定义不只是给编辑器做提示的。在这个鸿蒙跨平台项目里,三个实体类型(LargeItem、DeliveryOrder、DeliveryTeam)真正起的作用,是把前端、后端、调度算法、UI设计稿里的业务共识沉淀到了代码里。每次产品经理改业务规则,我们不是先去改组件,而是先改类型定义和状态机流转表,页面和逻辑自然跟着调整。
我个人实际操作中的体会是,这类项目最怕的不是技术难点,而是业务语言没对齐。类型定义和状态流转表,就是让业务语言在代码里“可视化”的最好工具。你写 canTransit('PICKED', 'DELIVERING') 返回 true,比任何注释都直观。
如果后续要扩展,我建议优先补两块:一是把地图轨迹数据接入类型系统,让 DELIVERING 状态的订单能实时展示配送员位置;二是增加消息推送类型,把订单状态变化主动推到配送员和调度员的设备上,配合上这个类型体系,整个大件配送的履约链路会清晰很多。
