Skip to content

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

本页对应 订单详情与药品物流,共 6 段视频,约 1 小时 23 分。

学习目标 ​

  1. 能使用 useClipboard 在问诊详情中实现复制订单编号,并完成权限判断、失败轻提示和复制成功提示。
  2. 能抽取并封装支付抽屉组件,通过 props 与 update:show 事件支持 v-model:show,并在问诊支付、问诊详情页面复用。
  3. 能根据处方状态与订单生成情况实现“购买药品”跳转:无效处方提示、未支付且无订单进入药品支付、其他情况进入药品订单详情。
  4. 能定义药品预支付与收货地址类型,调用对应 API,在挂载时加载并渲染收货地址、手机号脱敏、药品清单、金额及加载骨架。
  5. 能实现药品支付流程:校验用户协议、收货地址和处方 ID,创建药品订单并缓存订单 ID,复用支付抽屉并支持动态回跳地址。
  6. 能实现药品支付结果页:根据订单 ID 获取订单详情,按待支付状态条件渲染成功/失败结果,并提供查看订单与返回问诊室跳转。

知识点 ​

功能目标:把订单编号复制到剪贴板 ​

为什么需要:用户找客服时经常要提供订单编号。订单编号通常很长,手动选中、复制容易漏字符,体验也差。因此在问诊详情页提供一个“复制”按钮,点击后直接把当前订单编号写入剪贴板,是成本低、收益明确的小功能。

怎么想:复制按钮不负责“展示订单编号”,只负责“读取当前订单数据里的编号并写入剪贴板”。所以处理函数里要拿到 order.value.orderNumber,调用浏览器剪贴板能力;同时要考虑浏览器是否允许复制,以及复制成功后给用户一个轻提示。

怎么做:本节采用 VueUse 提供的 useClipboard。转写稿里出现的“usec bad”“yels clip bed”“use clip back”都是同音/误写,实际是 useClipboard。它返回的关键内容有 copy 复制函数和 isSupported 是否支持复制。copy 的返回值是 Promise,因此复制成功后可以再提示“已复制”。

先确认 useClipboard 的用法和限制 ​

为什么需要:不能一上来就调用复制 API。剪贴板 API 受安全上下文限制,转写中明确提到:只有在 HTTPS 网站下,或本地 localhost 开发环境下才能使用;HTTP 非 localhost 不支持。所以要先判断权限/支持情况。

怎么想:useClipboard 可以不传默认值,直接给 copy 传一个字符串;也可以传初始值。本节不需要默认值,只需要拿到 copy 和 isSupported。当 isSupported 为假时,终止复制并给用户轻提示;为真时再调用 copy。

怎么做:在组件中解构:

ts
const { copy, isSupported } = useClipboard()

易错点:不要以为“本地 localhost 能复制,线上 HTTP 也一定能复制”。本地开发环境通常不检查权限,线上非 HTTPS 会失败;另外用户拒绝浏览器剪贴板授权后,也不能继续复制,所以要保留失败提示。

在组件中实现 onCopy 处理函数 ​

为什么需要:复制逻辑要绑定到按钮点击事件上,并且要处理“不支持复制”“复制成功”“编号可能为空”等情况。

怎么想:准备一个 onCopy 函数,作为复制按钮的点击处理函数。函数内部先判断 isSupported.value,不支持就 showToast 提示并 return;支持则调用 copy,传入订单编号。转写中提到 orderNumber 可能是 undefined,而 copy 要求字符串,所以用默认空串或空值兜底,让类型检查通过。

怎么做:按转写还原关键代码:

ts
const onCopy = async () => {
  if (!isSupported.value) {
    showToast('未授权或不支持复制')
    return
  }

  await copy(order.value.orderNumber ?? '')
  showToast('已复制')
}

模板中给复制按钮绑定点击事件:

html
<button @click="onCopy">复制</button>

易错点:第一,不判断 isSupported 就直接复制;第二,copy 是 Promise,不 await 就提示“已复制”,可能复制失败也提示成功;第三,order.value.orderNumber 可能为空,未做兜底会导致类型检查不通过;第四,提示文案要区分“未授权/不支持”和“已复制”,不要混用。

调试与验证过程 ​

为什么需要:复制功能不是页面上看得见的变化,必须实际粘贴验证。

怎么想:在本地 localhost 开发环境点击复制按钮,然后到控制台或输入框里粘贴,检查剪贴板中是否出现订单编号。转写中就是这样验证的:点击复制后,在控制台粘贴,能看到订单编号已经进入剪贴板。

