Skip to content

评价医生与订单详情(一) ​

本页对应 评价医生与订单详情,共 7 段视频,约 1 小时 32 分。

学习目标 ​

  1. 能够根据消息类型条件渲染结束问诊消息与评价卡片组件,并利用评价数据的有无切换“已评价/未评价”两种展示状态,完成评分、评价内容、匿名标记的表单数据收集及提交按钮禁用状态控制,且在提交前对评分和内容做非空校验。
  2. 能够通过 provide/inject 将问诊订单信息跨级注入评价卡片组件,调用评价 API 提交医生 ID、订单 ID、评分、内容及匿名标记,并在提交成功后更新当前消息为已评价状态、正确回显评分。
  3. 能够搭建问诊记录页面(Item、List、页面组件三级结构),配置路由并在三个选项卡中通过 props 向对应列表组件传递问诊类型。
  4. 能够定义问诊订单列表的请求参数与响应数据类型,封装列表 API,并基于 van-list 实现滚动加载更多,根据当前页码与总页数判断是否加载完成。
  5. 能够根据问诊订单状态渲染对应的操作按钮(待支付、待接诊、咨询中、已完成、已取消),为状态文字添加对应颜色类名,并实现基本信息展示与详情页跳转。
  6. 能够使用 van-popover 实现“更多操作”气泡弹窗,通过计算属性根据处方 ID 控制选项禁用状态,并实现取消订单功能(API 调用、按钮 loading、try/catch/finally 异常处理及成功后状态与文字同步更新)。

知识点 ​

本节目标与整体思路 ​

问诊结束后,诊室会被医生手动关闭,或在超时后自动关闭。关闭后会产生两类新消息:一类是“结束问诊”的普通通知消息,另一类是“评价医生”的卡片消息。本节要完成的是:先把结束消息展示出来,再把评价卡片组件接入消息列表,最后收集评价表单数据,并在点击提交前做基础校验。整体顺序是:普通消息展示 → 评价卡片展示 → 表单数据收集 → 提交前校验。转写稿里“离开消息”是同音识别,按业务应统一理解为“结束消息”;“味品家/以评价”分别指“未评价/已评价”。

展示结束问诊消息 ​

为什么需要:诊室关闭后,用户需要明确看到“医生已关闭诊室”这类状态,否则聊天界面会显得突然中断。
怎么想:消息列表里已经按消息类型区分了多种模板,结束消息本质是普通消息,只是类型不同,所以不需要单独做复杂组件,只要在消息项组件里按类型条件渲染。
怎么做:打开 room/components 下的消息项组件,找到结束消息模板,取消注释或补上条件:当 item.message.type 等于结束通知类型时,展示 item.message.content。关键逻辑可整理为:

vue
<!-- 条件:消息类型为结束通知时展示 -->
<div v-if="item.message.type === MessageType.结束通知">
  {{ item.message.content }}
</div>

调试时刷新页面,此时诊室还是“咨询中”;到超级医生端点击“结束问诊”并确认,再回到诊室,就能看到“医生关闭诊室”的灰色消息。
易错点:不要被转写稿里的“离开消息”带偏,业务含义是结束消息;类型枚举名要以项目实际定义为准;展示字段是 content,不要误取其他字段。

抽取并展示评价卡片组件 ​

为什么需要:评价区域包含星级、评价内容、匿名勾选、提交按钮等交互,如果全部塞在消息列表里,消息项组件会变得臃肿,所以单独抽成 EvaluateCard 组件。
怎么想:这个组件有两种状态:已评价时只展示评分;未评价时展示表单。父级消息列表只负责在“已评价”或“未评价”消息类型下渲染组件,并把 item.message.evaluateDoctor 传进去。组件内部根据这个数据是否存在切换状态。
怎么做:在 components 下新建评价卡片组件,消息列表里导入并使用:

vue
<div class="message message-comment">
  <EvaluateCard :evaluateDoctor="item.message.evaluateDoctor" />
</div>

展示条件要同时包含已评价和未评价两种消息类型,因为组件内部还要区分两种状态。组件接收数据时,evaluateDoctor 要写成可选,因为未评价时它本来就是 undefined:

ts
defineProps<{ evaluateDoctor?: EvaluateDoctor }>()

