主题
订单详情与药品物流(二)
本页对应 订单详情与药品物流,共 5 段视频,约 1 小时 14 分。
学习目标
- 能够提取并复用订单详情 composable 函数,接收订单 ID,返回订单详情与 loading 状态,并在支付结果页和订单详情页中正确调用。
- 能够抽取药品清单组件,通过 props 接收药品清单数据,并在支付结果页和订单详情页中完成复用与动态渲染。
- 能够根据订单状态条件渲染订单详情页中的收货地址/最新物流信息、支付时间/支付方式以及不同状态下的操作按钮。
- 能够定义物流详情相关类型与 API 函数,在物流详情组件挂载时获取数据并渲染物流状态、快递信息及物流轨迹列表。
- 能够接入高德地图 JS API,完成 key 与 securityCode 配置、Window 类型扩展、地图初始化、地图样式与缩放比例设置。
- 能够封装地图标记绘制函数,绘制起点、终点和当前运输位置,并使用地图 API 将当前运输位置定位到地图中心并调整缩放比例。
知识点
本节目标与实现顺序
这一节做药品订单详情的前三步:准备路由和组件骨架、提取获取订单详情的 composable、提取药品清单组件。详情页的完整渲染留到下一节。为什么先做这三步?因为支付结果页已经写过“获取订单详情”和“展示药品清单”,详情页会重复同样的逻辑和结构;先把可复用部分抽出来,后面渲染详情页时就不用在两个页面里各维护一份。怎么想?先让路由能进入详情组件,再把数据逻辑抽成 useOrderDetail,再把药品清单抽成独立组件,最后在详情页把两者接起来。怎么做?按下面的顺序落地。易错点:不要一上来就写详情页模板,先把“能进入页面、能拿到数据、能复用清单”验证通。
准备订单详情路由与组件骨架
为什么需要?订单详情页需要通过订单 ID 访问,路由必须先能匹配到组件,否则后面的数据请求没有入口。怎么想?支付结果页的路由和详情页不同:支付结果页通常从查询参数拿订单 ID,详情页则把 ID 放在路径上,所以路由规则应设计成 order/:id 这类形式。怎么做?在 order 目录下新建订单详情组件(转写中称 order-detail.vue),写入基础结构;再到路由 index.ts 中复制一条规则,改成订单详情路径,组件指向订单详情组件。配置后在浏览器访问 order/后面跟ID,确认能进入详情组件。易错点:路由路径参数名要和后面取值一致;组件文件名、导入路径要对应;不要把详情页路由误配成支付结果页的规则。
提取 useOrderDetail 获取详情逻辑
为什么需要?支付结果页已经实现过获取订单详情的逻辑,详情页还要再用一次。如果复制两份,后续改接口、加 loading、改错误处理都要改两处。怎么想?把“根据订单 ID 请求详情、维护 order 数据、维护 loading 状态”抽到 composables/index.ts,函数名用 useOrderDetail,由调用方传入 ID。怎么做?把支付结果页里的请求逻辑复制到 composable 中,导入所需依赖;把原来从 useRouter 里取 ID 的方式改成“函数参数传入 ID”,这样支付结果页和详情页都能用。转写中还给数据加载加了 loading:请求开始设为 true,用 try/catch/finally,无论成功失败都在 finally 里改回 false,最后返回 order 和 loading。调用侧可以写成:
ts
const { order, loading } = useOrderDetail(id)易错点:ID 的类型要是字符串;不要在 composable 里写死某一种路由取值方式;finally 不能漏,否则失败后 loading 会一直转;原来页面里无用的导入要清理。
改造支付结果页复用 composable
为什么需要?抽了函数不改造旧页面,就没有真正复用,后面还容易出现两套逻辑。怎么想?支付结果页的订单 ID 来自路由 query,详情页来自路由 params,但 composable 只认传入的 ID,所以页面各自负责取 ID。怎么做?在支付结果页删掉原先重复的请求代码,改为调用 useOrderDetail,把从 route.query 中取到的订单 ID 传进去。改造后刷新支付结果页,确认订单数据仍能正常显示。易错点:支付结果页的 ID 来源是 query,不要照抄成 params;传参前确认类型;删除旧代码时不要误删模板还要用的变量。
提取药品清单组件
为什么需要?支付页和详情页都有药品清单,结构、样式一致,复制会导致样式和字段修改不同步。怎么想?把药品清单的模板、样式、props 一起抽成组件,放在 order/components 下(转写中称 order-med.vue)。组件只负责“接收药品清单数组并展示”,不负责请求。怎么做?从支付页复制药品清单的整个结构,新建组件基本结构后放入模板;在 script setup 中用 defineProps 接收药品清单字段,类型使用转写中提到的 medical 类型且为数组;把支付页里对应样式一并迁移到组件中。支付页原先写药品清单的位置改为使用该组件,并把订单中的药品清单传进去。易错点:只抽模板不抽样式,页面会变形;props 类型和数组字段别写错;支付页替换后要检查原来的 v-for 是否已移入组件。
在订单详情页接入数据与清单
为什么需要?前三步的最终目的,是让详情页能拿到订单数据并先把药品清单展示出来,为下一节渲染详情打基础。怎么想?详情页从路由路径参数取 ID,调用 useOrderDetail 拿 order,在订单存在时渲染内容;药品清单区域直接用刚提取的组件。怎么做?在详情组件中导入 useOrderDetail,通过 useRouter 拿到路由对象,用 route.params.id 取 ID 并转成字符串后传入;模板里加条件判断,订单数据存在时再渲染;药品清单位置导入并使用药品清单组件,把 order 中的药品清单字段传进去。刷新浏览器后,如果药品能动态展示,说明 composable 和药品清单组件都已生效。易错点:详情页 ID 在路径参数里,不是 query;route.params.id 要做字符串处理;条件渲染要包住依赖 order 的区域,避免初始为空时报错。
本节调试与小结
调试顺序建议按转写走:先访问 order/ID 看路由和组件是否正常;再替换详情组件结构看页面是否正常;然后改造支付结果页,确认复用 composable 后数据仍能显示;再提取药品清单组件,检查支付页样式;最后到详情页刷新,看到药品清单动态展示,就说明前三步完成。易错点集中在 ID 来源、类型、loading 收尾和组件样式迁移。下一节再继续完成订单详情页面的完整渲染。
本节目标与数据来源
本节只做“药品订单详情”的页面渲染:上一节已经准备了路由、详情组件,并提取了 useOrderDetail composable 和药品清单组件 OrderMedical,本节把数据真正展示到页面上。为什么需要先明确目标?因为详情页内容多,若不先区分“数据获取”和“条件渲染”,很容易把请求逻辑、展示逻辑和按钮交互混在一起。怎么想:页面先用 useRoute 取地址栏 params.id,再调用 useOrderDetail(id) 得到 order 和 loading,模板中用 v-if="order" 控制主体展示。怎么做:按转写稿,ID 从路由参数取,类型是字符串;数据来自 composable,不在详情页重复写请求。易错点:不要把 useRoute 写成 router,不要忘记 params.id 是字符串;模板中 ref 的解包方式以项目实际结构为准,转写稿中状态取的是 order.status.value。
标题与订单状态
为什么需要:标题不能只写“药品订单详情”,还要让用户看到当前订单状态。怎么想:固定文案 + 动态状态,状态来自 order.status。怎么做:在标题后插值显示状态;若项目状态是枚举,直接显示可能不是中文,需要按项目已有映射或枚举值展示。易错点:不要手写死状态;不要漏掉 .value 或重复解包,导致页面显示 [object Object]。调试时刷新页面,确认标题后出现“待收货”等状态文字,即说明状态字段取对了。
物流信息与收货地址的条件渲染
为什么需要:不是所有订单都有最新物流。已取消、待支付、待发货通常没有物流信息,此时应展示收货地址;已发货等状态才展示最新物流。怎么想:用订单状态做分支,三种无物流状态走地址盒,其他状态走物流盒。怎么做:按转写稿,判断 OrderType.Cancel、OrderType.Pay、OrderType.Send 等枚举,具体枚举名以项目为准;有物流时给物流盒加点击跳转:
ts
router.push(`/order/logistics/${order.id}`)模板结构可概括为:
vue
<div v-if="order.status.value === OrderType.Cancel
|| order.status.value === OrderType.Pay
|| order.status.value === OrderType.Send">
<!-- 收货地址 -->
</div>
<div v-else>
<!-- 最新物流信息 -->
</div>易错点:v-else 必须紧跟 v-if;状态枚举名不要凭记忆写;跳转路径按项目路由配置,转写稿中物流详情在 order/logistics 下拼订单 ID。
地址字段与手机号脱敏
为什么需要:收货地址要完整可读,手机号又不能明文全展示。怎么想:省市区和详细地址拼接展示,收货人后展示脱敏手机号。怎么做:按接口字段取省、市、区、详细地址、收货人、手机号;手机号用正则保留前三后四:
ts
const maskMobile = (mobile: string) =>
mobile.replace(/^(\d{3})\d{4}(\d{4})$/, '$1****$2')易错点:正则只匹配 11 位手机号;字段名不要写成同音字;地址字段缺失时要避免拼接出 undefined。
金额、编号、时间与支付信息
为什么需要:底部信息是订单凭证,但支付时间和支付方式只有支付过的订单才应出现。怎么想:金额、订单编号、创建时间通常总展示;支付信息用条件包起来。怎么做:按转写稿展示应支付金额、运费、优惠券抵扣、实付款、订单编号、创建时间;支付信息用 template 加条件,待发货、待收货、已完成等已支付状态才展示。支付方式判断:
vue
<div>支付时间:{{ order.payTime }}</div>
<div>
支付方式:{{ order.paymentMethod === 1 ? '支付宝' : '微信' }}
</div>易错点:已取消、待支付不要展示支付时间和支付方式;优惠券金额前有负号;金额字段和支付方式字段不要混用,转写稿中金额与 paymentMethod 是不同字段。
操作按钮按状态展示
为什么需要:不同订单状态允许的操作不同,按钮不能全部展示。怎么想:五种状态分别对应一组按钮:已取消、待支付、待发货、待收货、已完成。怎么做:每个按钮容器加 v-if 判断状态;待支付区域还要展示应付金额。易错点:不要用 v-show 造成隐藏按钮仍占位;状态判断不要漏;本节只做展示,按钮交互按转写稿不实现,留到后续。
调试与验证
为什么需要:条件渲染必须用不同状态订单验证。怎么想:先用待收货订单看物流信息、支付信息和“确认收货”按钮;再创建新问诊订单、开处方、创建药品订单,支付时放弃,得到待支付订单。怎么做:进入待支付详情,应只看到收货地址,没有最新物流、没有支付时间和支付方式,底部是待支付按钮。易错点:测试后要刷新页面确认数据;状态字段未更新时先查接口返回;不要因为一个状态正常就认为所有分支都正确。
物流详情页面的实现边界
为什么需要:药品订单详情页中,只有已经发货、运输中、派送中、待收货等状态才存在“最新物流信息”;点击这条信息要进入物流详情,展示物流状态、预计送达时间、快递公司、快递编号以及物流轨迹。地图区域先预留,后续再绘制轨迹。
怎么想:先准备路由和组件基础结构,再定义物流类型,接着写 API 函数,组件挂载时取数并渲染;地图初始化单独放在挂载后执行,避免 DOM 尚未生成就操作地图容器。
怎么做:新建 order/logistics.vue,配置路由 order/logistics/:id;从路由参数取订单 ID;页面结构先按转写稿中的结构放好。
易错点:路由参数是字符串,传 API 前要确认类型;不要一进入组件就初始化地图,必须等 DOM 生成后再执行。
先复用订单详情与药品清单
为什么需要:支付结果页已经实现过“获取订单详情”和“药品清单”展示,订单详情页再写一遍会重复。
怎么想:把订单详情请求封装成 useOrderDetail(id),把药品清单抽成 OrderMedical 组件。
怎么做:composable 中维护 order 和 loading,请求开始时 loading=true,结束用 finally 改回 false:
ts
export function useOrderDetail(id: string) {
const order = ref<OrderDetail>()
const loading = ref(false)
const getOrder = async () => {
loading.value = true
try {
order.value = (await getOrderDetail(id)).data
} finally {
loading.value = false
}
}
onMounted(getOrder)
return { order, loading }
}药品清单组件接收 medical: Medical[],在支付页和详情页复用。
易错点:支付结果页的 ID 来自 route.query.orderId,订单详情页的 ID 来自 route.params.id,不要混用;无用导入要清理。
定义物流类型与状态枚举
为什么需要:物流状态、轨迹、经纬度都要有类型,否则模板和地图代码容易写错字段。
怎么想:状态文字用枚举表达;每条轨迹包含内容、创建时间、状态;当前运输位置提供经纬度,供地图使用。
怎么做:在类型文件中补充:
ts
export enum LogisticsState {
已发货 = 1,
已揽件 = 2,
运输中 = 3,
派送中 = 4,
已签收 = 5
}
export interface Location {
longitude: number
latitude: number
}
export interface Express {
id: string
content: string
createTime: string
state: { value: LogisticsState; text: string }
}
export interface Logistics {
state: { value: LogisticsState; text: string }
estimatedTime: string
name: string
number: string
list: Express[]
currentLocationInfo: Location
}易错点:枚举值要按转写稿对应为“已发货、已揽件、运输中、派送中、已签收”,注意“已”不是“以”;类型未导入时会出现 any 提示。
准备 API 并在挂载时取数
为什么需要:物流详情数据来自后端,需要按订单 ID 查询。
怎么想:API 函数只负责发请求,组件负责在挂载时调用并保存结果。
怎么做:在 API 文件中新增 getLogistics(id),按接口文档地址拼接 ID,GET 请求;组件中:
ts
const logistics = ref<Logistics>()
const route = useRoute()
onMounted(async () => {
const id = route.params.id as string
logistics.value = (await getLogistics(id)).data
})易错点:接口地址和请求方式要以接口文档为准;route.params.id 要断言为字符串;数据未返回前模板要允许空值。
渲染物流详情与轨迹
为什么需要:用户要看到当前状态、预计送达、快递信息和完整轨迹。
怎么想:头部展示 logistics.state.value、estimatedTime、name、number;轨迹用 v-for 遍历 logistics.list。
怎么做:轨迹项展示 item.state.text、item.content、item.createTime,配合步骤条组件,默认激活第一条。
易错点:字段名要和类型一致,如 createTime、currentLocationInfo;物流数组为空时不要强行取第一项。
初始化高德地图
为什么需要:灰色区域要展示物流轨迹,先要把地图容器初始化出来。
怎么想:到高德开放平台注册、实名认证、创建 Web 应用,得到 key 和 securityJsCode;安装 @amap/amap-jsapi-loader,在组件挂载后加载 2.0 API。
怎么做:扩展 window 类型:
ts
interface Window {
_AMapSecurityConfig: { securityJsCode: string }
}加载并初始化:
ts
window._AMapSecurityConfig = { securityJsCode: '你的securityJsCode' }
AMapLoader.load({
key: '你的key',
version: '2.0'
}).then((AMap) => {
const map = new AMap.Map('map', {
mapStyle: 'amap://styles/grey',
zoom: 12
})
})易错点:2.0 版本需要配置 securityJsCode;map 容器 ID 要对应;初始化要放在 onMounted 之后。
绘制起点、终点和当前运输位置
为什么需要:轨迹上要标出起点、终点和货车当前位置。
怎么想:直接给 marker 配图片无法设置大小,所以用 AMap.Icon + AMap.Size 控制尺寸,再用 AMap.Pixel 调整偏移,让图标尖角对准坐标点。
怎么做:封装 getMarker(point, image, width=25, height=30):
ts
const getMarker = (point: Location, image: string, width = 25, height = 30) => {
const icon = new AMap.Icon({
size: new AMap.Size(width, height),
image,
imageSize: new AMap.Size(width, height)
})
const marker = new AMap.Marker({
position: [point.longitude, point.latitude],
icon,
offset: new AMap.Pixel(-width / 2, -height)
})
map.add(marker)
return marker
}起点用 star.png,终点用 end.png,当前运输位置用 car.png,货车图按约 33×20 缩放。当前点取 logistics.currentLocationInfo。
易错点:偏移量计算错误会让图标尖角偏离坐标;当前点可能为空,模板或代码中要做非空断言/判断;marker 创建后要 map.add。
定位当前运输位置并缩放
为什么需要:地图初始显示范围较大,用户看不清当前货车位置。
怎么想:等标记绘制完成后,把当前 marker 传给 setFitView,让地图居中;再用 setZoom 放大比例。
怎么做:按转写稿示例,在 3 秒后执行:
ts
setTimeout(() => {
map.setFitView([currentMarker])
map.setZoom(10)
}, 3000)易错点:setFitView 接收数组;要在 marker 创建完成后再调用;缩放值过大会丢失周边轨迹,按实际效果调整。
代码示例
1. 提取 useOrderDetail composable 与药品清单组件
ts
// src/composables/index.ts
import { onMounted, ref } from 'vue'
import { getMedicalOrderDetail } from '@/api/order'
import type { OrderDetail } from '@/types/order'
// 传入订单 ID,返回订单详情数据和 loading 状态
export const useOrderDetail = (id: string) => {
const order = ref<OrderDetail>()
const loading = ref(false)
const getOrderDetail = async () => {
loading.value = true
try {
const res = await getMedicalOrderDetail(id)
order.value = res.data
} finally {
// 成功或失败都要把 loading 关掉
loading.value = false
}
}
onMounted(getOrderDetail)
return { order, loading }
}vue
<!-- src/views/order/components/OrderMedical.vue -->
<script setup lang="ts">
import type { Medical } from '@/types/order'
// 药品清单由父组件通过 props 传入
defineProps<{ medicines: Medical[] }>()
</script>
<template>
<div class="order-medical">
<div class="head">
<span>药品清单</span>
<span>共 {{ medicines.length }} 件药品</span>
</div>
<div class="item" v-for="item in medicines" :key="item.id">
<img :src="item.avatar" alt="" />
<div class="info">
<p>{{ item.name }}</p>
<p>{{ item.specs }}</p>
</div>
<p class="price">¥{{ item.price }}</p>
<p class="count">x{{ item.quantity }}</p>
</div>
</div>
</template>
<style lang="scss" scoped>
.order-medical {
padding: 12px;
background: #fff;
.head {
display: flex;
justify-content: space-between;
}
.item {
display: flex;
align-items: center;
img {
width: 60px;
height: 60px;
}
.info {
flex: 1;
}
}
}
</style>useOrderDetail通过参数接收id,而不是在内部用useRoute()取:支付结果页的 ID 在query上,详情页的 ID 在params上,只有传参才能复用。try...finally是易错点:只写try成功分支会把loading永远卡在true,失败时页面一直转圈。- 药品清单组件把「结构 + 样式」一起搬过来,父组件只负责传
medicines,否则支付页和详情页各写一份,改一处要改两处。 - 组件放在
views/order/components/下,与页面同级,避免和全局组件混在一起。
2. 订单详情页:按订单状态做条件渲染
vue
<!-- src/views/order/OrderDetail.vue -->
<script setup lang="ts">
import { computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useOrderDetail } from '@/composables'
import OrderMedical from './components/OrderMedical.vue'
import { OrderType } from '@/enums'
const route = useRoute()
const router = useRouter()
// 详情页 ID 在路径上:/order/:id
const id = route.params.id as string
const { order } = useOrderDetail(id)
// 已取消 / 待支付 / 待发货:没有物流信息,展示收货地址
const showAddress = computed(
() =>
order.value?.status === OrderType.Cancel ||
order.value?.status === OrderType.Pay ||
order.value?.status === OrderType.Send
)
// 手机号脱敏:138****1234
const formatMobile = (mobile?: string) =>
mobile ? mobile.replace(/^(\d{3})\d{4}(\d{4})$/, '$1****$2') : ''
// 点击最新物流信息,跳到物流详情
const goLogistics = () => router.push(`/order/logistics/${order.value!.id}`)
</script>
<template>
<div class="order-detail" v-if="order">
<div class="title">
药品订单详情 <span>{{ order.statusValue }}</span>
</div>
<!-- 1. 收货地址 / 最新物流:按订单状态二选一 -->
<div class="address" v-if="showAddress">
<p>
{{ order.addressInfo?.province }}{{ order.addressInfo?.city
}}{{ order.addressInfo?.county }}{{ order.addressInfo?.detail }}
</p>
<p>
{{ order.addressInfo?.receiver }}
{{ formatMobile(order.addressInfo?.mobile) }}
</p>
</div>
<div class="logistics" v-else @click="goLogistics">
<p>{{ order.express?.info?.content }}</p>
<p>{{ order.express?.info?.time }}</p>
</div>
<!-- 2. 药品清单组件 -->
<OrderMedical :medicines="order.medicines" />
<!-- 3. 金额信息 -->
<div class="money">
<p>应付款:{{ order.payment }}</p>
<p>运费:{{ order.expressFee }}</p>
<p>优惠券:-{{ order.couponDiscount }}</p>
<p>实付款:{{ order.actualPayment }}</p>
</div>
<!-- 4. 订单信息:只有支付过的订单才有支付时间和支付方式 -->
<div class="info">
<p>订单编号:{{ order.orderNumber }}</p>
<p>创建时间:{{ order.createTime }}</p>
<template
v-if="
order.status === OrderType.Send ||
order.status === OrderType.Tick ||
order.status === OrderType.Complete
"
>
<p>支付时间:{{ order.payTime }}</p>
<p>支付方式:{{ order.paymentMethod === 1 ? '支付宝' : '微信' }}</p>
</template>
</div>
<!-- 5. 操作按钮:5 种状态分别展示 -->
<div class="actions">
<template v-if="order.status === OrderType.Cancel">订单已取消</template>
<template v-else-if="order.status === OrderType.Pay">
<span>需付款:{{ order.actualPayment }}</span>
<button>立即支付</button>
</template>
<template v-else-if="order.status === OrderType.Send">等待发货</template>
<template v-else-if="order.status === OrderType.Tick">
<button>确认收货</button>
</template>
<template v-else>订单已完成</template>
</div>
</div>
</template>- 收货地址和最新物流用
v-if / v-else互斥渲染:待支付、已取消、待发货这三种状态根本没有物流信息,写死物流盒子会渲染出空内容。 - 支付信息必须包一层
<template v-if>:<template>不会产生多余 DOM 标签,而如果直接给两个<p>各写一次v-if,条件会重复且容易写漏。 formatMobile正则^(\d{3})\d{4}(\d{4})$要锚定首尾,否则 11 位手机号中段任意位置都会被替换;记得用$1****$2拼接。- 模板里
order已由ref自动解包,写成order.status即可;但在script中必须写order.value.status,showAddress里漏写.value会永远返回undefined。 - 列表渲染和组件传值都用
order.medicines,前提是页面根节点上有v-if="order",否则首屏数据未返回时会报「读取 undefined 的属性」。
3. 物流详情页:高德地图初始化 + 绘制起点/终点/当前运输位置
ts
// src/types/global.d.ts —— 扩展 window 全局类型
interface Window {
_AMapSecurityConfig: {
securityJsCode: string
}
}
// src/main.ts —— 2.0 版本必须先配置安全密钥
window._AMapSecurityConfig = {
securityJsCode: '你的安全密钥'
}vue
<!-- src/views/order/OrderLogistics.vue -->
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import AMapLoader from '@amap/amap-jsapi-loader'
import { getMedicalOrderLogistics } from '@/api/order'
import type { Logistics, Location } from '@/types/order'
import startImg from '@/assets/start.png'
import endImg from '@/assets/end.png'
import carImg from '@/assets/car.png'
const route = useRoute()
const id = route.params.id as string
const logistics = ref<Logistics>()
let AMap: any = null
let map: any = null
// 1. 加载高德 JSAPI 并初始化地图
const initMap = async () => {
AMap = await AMapLoader.load({
key: '你的高德 Key',
version: '2.0'
})
map = new AMap.Map('map', {
mapStyle: 'amap://styles/grey', // 自定义风格
zoom: 12 // 缩放比例
})
}
// 2. 封装创建标记的函数,供起点、终点、运输位置复用
const getMarker =常见错误
1. 抽取 composable 后,详情页拿到的 ID 是 undefined
- 现象:订单详情页能进入,但接口请求参数为空、页面不渲染或 404。
- 原因:直接把支付结果页的逻辑搬进
useOrderDetail,函数内部仍用useRoute().query.id取 ID;而详情页的 ID 在route.params.id(路径上),物流详情页同理。 - 修法:composable 不自己取路由,改为接收
id参数;调用方各自从route.params.id/route.query.orderId取值传入,并断言为string。
2. 复制逻辑导致两份代码各自演化
- 现象:改了详情页的 loading 逻辑,支付结果页没变;或两个页面请求时机不一致。
- 原因:第二次用到时选择"拷贝一份",而不是抽函数。
- 修法:第二次出现即抽取;抽取时把可变部分(ID、是否需要 loading)作为参数或返回值暴露,不要在函数里写死调用方上下文。
3. 支付时间/支付方式在"待支付""已取消"订单上仍然显示
- 现象:未付款订单详情里出现空白或
undefined的支付信息行。 - 原因:只判断了字段是否存在,没有按订单状态做白名单判断;或只给外层一个大
v-if,把两个不相关容器绑在一起。 - 修法:用
<template v-if="...">包裹整组,条件写成"待发货 / 待收货 / 已完成"三种状态之一;按钮区同理按状态一一对应,别用v-else兜底猜。
4. 请求异常时页面永远白屏,loading 一直转
- 现象:接口报错后既不展示数据也不展示错误,
loading恒为 true。 - 原因:只写了
loading = true和成功分支,没有try/catch/finally。 - 修法:
try { ... } catch { ... } finally { loading = false };失败时至少保证order为undefined,模板用v-if="order"兜住。
5. window._AMapSecurityConfig 报"属性不存在"
- 现象:TS 编译报错,或运行时地图加载成功但控制台警告安全密钥未配置。
- 原因:只赋了值,没有扩展
Window类型;或securityJsCode拼写与官方要求不一致。 - 修法:新建
global.d.ts,interface Window { _AMapSecurityConfig: { securityJsCode: string } },注意是interface合并而非type覆盖。
6. 地图容器空白 / 报 "map container not found"
- 现象:
new AMap.Map('map')执行了但页面没地图,或直接抛错。 - 原因:在
setup顶层或onMounted之前初始化,DOM 还没生成;或 JSAPI 还没加载完就 new 实例。 - 修法:
onMounted里 →AMapLoader.load(...)→ 在.then回调中再new AMap.Map(...),保证"DOM 就绪 → 脚本就绪 → 实例化"的时序。
7. 标记图片的尖端没有对准坐标点
- 现象:起点/终点图标整体偏右下,看起来"飘"在坐标旁边。
- 原因:marker 默认以图标左上角对齐经纬度;且二倍图直接按原尺寸渲染导致模糊、过大。
- 修法:两件事分开处理——
icon用new AMap.Icon({ size: new AMap.Size(w/2, h/2), imageSize: new AMap.Size(w, h), image })解决清晰度和大小;offset: new AMap.Pixel(-w/4, -h/2)解决对齐(按渲染尺寸计算,别按原图算)。
8. setFitView 不生效
- 现象:调用后视野没变化。
- 原因:marker 还没
map.add()就setFitView,或传入的是空数组。 - 修法:先
map.add(marker),再map.setFitView([marker]);需要动画过渡时配合setZoom一起调用。
练习
三页统一数据层:把支付结果页、订单详情页、物流详情页中重复的"取 ID → 请求 → loading"逻辑收敛为 composable。要求:支付结果页在
loading为 true 时展示骨架/占位,请求失败时展示可点击重试的提示,且三页共用同一份错误处理。状态驱动展示对照表:为订单的 5 种状态(待支付 / 待发货 / 待收货 / 已完成 / 已取消)列一张表,明确"收货地址、最新物流信息、支付信息、底部按钮"各自的展示规则,然后按这张表重构详情页模板。目标:模板中不再出现散落的、含义不明的
v-if组合;如果条件超过 3 个,尝试用计算属性或状态→配置对象的映射替代。标记函数复用:把创建 marker 的逻辑封装为
getMarker(point, image, width = 25, height = 30),用它绘制起点、终点、当前运输位置三处(三个图标尺寸各不相同)。验证三个图标的尖端都精确落在各自坐标上;然后实现"进入页面先看全貌,3 秒后定位到当前位置并放大"。地图接入的健壮性:将高德的
key与securityJsCode改为从环境变量读取;为AMapLoader.load增加加载失败分支(展示占位区域 + 重试按钮),并确保重复点击重试不会重复加载脚本、重复创建地图实例。类型与展示解耦:为物流状态枚举补一份"状态值 → 中文文案/颜色"的映射,供物流详情列表和地图标记共用;同时检查物流信息相关类型中可能为空的字段(预计送达时间、轨迹坐标等),把可选性在类型上标注出来,并在渲染处做兜底。
课件图示