怎么做:刷新项目,进入问诊详情,点击“复制”,再到控制台粘贴验证。若失败,依次检查:isSupported 是否为真、浏览器是否弹过权限提示、copy 是否抛错、传入的是否是字符串。最后提交代码,提交说明可为“问诊详情-复制订单编号”。

易错点:不要把“按钮点击了”当成“复制成功了”;也不要在本地验证通过后就忽略 HTTPS 环境差异。复制功能的核心不是按钮样式,而是权限判断、Promise 处理和用户提示三件事。

为什么把支付抽屉抽成组件 ​

为什么需要:问诊支付页点击“立即支付”会弹出支付抽屉;问诊详情在待支付状态下点击“继续支付”,也要弹出同样的抽屉。如果两处各写一份模板和支付逻辑,后续改支付方式、改回跳地址都要改两遍,容易漏。

怎么想:先判断哪些是“公共的”:抽屉模板、支付方式选择、调用支付、生成支付地址、跳转第三方平台。哪些是“使用处不同的”:当前订单 ID、实付金额、抽屉显示隐藏、关闭前是否拦截。公共的留在组件内,不同的通过 props 传进去。

怎么做:在 components 下新建通用支付抽屉组件(转写稿中称 CpPay 组件),先从 consult-pay 页面把支付抽屉的模板结构拷过来,再把 pay 支付函数和 paymentMethods 支付方式数据拷过来。拷完后会出现标红,这些标红项就是组件缺少、需要由父组件传入的内容。

易错点:不要一上来就改逻辑,先搬模板再搬逻辑,最后看缺什么补什么;拷过来的 ref、轻提示、加载中提示、获取支付地址 API 等要重新导入;无用的导入要清理,否则类型检查和构建会报错。

先搬模板再搬逻辑 ​

为什么需要:支付抽屉不是纯展示组件,它内部还要完成“选支付方式→立即支付→拿支付地址→跳转”的流程。只搬模板会导致点击无反应。

怎么想:模板里原来直接使用的 show、orderId、实付金额、onClose,在组件内部不再天然存在,要区分“组件内部状态”和“父组件传入数据”。支付方式 paymentMethods、支付函数 pay 属于组件内部逻辑,可以保留;订单 ID、金额、显示隐藏属于外部数据。

怎么做:把支付抽屉模板放进新组件,保留支付方式选择和支付按钮逻辑;把原来页面里的 pay 函数、paymentMethods 一并迁入;然后逐个处理标红项,记录需要暴露的 props。

易错点:不要把整个问诊支付页面都搬进抽屉组件;show 不是组件自己创建的 ref,而是父组件控制;orderId 不要继续写 orderId.value,因为 props 不是 ref。

用 props 补齐缺失数据 ​

为什么需要:支付抽屉要知道“给哪个订单支付”“显示多少钱”“是否显示”“关闭前要不要拦截”,这些只有使用它的页面知道。

怎么想:按转写稿记录四类缺失:show 控制显示隐藏;orderId 订单 ID;actualPayment 实付金额;onClose 关闭前引导函数。其中 onClose 不是所有页面都需要:问诊支付要引导用户不要关闭,问诊详情不需要刻意拦截,所以设为可选。

怎么做:用 defineProps 约定类型,核心写法如下:

ts
const props = defineProps<{
  show: boolean
  orderId: string
  actualPayment: number
  onClose?: () => void
}>()

模板中订单 ID 改为 props.orderId,实付金额改为 props.actualPayment。onClose 按项目原有 action-sheet 的关闭前钩子接入,未传时不拦截。

易错点:orderId 必须是字符串类型;actualPayment 是数字类型;onClose 要加问号表示可选;父组件传参时用 kebab-case,例如 :order-id、:actual-payment、:on-close。

用 v-model:show 打通显示隐藏 ​

为什么需要:父页面要能打开抽屉,抽屉关闭时也要把“已关闭”同步回父页面。只传 show 只能单向控制,用户点关闭后父页面的 show 不会变。

怎么想:Vue 的 v-model:show 本质是 :show="show" 加 @update:show="show = $event"。子组件要接收 show,并在 action-sheet 触发 update:show 时向外抛同名事件。

怎么做:定义 emits:

ts
const emit = defineEmits<{
  (e: 'update:show', value: boolean): void
}>()