组件内用 v-if="evaluateDoctor" 展示已评价,v-else 展示表单。调试时刷新页面,可以看到评价消息卡片;再通过 Vue Devtools 观察传入的数据是否存在。
易错点:只判断“已评价”会漏掉未评价卡片;evaluateDoctor 写成必填会在未评价时报警;已评价和未评价的切换依据是数据是否存在,不要靠额外布尔值硬编码。

收集评价表单数据 ​

为什么需要:提交评价前必须拿到评分、评价内容、是否匿名三个数据,否则后续接口没有参数可传。
怎么想:评分和内容用 ref 收集,匿名默认不勾选;提交按钮的灰色/绿色样式由计算属性控制,条件是评分和内容都填写。
怎么做:在评价卡片组件中准备:

ts
const score = ref(0)
const content = ref('')
const anonymousFlag = ref(false)

const disabled = computed(() => !score.value || !content.value)

模板中分别用 v-model 绑定评分、评价内容、匿名勾选;提交按钮根据 disabled 加禁用类名,并绑定点击事件 onSubmit。调试时打开 Vue Devtools,点击星级看 score 是否变化,输入内容看 content 是否收集,勾选匿名看 anonymousFlag 是否在 true/false 间切换;两个必填项都填写后按钮应从灰色变绿色。
易错点:匿名默认值是 false,不要写成字符串;按钮的禁用类名只是样式,不代表真正不可点击;字段命名要跟后台接口约定保持一致,避免提交时再改名。

提交前校验 ​

为什么需要:虽然按钮有灰色禁用样式,但它仍然可以被点击,所以提交前必须再校验一次,防止空评分或空内容进入提交流程。
怎么想:点击提交时先判断评分,再判断内容,任何一项不满足就提示并 return,不要继续往下执行。
怎么做:在 onSubmit 中写:

ts
if (!score.value) {
  showToast('请选择评分')
  return
}
if (!content.value) {
  showToast('请填写评价')
  return
}

调试时刷新页面,不填任何内容直接点“提交评价”,应先提示“请选择评分”;选完评分再点,应提示“请填写评价”;两项都填写后流程正常。
易错点:提示后必须 return,否则还会继续执行;校验顺序按转写稿先评分后内容;本节只做到提交前校验,真正调用提交接口属于下一节内容,不要提前混入。

先分清两条消息:结束消息与评价消息 ​

问诊结束后,诊室可能被医生关闭,也可能到时自动关闭。关闭后消息列表里会出现两类新消息:一条是“结束问诊”的普通通知消息,另一条是“评价医生”的卡片消息。为什么需要先分清?因为二者展示方式不同:结束消息只是灰色通知,评价消息包含表单、校验、提交和状态切换,不能混在普通文本里处理。

怎么想:先在消息列表组件 RoomMessage.vue 中按 messageType 判断。结束消息类型命中时,展示 message.content;评价消息则单独抽成 EvaluateCard.vue,由父组件传入评价数据。怎么做:在消息模板中增加条件分支,结束消息用普通消息容器展示;评价消息在“已评价”和“未评价”两种类型下都渲染评价卡片。易错点:不要只判断“已评价”,否则未评价表单不会出现;也不要把评价卡片直接写进消息列表,否则交互和状态会越来越乱。

评价卡片为什么要单独抽离 ​

评价卡片包含星级评分、评价内容输入、匿名勾选、提交按钮,还有“已评价/未评价”两种状态。为什么抽组件?因为交互多、状态多,放在消息列表里会导致模板臃肿。怎么想:组件只负责展示和收集表单数据,提交动作通过事件或父级注入的方法完成;已评价时展示分数,未评价时展示表单。怎么做:在 RoomMessage.vue 中导入 EvaluateCard,并传入 item.message.evaluateDoctor。组件内用 defineProps 接收,类型设为可选,因为未评价时该字段不存在。

ts
const props = defineProps<{
  evaluateDoctor?: EvaluateDoctor
}>()

模板中根据 evaluateDoctor 是否存在切换:

vue
<div v-if="evaluateDoctor">已评价:{{ evaluateDoctor.score }} 分</div>
<div v-else>评价表单</div>