原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 药品订单-订单详情-01使用提取的详情函数和药品清单 | 11:48 |
| 2 | 药品订单-订单详情-02完成渲染 | 14:03 |
| 3 | 药品订单-物流详情 | 12:49 |
| 4 | 药品订单-使用高德地图 | 13:18 |
| 5 | 药品订单-绘制起点终点当前运输位置 | 22:17 |
小结
- 复用的触发点是"第二次出现",但复用的前提是把上下文从函数里剥离出去——ID 这类东西必须由调用方传入,否则复用即埋雷。
- 凡是"某状态下才显示"的地方,都值得先把规则整理成一张表再写代码;散落的
v-if是这类页面后续返工的主要来源。 - 异步数据获取的三件套(初始值、loading、finally 复位)缺一不可,模板侧永远假设"数据可能不存在"。
- 接入第三方地图 SDK 本质上只有三件事:加载脚本、挂到全局对象、补类型声明。TS 项目里最容易漏、也最容易被忽略的是第三步。
- 地图相关的一切都强依赖时序:DOM 就绪 → SDK 就绪 → 实例化 → 添加覆盖物 → 调整视野,任何一步提前都会以"白屏"或"无效果"的形式暴露,且报错信息通常不直观。
- 用图片做标记时,"渲染尺寸"和"对齐偏移"是两个独立问题,混在一起调只会来回改;先定尺寸,再算偏移。
- 后台返回的时间、金额、手机号等展示型数据,格式化规则应尽早统一约定,否则会散落在各个模板里重复实现。
- 交互行为(按钮点击后的动作)可以延后实现,但结构必须与状态严格对应,这样后续补交互时只是填空,而不是重构。