模板里 action-sheet 不要直接 v-model 绑定 props.show,因为 props 不能直接修改。应写成 :show="props.show",监听它的 update:show,再调用 emit('update:show', val) 通知父组件。这样组件就支持 v-model:show。

易错点:事件名必须是 update:show,不能写成 updateShow;emit 参数必须是布尔值;不要直接给 props.show 赋值;action-sheet 自身也有 update:show,要把它转成自己的 emit。

在两个页面复用与调试 ​

为什么需要:组件封装完必须验证“问诊支付能用”和“问诊详情能用”,否则只是代码搬家。

怎么做:在问诊支付页使用支付抽屉组件,绑定 v-model:show="show"、:order-id、:actual-payment,并传入 :on-close 做关闭拦截。在问诊详情页的“继续支付”按钮上绑定点击事件,把 show 改为 true;页面下方使用组件,绑定当前问诊订单 ID 和实付金额,不传 on-close。

调试时先测问诊支付:点击立即支付能否打开抽屉、选择支付宝后能否跳转第三方;再测问诊详情:点击继续支付能否打开抽屉、样式是否正常。转写稿中第一次测试发现样式有问题,随后把支付抽屉样式拷到组件底部,并清理无用导入。

易错点:问诊详情不要传 on-close;show 默认应为 false;订单 ID 要取当前 item 的订单 ID,不要取错;样式要跟着组件走;改完组件后要回归问诊支付页,避免旧页面绑定失效。

需求判断:为什么不是所有“购买药品”都进支付页 ​

为什么需要:处方消息卡片底部有“购买药品”按钮,但处方有状态,不能无脑跳支付页。过期处方不能支付;已支付或已经生成过订单的处方,也不应重复创建药品订单。
怎么想:先判断处方是否有效,再判断“未支付且没有订单ID”,只有这个组合才进入药品支付;其他情况进入药品订单详情。
怎么做:在处方消息卡片里给按钮绑定点击事件,把当前处方对象传给处理函数。处理函数里先排除空处方,再按状态分支跳转。
易错点:处方参数可能是 undefined,不能直接读 p.status;无效处方要提示“处方已失效”后终止;未支付但已有 orderId 时也应去详情,而不是再次支付。

(本节笔记截图位置:处方消息卡片底部“购买药品”按钮。)

点击事件与类型处理:先把可选处方收窄 ​

为什么需要:模板里的处方数据来自消息对象,类型上可能是可选值。若不先处理空值,后续判断和路由跳转都会报类型错误或运行时报错。
怎么想:函数参数写成可选,进入函数第一件事就是 if (!p) return,把类型从“可能没有”收窄为“一定有”。
怎么做:

ts
// 处方消息卡片:点击“购买药品”
const router = useRouter()

const goPay = (p?: Prescription) => {
  if (!p) return

  if (p.status === PrescriptionStatus.Invalid) {
    showToast('处方已失效')
    return
  }

  if (p.status === PrescriptionStatus.Unpaid && !p.orderId) {
    router.push({ path: '/order-pay', query: { id: p.id } })
    return
  }

  router.push({ path: '/order-detail', query: { orderId: p.orderId } })
}

易错点:PrescriptionStatus 要从项目枚举中导入;未支付状态、无效状态不要手写字符串;showToast 是轻提示,不要写成 alert;跳支付页带的是处方ID,跳详情带的是订单ID,二者不能混。

分支跳转:三条路径对应三种业务结果 ​

为什么需要:同一个按钮在不同处方状态下结果不同,分支必须清楚,否则会出现“已支付还去支付”“过期处方还能下单”等问题。
怎么想:按业务优先级排列:空处方直接返回;无效处方提示;未支付且无订单去支付;剩余情况去订单详情。
怎么做:用 router.push 跳转。药品支付页用 query.id 传处方ID,后续页面从地址栏取这个ID拉预支付信息;订单详情用 query.orderId 传订单ID。
易错点:转写稿里“路径上拼接”容易听成路径参数,但后续页面实际通过 route.query.id、route.query.orderId 取参,所以这里统一用 query;不要漏掉 return,否则会继续执行后面的详情跳转。

配置药品支付路由与页面骨架 ​

为什么需要:点击后要落到药品支付页,如果没有路由和组件,页面会白屏或 404。
怎么想:在 order 模块下新建 order-pay.vue,在路由表里增加 /order-pay,标题设为“药品支付”;页面结构先复制笔记中的基础布局,数据渲染留到下一节。
怎么做:

ts
{
  path: '/order-pay',
  component: () => import('@/views/order/order-pay.vue'),
  meta: { title: '药品支付' }
}