易错点:evaluateDoctor 必须允许 undefined,否则父组件传值会报类型错误;已评价卡片里的分数不要写死,转写稿调试时就出现过“评价 4 分却显示 3 分”,最后改为读取评价消息中的 score。

表单数据收集与按钮禁用 ​

为什么需要收集?提交评价需要评分、内容、是否匿名三个表单值。怎么想:评分默认 0,内容默认空串,匿名默认 false;提交按钮是否禁用由评分和内容是否填写决定。怎么做:用 ref 定义 score、content、anonymousFlag,用计算属性控制禁用样式。

ts
const score = ref(0)
const content = ref('')
const anonymousFlag = ref(false)

const disabled = computed(() => {
  return !score.value || !content.value
})

模板绑定:

vue
<div v-model="score">星级评分</div>
<input v-model="content" />
<input type="checkbox" v-model="anonymousFlag" />
<button :class="{ disabled }" @click="onSubmit">提交评价</button>

易错点:按钮只是“灰色禁用样式”,并不等于不能点击。转写稿中明确验证过:灰色按钮仍可点击,所以 onSubmit 里必须再次校验:没有评分提示“请选择评分”,没有内容提示“请填写评价”。

提交评价:API、跨级注入与类型守卫 ​

为什么需要 API?评价不是通过 socket 提交,而是走传统接口。转写稿中接口需要 5 个参数:doctorId、orderId、score、content、anonymousFlag。但卡片组件只有后三项,前两项在问诊订单详情里,而订单详情与卡片之间隔着 RoomMessage,属于跨级通信。怎么想:用 provide/inject 注入问诊订单 consult。怎么做:在问诊室页面 provide('consult', consult),在评价卡片中 inject('consult'),并补类型。

ts
provide('consult', consult)

const consult = inject<Ref<ConsultOrderItem>>('consult')

提交时先做类型守卫:consult.value 不存在就提示“未找到订单”;doctorInfo 不存在也不能继续。匿名值要转成后台需要的 0/1:

ts
anonymousFlag: anonymousFlag.value ? 1 : 0

易错点:inject 后不写类型会得到 unknown;接口参数要求必填,而 consult 可能为空,必须先用条件判断收窄类型,否则 TypeScript 会报“可能为 undefined”。

提交成功后修改消息状态 ​

为什么还要改状态?提交成功后,当前卡片应从“未评价”变成“已评价”,并显示分数。怎么想:消息数据由问诊室页面提供,所以修改函数也应定义在提供方,遵循“数据由谁提供,由谁修改”。怎么做:在问诊室页面定义 completeEvaluate(score),找到当前未评价消息,给它补上评价信息,并把消息类型改为已评价;再通过 provide 把函数注入给评价卡片,提交成功后调用。

ts
function completeEvaluate(score: number) {
  const item = list.value.find(
    i => i.messageType === messageType.cardEva
  )
  if (!item) return
  item.message.evaluateDoctor = { score }
  item.messageType = messageType.cardEvaDone
}

易错点:查找消息时要确认当前未评价消息的唯一性;修改后已评价卡片要读取 evaluateDoctor.score,不要继续显示写死分数。调试时先看网络请求是否成功,再看卡片状态是否切换,最后刷新页面确认后台状态和分数一致。

本节目标与页面拆分 ​

为什么需要:问诊记录页面不是单个组件能完成的,它既有列表项,又有不同订单类型的选项卡。如果不先拆分,页面结构会和数据逻辑混在一起,后续加载数据、渲染 Item、取消订单都会很难维护。

怎么想:按“最小复用单元 → 列表容器 → 页面容器”拆。最小单元是 ConsultItem,若干 Item 放进 ConsultList,三个 List 再放进 ConsultPage。页面外层由导航和选项卡组成,三个选项卡分别对应极速问诊、找医生、开药问诊。

怎么做:在用户模块下建立页面和组件目录:components/ConsultItem.vue、components/ConsultList.vue、views/user/ConsultPage.vue。Item 和 List 的结构从笔记中复制,先保证结构存在,不急着写完整业务。

易错点:不要把三个选项卡写成三份重复页面;不要把 Item 的数据请求写进 Item 组件,Item 只负责展示和触发操作。

