主题
评价医生与订单详情(一)
本页对应 评价医生与订单详情,共 7 段视频,约 1 小时 32 分。
学习目标
- 能够根据消息类型条件渲染结束问诊消息与评价卡片组件,并利用评价数据的有无切换“已评价/未评价”两种展示状态,完成评分、评价内容、匿名标记的表单数据收集及提交按钮禁用状态控制,且在提交前对评分和内容做非空校验。
- 能够通过 provide/inject 将问诊订单信息跨级注入评价卡片组件,调用评价 API 提交医生 ID、订单 ID、评分、内容及匿名标记,并在提交成功后更新当前消息为已评价状态、正确回显评分。
- 能够搭建问诊记录页面(Item、List、页面组件三级结构),配置路由并在三个选项卡中通过 props 向对应列表组件传递问诊类型。
- 能够定义问诊订单列表的请求参数与响应数据类型,封装列表 API,并基于 van-list 实现滚动加载更多,根据当前页码与总页数判断是否加载完成。
- 能够根据问诊订单状态渲染对应的操作按钮(待支付、待接诊、咨询中、已完成、已取消),为状态文字添加对应颜色类名,并实现基本信息展示与详情页跳转。
- 能够使用 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两处。
常见错误
评价卡片一用就报“缺少必填属性” 现象:
room-message里写<EvaluateCard :evaluate-doctor="item.message.evaluateDoctor" />,编辑器标红。 原因:未评价的消息本来就没有evaluateDoctor,props 却声明成必填。 修法:改为可选evaluateDoctor?: EvaluateDoctor,组件内用“该对象是否存在”分叉已评价或未评价。provide 传了
.value,注入后数据不更新 现象:提交评价时拿到的订单信息是旧值,或 ID 为空。 原因:传出去的是解包后的普通值,响应式链路已断。 修法:provide('consult', consult)直接传 ref;inject 侧标注Ref<ConsultOrderItem>,并注意ref是类型需用import type。提交按钮只加了禁用 class,仍能点 现象:一分没打也能点“提交评价”,直接发出脏请求。 原因:
class="disabled"只是视觉样式,不拦截 click。 修法:onSubmit开头做守卫,if (!score.value) return showToast('请选择评分'),再校验 content。评价完分数不对(评 4 分显示 3 分) 现象:状态已切成已评价,但分值固定。 原因:模板里的分数是写死的占位数字。 修法:绑到
message.evaluateDoctor.score;完成评价回写消息时把 score 一并写入。inject 的订单数据直接链式取值,TS 报错/白屏 现象:
consult.value.doctorInfo.id提示可能为 undefined。 原因:订单信息异步注入,类型上必须当可空处理。 修法:提交前加类型守卫——先判consult.value,再判doctorInfo,任一缺失就 toast 并 return。加载更多用
list.value = res.data.rows现象:往下滚一次,前面已加载的记录全没了。 原因:覆盖赋值而非追加。 修法:list.value.push(...res.data.rows);用current < pageTotal决定current++还是finished = true。更多操作的 actions 用 ref 定义 现象:翻到不同订单卡片,“查看处方”的禁用状态不跟着变。 原因:disabled 依赖
props.item,静态数组不会重算。 修法:改成 computed,返回[{ text: '查看处方', disabled: !props.item.prescriptionId }, ...]。取消订单成功但卡片还显示待支付 现象:提示“取消成功”,状态文字和按钮没变。 原因:只更新了服务端,没同步本地 item。 修法:成功后改
item.status和状态文字;失败走 catch 提示;loading 放 finally 收尾。
练习
- 给评价卡片补“提交中”态:点击后按钮 loading 且不可重复提交,成功关闭 loading 并切换成已评价卡片。
- 把“未找到订单”这类守卫抽成组合式函数
useConsultOrder(),在评价卡片和问诊记录 item 中复用。 - 给问诊记录列表加下拉刷新:重置
current = 1、清空 list、finished = false后重拉第一页,验证不会与上拉加载冲突。 - 补完“更多操作”里的查看处方:有
prescriptionId时跳转处方详情,没有则保持禁用并给出提示。 - 把 item 底部按钮区里那几段复制的
v-if收敛成一份“状态 → 按钮配置”的映射,替换现有分支。
课件图示






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