易错点:组件路径要放在 order 文件夹下;路由地址不要写成问诊支付;标题别复制成旧标题;页面骨架先搭好,不要在本节提前写预支付接口逻辑。

调试过程与易错清单 ​

为什么需要:这类跳转逻辑必须用真实处方状态验证,否则只测一种状态很容易漏分支。
怎么想:先在问诊室找到处方卡片,点击“购买药品”,观察地址栏是否进入 /order-pay?id=处方ID;再检查页面是否显示基础布局。
怎么做:浏览器刷新后点击按钮,正常未支付且无订单的处方应进入药品支付页;若白屏,先查路由是否注册、组件路径是否正确;若提示失效,查处方状态枚举;若直接进详情,查 orderId 是否已有值。
易错点:本地开发环境能正常跳转不代表线上权限问题;本节只做“进入药品支付”的跳转与骨架,预支付信息、收货地址、支付抽屉是后续内容,不要提前混入。

代码示例 ​

1. 问诊详情 —— 复制订单编号(useClipboard) ​

vue
<script setup lang="ts">
import { useClipboard } from '@vueuse/core'
import { showToast } from 'vant'

const props = defineProps<{ orderNumber?: string }>()

// useClipboard 返回 copy 函数和 isSupported(当前环境是否允许使用剪贴板)
const { copy, isSupported } = useClipboard()

// 点击「复制」按钮时触发
const onCopy = async () => {
  // 1. 先判断浏览器/协议是否支持剪贴板 API
  if (!isSupported.value) {
    showToast('未授权,不支持复制')
    return
  }
  // 2. copy 必须传字符串,返回值是 Promise,成功后再提示
  await copy(props.orderNumber ?? '')
  showToast('已复制')
}
</script>

<template>
  <div class="order-no">
    <span>{{ props.orderNumber }}</span>
    <van-button size="mini" plain type="primary" @click="onCopy">复制</van-button>
  </div>
</template>

关键行与易错点

  • isSupported 必须先判断:只有 HTTPS 协议或 localhost 本地开发环境才允许调用剪贴板 API,普通 HTTP 下会失败,此时给用户轻提示而不是静默报错。
  • copy(xxx) 的参数必须是字符串,订单编号字段可能是 undefined,所以用 ?? '' 兜底,否则类型检查和运行都会出问题。
  • copy 返回的是 Promise,处理函数要写成 async,await 之后再 showToast('已复制'),这样提示和复制结果才对应得上。
  • 本地 localhost 下浏览器不会弹授权框,容易误以为「权限判断没用」,要理解这是环境差异而不是代码问题。

2. 支付抽屉组件封装(props + v-model:show + 事件) ​

vue
<script setup lang="ts">
import { ref } from 'vue'
import { showLoadingToast, showToast } from 'vant'
import { getPayUrl } from '@/services/consult'

// 从原问诊支付页面搬过来的逻辑所需的数据与函数
const props = defineProps<{
  show: boolean                 // 控制抽屉显隐
  orderId: string               // 订单 ID
  actualPayment: number         // 实付金额
  onClose?: () => void          // 关闭前的引导函数,可不传
}>()

// 支持 v-model:show 双向绑定
const emit = defineEmits<{
  (e: 'update:show', value: boolean): void
}>()

const payment = ref(1)
// 支付方式列表沿用原问诊支付页面的数据
const paymentMethods = ref<{ value: number; label: string }[]>([])

// 点击「立即支付」:根据订单 ID 拿支付地址并跳转第三方
const pay = async () => {
  const loading = showLoadingToast({ message: '加载中...', forbidClick: true, duration: 0 })
  try {
    const { data } = await getPayUrl(props.orderId)
    loading.close()
    window.location.href = data.url
  } catch (e) {
    loading.close()
    showToast('支付失败')
  }
}
</script>

<template>
  <van-action-sheet
    :show="props.show"
    @update:show="emit('update:show', $event)"
    :before-close="props.onClose"
  >
    <div class="pay-sheet">
      <h3>选择支付方式</h3>
      <van-radio-group v-model="payment">
        <van-cell-group>
          <van-cell
            v-for="item in paymentMethods"
            :key="item.value"
            :title="item.label"
            clickable
            @click="payment = item.value"
          >
            <template #right-icon>
              <van-radio :name="item.value" />
            </template>
          </van-cell>
        </van-cell-group>
      </van-radio-group>

      <div class="pay-amount">
        实付款:<span>¥{{ props.actualPayment }}</span>
      </div>
      <van-button type="primary" block @click="pay">立即支付</van-button>
    </div>
  </van-action-sheet>