先准备 Item 与 List 组件 ​

为什么需要:Item 是每条问诊记录的展示单元,List 负责承载多条 Item,并为后续“加载更多”留位置。

怎么想:先让 List 里临时遍历 5 个 Item,验证组件结构能渲染;等后续加载数据时,再把写死的遍历改成真实列表数据。

怎么做:在 components 下新建 ConsultItem.vue,复制笔记中的 Item 结构;再新建 ConsultList.vue,导入 ConsultItem,在模板中遍历展示。转写稿中 List 组件先遍历 5 个 Item,这是搭建阶段的临时结构。

易错点:组件名、文件名和导入路径要一致;ConsultItem 与 ConsultList 不要互相循环导入。

搭建 ConsultPage 页面 ​

为什么需要:页面组件负责组织导航、选项卡和三个 List,是路由真正挂载的入口。

怎么想:页面只做布局和类型分发,不直接处理每条记录的按钮逻辑。三个选项卡下分别放一个 ConsultList。

怎么做:在 views/user 下新建 ConsultPage.vue,从笔记复制页面结构。页面中包含标题/导航和三个选项卡,每个选项卡下使用 ConsultList。关键结构示意:

vue
<ConsultList :type="ConsultType.Fast" />
<ConsultList :type="ConsultType.Doctor" />
<ConsultList :type="ConsultType.Medication" />

易错点:三个 List 必须传不同 type,否则三个选项卡会查同一类订单;页面组件路径要和路由配置一致。

配置路由与入口跳转 ​

为什么需要:组件准备好后,如果没有路由和入口,浏览器里看不到页面。

怎么想:参照问诊支付的路由规则,在 router/index.ts 中增加问诊记录路由;同时给首页“问诊记录”文字加点击跳转。

怎么做:路由路径按转写稿约定为 /user/consult,组件指向 views/user/ConsultPage.vue,标题为“问诊记录”。入口处使用:

ts
router.push('/user/consult')

转写稿中调试时发现首页“急速问诊”右上角的“问诊记录”点击没效果,后来补上 click 跳转;同时“我的问诊”入口也能进入页面。

易错点:路由路径不要漏掉 /user;入口文字加点击事件时注意不要与父级点击冲突;刷新页面验证路由是否真正生效。

给三个列表传入订单类型 ​

为什么需要:三个选项卡对应不同订单类型,List 必须知道当前查哪一类,后续加载数据才能带对参数。

怎么想:类型统一放在枚举 ConsultType 中:Fast 表示极速问诊,Doctor 表示找医生,Medication 表示开药问诊。页面通过 props 把类型传给 List。

怎么做:页面中给每个 ConsultList 传 type;List 中用 defineProps 接收:

ts
const props = defineProps<{ type: ConsultType }>()

调试时打开 Vue Devtools,找到 List 组件,查看 props.type,转写稿中看到 type = 2,对应极速问诊,说明传值成功。

易错点:不要把枚举当普通字符串随意写;从枚举导入时如果只使用值,不要误加 type 导入;三个选项卡的 type 要分别对应正确枚举值。

调试与验证 ​

为什么需要:搭建页面阶段最容易出现“组件写了但页面不显示”“路由能进但类型不对”的问题,必须逐层验证。

怎么想:先验证路由入口,再验证页面结构,最后验证 List 的 props.type。

怎么做:刷新页面,从首页“急速问诊”点击“问诊记录”,确认进入 /user/consult;再从“我的问诊”进入,确认页面正常;打开 Vue Devtools 查看 List 组件 props.type 是否为预期枚举值。

易错点:只验证一个入口不够;只看页面不查 props 容易漏掉类型传错;临时写死的 5 条 Item 只是结构验证,后续要替换为真实数据。

代码示例 ​

1. 评价医生卡片组件(结束消息后的评价消息 + 提交评价) ​

