Skip to content

订单详情与药品物流(二) ​

本页对应 订单详情与药品物流,共 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 一起调用。

练习 ​

  1. 三页统一数据层:把支付结果页、订单详情页、物流详情页中重复的"取 ID → 请求 → loading"逻辑收敛为 composable。要求:支付结果页在 loading 为 true 时展示骨架/占位,请求失败时展示可点击重试的提示,且三页共用同一份错误处理。

  2. 状态驱动展示对照表:为订单的 5 种状态(待支付 / 待发货 / 待收货 / 已完成 / 已取消)列一张表,明确"收货地址、最新物流信息、支付信息、底部按钮"各自的展示规则,然后按这张表重构详情页模板。目标:模板中不再出现散落的、含义不明的 v-if 组合;如果条件超过 3 个,尝试用计算属性或状态→配置对象的映射替代。

  3. 标记函数复用:把创建 marker 的逻辑封装为 getMarker(point, image, width = 25, height = 30),用它绘制起点、终点、当前运输位置三处(三个图标尺寸各不相同)。验证三个图标的尖端都精确落在各自坐标上;然后实现"进入页面先看全貌,3 秒后定位到当前位置并放大"。

  4. 地图接入的健壮性:将高德的 key 与 securityJsCode 改为从环境变量读取;为 AMapLoader.load 增加加载失败分支(展示占位区域 + 重试按钮),并确保重复点击重试不会重复加载脚本、重复创建地图实例。

  5. 类型与展示解耦:为物流状态枚举补一份"状态值 → 中文文案/颜色"的映射,供物流详情列表和地图标记共用;同时检查物流信息相关类型中可能为空的字段(预计送达时间、轨迹坐标等),把可选性在类型上标注出来,并在渲染处做兜底。

课件图示 ​

课件第15页:药品订单-使用高德地图 得到key和jscode 使用jsloader加载 初始

课件第13页:药品订单-物流详情 路由与组件布局 物流详情类型 物流详情API 展示物流详情页

课件第9页:药品订单-支付信息 路由与组件布局 药品订单详情类型 药品订单详情API 获取数

课件第11页:药品订单-订单详情 路由与组件布局 提取订单详情逻辑 提取药品订单组件 展示订单

课件第17页:药品订单-自定义绘制路径 自动规划路径 关闭道路情况 自定义绘制路径 关闭沿途标

课件第20页:药品订单-绘制起点终点当前运输标记 绘制起点标记 提取创建标记函数 绘制终点运输

原视频对照 ​

#视频时长
1药品订单-订单详情-01使用提取的详情函数和药品清单11:48
2药品订单-订单详情-02完成渲染14:03
3药品订单-物流详情12:49
4药品订单-使用高德地图13:18
5药品订单-绘制起点终点当前运输位置22:17

小结 ​

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