</template>

关键行与易错点

  • 抽屉内缺少的东西(show、orderId、actualPayment、onClose)全部通过 defineProps 暴露;onClose 加 ? 因为问诊详情页不需要拦截关闭,只有问诊支付页才传。
  • 模板里不能对 props.show 用 v-model:props 是只读的,必须写成 :show="props.show" + @update:show="emit('update:show', $event)",配合 defineEmits 声明 update:show,才能让父组件用 v-model:show。
  • 事件名必须是 update:show(冒号不能写错),这是 v-model:show 的约定;$event 就是 action-sheet 抛出的新布尔值。
  • 关闭前引导用 before-close 绑定 props.onClose,未传时值为 undefined,van 会按「允许关闭」处理,不会报错。
  • orderId 不能用 orderId.value,因为它已经是 props 而不是 ref。
  • 改造后记得跑一遍类型检查(pnpm type-check),能快速定位到 consult pay、consult detail 两个旧使用处缺少必传属性。

3. 药品订单 —— 点击「购买药品」的路由判断 ​

ts
// 处方消息卡片中的购买药品按钮逻辑
import { useRouter } from 'vue-router'
import { showToast } from 'vant'
import type { Prescription } from '@/types/consult'
import { PrescriptionStatus } from '@/enums'

const router = useRouter()

const buy = (prescription?: Prescription) => {
  // 1. 类型守卫:处方信息可能是 undefined,直接返回
  if (!prescription) return

  // 2. 无效处方:提示后终止
  if (prescription.status === PrescriptionStatus.Invalid) {
    showToast('处方已失效')
    return
  }

  // 3. 未支付 且 从未根据该处方生成过订单 → 去药品支付页
  if (
    prescription.status === PrescriptionStatus.NotPayment &&
    !prescription.orderId
  ) {
    router.push(`/order/pay?id=${prescription.id}`)
    return
  }

  // 4. 其他情况一律去药品订单详情
  router.push(`/order/${prescription.orderId}`)
}
vue
<!-- 处方卡片底部按钮 -->
<template>
  <van-button type="primary" size="small" @click="buy(item.msg.prescription)">
    购买药品
  </van-button>
</template>

关键行与易错点

  • 函数参数要声明为可选(prescription?: Prescription),因为卡片数据里处方字段可能是 undefined;先做 if (!prescription) return 的类型收窄,后面才能安全访问 status、orderId。
  • 判断顺序不能乱:先失效 → 再未支付且无订单 → 其余走详情,否则会把失效处方也跳去支付页。
  • 「未支付」和「没生成过订单」是两个条件同时满足才去支付页,orderId 有值说明订单已生成,应直接去详情,避免重复下单。
  • 路由跳转要用 useRouter() 拿到 router 实例,push 的路径按约定拼处方 ID / 订单 ID,别漏掉 id 参数,否则支付页拿不到 route.query.id 就无法拉预支付信息。
  • 轻提示统一用 showToast,无效处方提示后必须 return,不然会继续往下执行。