vue
<!-- src/views/room/components/EvaluateCard.vue -->
<template>
  <div class="evaluate-card">
    <!-- 已评价:只展示分数 -->
    <div v-if="evaluateDoctor" class="evaluated">
      已评价:<span class="score">{{ evaluateDoctor.score }}</span> 分
    </div>

    <!-- 未评价:展示表单 -->
    <div v-else class="evaluate-form">
      <van-rate v-model="score" :count="5" />
      <van-field v-model="content" type="textarea" placeholder="请输入评价内容" />
      <van-checkbox v-model="anonymousFlag">匿名评价</van-checkbox>
      <van-button :class="{ disabled }" @click="onSubmit">提交评价</van-button>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, inject } from 'vue'
import { showToast } from 'vant'
import type { Ref } from 'vue'
import type { ConsultOrderItem, EvaluateDoctor } from '@/types/consult'
import { evaluateConsultOrder } from '@/services/consult'

// 已评价时有数据,未评价时为 undefined,所以必须是可选属性
const props = defineProps<{ evaluateDoctor?: EvaluateDoctor }>()

// 跨级注入:问诊订单详情 + 修改消息状态的函数
const consult = inject<Ref<ConsultOrderItem>>('consult')
const completeEvaluate = inject<(score: number) => void>('completeEvaluate')

const score = ref(0)
const content = ref('')
const anonymousFlag = ref(false)

// 评分或内容为空 -> 按钮置灰
const disabled = computed(() => !score.value || !content.value)

const onSubmit = async () => {
  // 按钮只是“样式禁用”,仍可点击,所以提交前必须再校验
  if (!score.value) return showToast('请选择评分')
  if (!content.value) return showToast('请填写评价')

  // 类型守卫:避免 consult / docInfo 为 undefined 的类型报错
  if (!consult?.value) return showToast('未找到订单')
  if (!consult.value.docInfo) return

  await evaluateConsultOrder({
    docId: consult.value.docInfo.id,   // 医生 ID
    orderId: consult.value.id,         // 问诊订单 ID
    score: score.value,
    content: content.value,
    anonymousFlag: anonymousFlag.value ? 1 : 0 // 后台要 0/1
  })

  // 评价成功后,把“未评价”消息改成“已评价”,并带上分数
  completeEvaluate?.(score.value)
  showToast('评价成功')
}
</script>

要点 / 易错点

  • 评价卡片必须由“已评价 / 未评价”两种消息类型都渲染,组件内部再用 evaluateDoctor 有没有值来切换状态,所以 prop 一定要写成 evaluateDoctor?:,否则父组件会报“缺少必填属性”。
  • disabled 只控制按钮的灰色样式,不能阻止点击,所以 onSubmit 里必须再做“请选择评分 / 请填写评价”两次校验。
  • docId、orderId 不在卡片里,是通过 inject('consult') 从问诊室注入拿到的,这是跨级通讯而不是逐层 props。
  • 已评价卡片上的分数不要写死(转写稿里一开始写死成 3,导致 4 分显示 3 分),要绑定 evaluateDoctor.score。
  • anonymousFlag 是布尔值,接口要 0/1,提交时要 ? 1 : 0 转换。

2. 问诊记录列表——滚动加载更多 ​

vue
<!-- src/views/user/components/ConsultList.vue -->
<template>
  <div class="consult-list">
    <van-list
      v-model:loading="loading"
      :finished="finished"
      finished-text="没有更多了"
      @load="onLoad"
    >
      <ConsultItem v-for="item in list" :key="item.id" :item="item" />
    </van-list>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import ConsultItem from './ConsultItem.vue'
import { getConsultOrderList } from '@/services/consult'
import type { ConsultType, ConsultOrderItem, ConsultOrderListParams } from '@/types/consult'

// 三个选项卡各自传自己的类型进来
const props = defineProps<{ type: ConsultType }>()

const list = ref<ConsultOrderItem[]>([])
const loading = ref(false)
const finished = ref(false)

const params = ref<ConsultOrderListParams>({
  type: props.type,
  current: 1,
  pageSize: 5
})

const onLoad = async () => {
  const res = await getConsultOrderList(params.value)

  // 累加,不能写 list.value = res.data.rows
  list.value.push(...res.data.rows)

  if (params.value.current < res.data.pageTotal) {
    params.value.current++ // 还有下一页
  } else {
    finished.value = true // 最后一页,显示“没有更多了”
  }

  loading.value = false // 请求结束必须关掉加载中
}
</script>

要点 / 易错点

  • van-list 靠 v-model:loading 触发 @load,请求结束后必须把 loading 置回 false,否则不会再触发下一页。
  • 判断是否还有数据:当前页码 < 总页数 pageTotal 才 current++,否则 finished = true。
  • 数据要用 push(...rows) 追加,直接赋值会丢掉前面已加载的数据。
  • params 里的 type 来自 props.type(枚举 ConsultType),父页面三个选项卡分别传 ConsultType.Fast / Doctor / Medication。

3. 问诊记录 Item——更多操作气泡弹窗 + 取消订单 ​

vue
<!-- src/views/user/components/ConsultItem.vue(节选) -->
<template>
  <div class="consult-item">
    <!-- 底部按钮:待支付 / 待接诊 共用取消问诊 -->
    <van-button
      v-if="item.status === OrderType.ConsultPay || item.status === OrderType.ConsultWait"
      :loading="loading"
      @click="cancelConsultOrder(item)"
    >
      取消问诊
    </van-button>

    <!-- 已完成状态下的更多操作 -->
    <div v-if="item.status === OrderType.ConsultComplete" class="more">
      <van-popover
        v-model:show="showPopover"
        :actions="actions"
        placement="top-start"
        @select="onSelect"
      >
        <template #reference>更多操作</template>
      </van-popover>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed } from 'vue'
import { showSuccessToast, showFailToast } from 'vant'
import { cancelOrder } from '@/services/consult'
import type { ConsultOrderItem } from '@/types/consult'
import { OrderType } from '@/enums'

const props = defineProps<{ item: ConsultOrderItem }>()

/* ---------- 更多操作 ---------- */
const showPopover = ref(false)

// 是否禁用依赖 item 的处方 ID,所以用 computed
const actions = computed(() => [
  { text: '查看处方', disabled: !props.item.prescriptionId },
  { text: '删除订单' }
])

const onSelect = (action: { text: string }) => {
  // 后续在这里接查看处方 / 删除订单的业务
  console.log(action)
}

/* ---------- 取消订单 ---------- */
const loading = ref(false)

const cancelConsultOrder = async (item: ConsultOrderItem) => {
  loading.value = true
  try {
    await cancelOrder(item.id)          // put 请求,只需订单 ID
    item.status = OrderType.ConsultCancel // 改状态
    item.statusValue = '已取消'           // 改状态文字
    showSuccessToast('取消成功')
  } catch (e) {
    showFailToast('取消失败')
  } finally {
    loading.value = false // 无论成功失败都要关闭 loading
  }
}
</script>

要点 / 易错点

  • actions 必须用 computed,因为“查看处方”是否禁用依赖 props.item.prescriptionId;写成普通数组就不会随数据变化。
  • 气泡弹窗要加 placement="top-start",否则默认位置偏出可视区。
  • 取消订单成功后要同时改 item.status(控制按钮切换)和 item.statusValue(控制状态文字),只改一个会出现“已取消”文字配待支付按钮。
  • loading 放在 finally 里复位,成功、失败分支都要执行到。
  • “取消问诊”按钮在待支付和待接诊两种状态下都要绑定,别忘了 :loading 和 @click 两处。