常见错误 ​

  1. 复制按钮点了没反应,或控制台报「copy 参数必须是 string」 现象:onCopy 里调用 copy(order.value.orderNumber),TS 报红、运行时偶尔复制出 "undefined"。 原因:订单号在类型上是可选的(string | undefined),useClipboard 的 copy 只接受字符串,且未做空值兜底。 修法:copy(order.value.orderNumber ?? ''),并在调用前判断 if (!supported) return showToast('当前环境不支持复制');copy 返回 Promise,成功后再 showToast('已复制')。

  2. 支付抽屉里仍写 v-model="show",抽屉关不掉 / 控制台警告「Set operation on key failed」 现象:点击遮罩或取消,action-sheet 不消失,或报 props 只读警告。 原因:show 已从组件内部 ref 变成 defineProps 的只读属性,v-model 会尝试回写 props。 修法:模板改成 :show="props.show",并监听 @update:show="val => emit('update:show', val)";父组件才用 v-model:show。

  3. 父组件 v-model:show 失效,抽屉能开不能关 现象:父页面 show = true 能打开,但抽屉内部关闭后父页面 show 仍是 true,再次点击「继续支付」无反应。 原因:只声明了 show 这个 prop,漏了 defineEmits 里的 update:show,子组件状态变化没有向上通知。 修法:const emit = defineEmits<{ (e: 'update:show', v: boolean): void }>(),在 action-sheet 的 update:show 回调里转发出去。

  4. 给 pay-callback 加必填 prop 后,其他页面运行时报错或跳到 /undefined 现象:药品支付正常,但从问诊支付/问诊详情进入时支付成功跳到了错误路由。 原因:pay-callback 设为必填,历史调用点没传,编译期没拦截住(或直接跳过了检查)。 修法:改完公共组件立刻跑 pnpm type-check,按提示给 consult-pay、consult-detail 补 pay-callback="/room";如确实可缺省,就设为可选并给默认值。

  5. 支付结果页同时显示骨架屏和真实内容,或条件渲染永远走「支付成功」分支 现象:骨架屏一直挂在页面上;或无论是否支付成功都显示成功界面。 原因:一是漏写 v-if / v-else 的配对;二是 order.status 的类型没从 OrderType 枚举里取,退化成 any/字符串比较失败。 修法:骨架屏加 v-if="!order"、内容加 v-else;状态判断用枚举 OrderType.MedicinePay 而非硬编码字符串,并确认枚举已 import。

  6. 连点「立即支付」生成多笔药品订单 现象:网络稍慢时快速点两次,后台多出两条订单。 原因:onSubmit 里没有「已有 orderId 就不再请求」的短路,也没用 loading 锁住按钮。 修法:入口先 if (orderId.value) { show.value = true; return },请求前置 loading = true,try/finally 中复位,按钮绑 :loading。

练习 ​

  1. 给复制功能加降级方案:当 useClipboard 的 isSupported 为 false(如 HTTP 环境)时,改用 document.execCommand('copy') 或提示用户手动长按复制,保证功能可用。
  2. 为支付抽屉增加一个可选 prop title(默认「选择支付方式」),并同步在 consult-pay、consult-detail、order-pay 三处调用点验证不传时行为不变。
  3. 把「处方有效性 → 是否已生成订单 → 跳转目标」的判断抽成一个纯函数 resolvePrescriptionTarget(prescription),返回路由对象,并为其写 3 条单元测试(失效 / 未支付无订单 / 其他)。
  4. 在药品支付页给「立即支付」加防抖或 loading 锁,构造慢网络场景验证:连续点击 5 次只产生 1 个订单号。
  5. 改造支付结果页,增加「支付中」的第三种状态展示(订单状态仍为待支付但刚回跳、且 3 秒内允许轮询一次详情),并说明何时停止轮询。

课件图示 ​

课件第3页:药品订单-进入药品支付 诊室有效处方 未支付 没订单 药品支付 药品订单

课件第7页:药品订单-完成支付 生成药品订单API 生成订单 改造支付抽屉组件 实现药品支付

课件第5页:药品订单-支付信息 预支付信息类型 预支付&地址API 预支付&地址数据 展示页

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

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

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

原视频对照 ​

#视频时长
1问诊详情-复制订单编号06:48
2问诊详情-支付抽屉组件17:16
3药品订单-进入药品支付09:03
4药品订单-支付信息展示17:55
5药品支付-进行支付18:31
6药品订单-支付结果13:52

小结 ​

  • 可复用组件的边界不在模板,而在 props / emits:凡是「组件内需要但自己造不出来」的数据一律 props 进来,「组件内改了要外面知道」的一律 emit 出去,模板搬运只是第一步。
  • 双向绑定不是语法糖问题,而是契约问题:v-model:show 必须由 :show + update:show 成对实现,缺一半就会出现「能开不能关」这类半瘫状态。
  • 公共组件的每一次签名变更都要用类型检查兜底。pnpm type-check 在这里的价值不是找错,而是列出所有需要一起改的调用点,把重构风险从运行时前移到编译时。
  • 状态判断一律用枚举,不要用字面量字符串。订单状态这类跨端共享的语义,一旦写死字符串,后台加一个状态就静默走错分支。
  • 页面级数据展示的统一套路是「先有类型 → 再有 API → 挂载取数 → 有数据才渲染」,骨架屏与内容用 v-if / v-else 严格互斥,避免出现空数据下的白屏或双份渲染。
  • 支付这类会产生副作用的操作,幂等性要在前端先兜一层:已有 orderId 就不再建单,只重开抽屉,把重复提交挡在请求之前。
  • 判断「支付是否成功」不要看回跳本身,要看服务端订单状态——回跳只代表用户从第三方平台回来了,不代表钱到了。