常见错误 ​

  1. 评价卡片一用就报“缺少必填属性” 现象:room-message 里写 <EvaluateCard :evaluate-doctor="item.message.evaluateDoctor" />,编辑器标红。 原因:未评价的消息本来就没有 evaluateDoctor,props 却声明成必填。 修法:改为可选 evaluateDoctor?: EvaluateDoctor,组件内用“该对象是否存在”分叉已评价或未评价。

  2. provide 传了 .value,注入后数据不更新 现象:提交评价时拿到的订单信息是旧值,或 ID 为空。 原因:传出去的是解包后的普通值,响应式链路已断。 修法:provide('consult', consult) 直接传 ref;inject 侧标注 Ref<ConsultOrderItem>,并注意 ref 是类型需用 import type。

  3. 提交按钮只加了禁用 class,仍能点 现象:一分没打也能点“提交评价”,直接发出脏请求。 原因:class="disabled" 只是视觉样式,不拦截 click。 修法:onSubmit 开头做守卫,if (!score.value) return showToast('请选择评分'),再校验 content。

  4. 评价完分数不对(评 4 分显示 3 分) 现象:状态已切成已评价,但分值固定。 原因:模板里的分数是写死的占位数字。 修法:绑到 message.evaluateDoctor.score;完成评价回写消息时把 score 一并写入。

  5. inject 的订单数据直接链式取值,TS 报错/白屏 现象:consult.value.doctorInfo.id 提示可能为 undefined。 原因:订单信息异步注入,类型上必须当可空处理。 修法:提交前加类型守卫——先判 consult.value,再判 doctorInfo,任一缺失就 toast 并 return。

  6. 加载更多用 list.value = res.data.rows 现象:往下滚一次,前面已加载的记录全没了。 原因:覆盖赋值而非追加。 修法:list.value.push(...res.data.rows);用 current < pageTotal 决定 current++ 还是 finished = true。

  7. 更多操作的 actions 用 ref 定义 现象:翻到不同订单卡片,“查看处方”的禁用状态不跟着变。 原因:disabled 依赖 props.item,静态数组不会重算。 修法:改成 computed,返回 [{ text: '查看处方', disabled: !props.item.prescriptionId }, ...]。

  8. 取消订单成功但卡片还显示待支付 现象:提示“取消成功”,状态文字和按钮没变。 原因:只更新了服务端,没同步本地 item。 修法:成功后改 item.status 和状态文字;失败走 catch 提示;loading 放 finally 收尾。

练习 ​

  1. 给评价卡片补“提交中”态:点击后按钮 loading 且不可重复提交,成功关闭 loading 并切换成已评价卡片。
  2. 把“未找到订单”这类守卫抽成组合式函数 useConsultOrder(),在评价卡片和问诊记录 item 中复用。
  3. 给问诊记录列表加下拉刷新:重置 current = 1、清空 list、finished = false 后重拉第一页,验证不会与上拉加载冲突。
  4. 补完“更多操作”里的查看处方:有 prescriptionId 时跳转处方详情,没有则保持禁用并给出提示。
  5. 把 item 底部按钮区里那几段复制的 v-if 收敛成一份“状态 → 按钮配置”的映射,替换现有分支。

课件图示 ​

课件第11页:问诊记录-渲染Item组件 展示基本信息 区别展示操作按钮 更多操作

课件第3页:评价医生-结束消息和评价消息 展示结束问诊消息 展示评价卡片组件 收集评价信息

课件第5页:评价医生-提交评价 评价医生API函数 注入问诊订单信息 提交评价 修改消息状态

课件第7页:问诊记录-搭建页面 问诊Item组件 问诊List组件 问诊记录页面 传入订单类

课件第9页:问诊记录-加载数据 参数和响应类型 定义问诊记录API 加载问诊记录效果

课件第13页:问诊记录-取消订单 取消订单API函数 取消订单逻辑 使用取消逻辑 待付款,待接

原视频对照 ​

#视频时长
1评价医生-结束消息和评价消息展示17:09
2评价医生-提交评价18:26
3问诊记录-搭建页面09:50
4问诊记录-加载数据14:55
5问诊记录-渲染Item组件-01-基本信息与操作按钮16:43
6问诊记录-渲染Item组件-02-更多操作效果08:48
7问诊记录-取消订单07:01

小结 ​

  1. 多类型消息共用同一槽位时,判据必须挂在消息数据本身(类型字段 + 可选负载),不能靠位置或顺序猜。
  2. 一个组件承载两种形态(已评价/未评价)比拆两个组件省事,代价是 props 必须按最弱形态声明,不能按理想形态声明。
  3. 跨层级传值一旦引入 provide/inject,就要同时解决两件事:传 ref 保响应式、补类型保安全,缺一件都会在后面还债。
  4. 视觉禁用不等于逻辑禁用,凡是“看起来不能点”的地方,提交前都要有兜底校验。
  5. 列表分页的难点不在请求本身,而在“追加还是覆盖”和“何时判定到底”这两个判断。
  6. 状态驱动的按钮区天然是状态机,越早配置化收敛,越不会随状态增加而失控。
  7. 数据由谁提供就由谁修改——子组件只负责发出意图,回写统一交给数据源,这条在消息状态回写上体现得最明显。