主题
评价医生与订单详情(二)
本页对应 评价医生与订单详情,共 7 段视频,约 1 小时 13 分。
学习目标
- 能够基于接口文档完成问诊订单删除 API 的定义,并在问诊记录与问诊详情中接入删除逻辑且验证删除结果。
- 能够将查看处方逻辑封装为 composable,并在问诊室、问诊记录及问诊详情中复用且验证处方预览功能。
- 能够完成问诊详情页面的路由配置、骨架屏与数据渲染,并将状态、患病时长、就诊情况等数字字段转换为正确文字展示。
- 能够封装“更多操作”组件,约定并接入 props 与自定义事件,实现查看处方禁用控制、查看处方和删除订单的父子组件通信。
- 能够根据订单状态渲染倒计时和对应操作按钮,并验证待支付、待接诊、咨询中、已完成、已取消等状态的展示差异。
- 能够将取消订单和删除订单逻辑分别封装为 composable,在问诊记录与问诊详情中复用,并实现删除成功后通知父组件更新列表或跳转问诊记录页。
知识点
删除订单要解决什么问题
问诊记录列表里有两个删除入口:普通订单卡片上的“删除订单”按钮,以及“已完成”订单里“更多操作”中的“删除”。需求本身不复杂:点击后把当前订单从列表中移除。但这里有一个关键约束:列表数据存放在父组件 consult-list 中,子组件 consult-item 只负责展示和触发操作。数据由谁提供,就由谁修改,所以子组件不能直接改列表,只能把“要删除的订单 ID”通知给父组件。
为什么需要这样拆?因为如果子组件直接改父组件数据,数据流会变乱,后续分页、空列表、重新加载都不好维护。怎么想?先准备删除 API,再在子组件里做按钮 loading、请求、提示和事件通知,最后让父组件按 ID 过滤列表。怎么做?下面按步骤展开。
准备删除订单 API
为什么需要:删除动作要落到后端,组件里不能自己“假删除”。怎么想:删除只需要知道订单 ID,请求成功后不关心响应体,只要请求成功就代表删除成功。怎么做:在 api/consult.ts 中新增 deleteOrder,请求方式为 DELETE,地址按接口文档拼接订单 ID。
ts
// 地址以接口文档为准,这里用常量表示删除地址
export const deleteOrder = (id: string) => {
return request.delete(`${DELETE_ORDER_URL}/${id}`)
}易错点:不要传整个订单对象却只使用其中的 ID;请求方式别写成 GET;地址拼接时注意斜杠和 ID 类型。
子组件实现删除逻辑
为什么需要:按钮点击后要有 loading,防止重复点击;成功失败要有提示;删除成功后要通知父组件。怎么想:删除逻辑放在 consult-item 中,维护 deleteLoading,处理函数接收 item,请求成功后 emit('on-delete', item.id)。怎么做:
ts
const deleteLoading = ref(false)
const emit = defineEmits<{
(e: 'on-delete', id: string): void
}>()
const deleteConsultOrder = async (item: ConsultOrderItem) => {
deleteLoading.value = true
try {
await deleteOrder(item.id)
showSuccessToast('删除成功')
emit('on-delete', item.id)
} catch (e) {
showFailToast('删除失败')
} finally {
deleteLoading.value = false
}
}按钮绑定:
html
<button
:loading="deleteLoading"
@click="deleteConsultOrder(item)"
>
删除订单
</button>易错点:loading 必须放在 finally 里复位,否则失败后按钮一直转;emit 要放在删除成功之后;事件名要和父组件监听名一致;类型别写成 number,订单 ID 按项目约定通常是 string。
已完成订单的“更多操作”也要删除
为什么需要:已完成订单的删除入口藏在“更多操作”气泡里,如果只处理普通按钮,会漏掉这个入口。怎么想:onSelect 会收到两个参数,action 是当前选项对象,index 是选项索引。查看处方索引为 0,删除订单索引为 1,因此用索引判断更直接。怎么做:
ts
const onSelect = (action: ConsultOrderItem, index: number) => {
if (index === 1) {
deleteConsultOrder(props.item)
}
}易错点:不要用 action.text 做脆弱判断;索引 0 和 1 要分清;删除逻辑复用同一个 deleteConsultOrder,不要另写一套。
父组件真正删除数据并处理空列表
为什么需要:子组件只通知,不持有列表;父组件拿到 ID 后要从 list 中移除。怎么想:用 filter 保留 id !== 被删除 ID 的项。删除后如果列表为空,需要重新调用 onLoad 拉一次数据,避免页面停留在空列表或分页状态异常。怎么做:
ts
const onDelete = (id: string) => {
list.value = list.value.filter(item => item.id !== id)
if (list.value.length === 0) {
onLoad()
}
}模板监听:
html
<ConsultItem
v-for="item in list"
:key="item.id"
:item="item"
@on-delete="onDelete"
/>易错点:filter 条件写成 !==,不要写成 ===;删除后空列表判断用 length === 0;重新加载函数名按项目实际写法调用;不要把父组件的 list 直接传给子组件修改。
调试与验证
先在浏览器把网络调成慢速 3G,点击“删除订单”,观察按钮是否出现 loading、成功后订单是否从列表消失。再进入“已完成”订单,点击“更多操作”里的删除,确认同样生效。转写稿中的验证过程是:普通删除后订单消失;已完成订单点“更多”删除后也消失。若失败,优先检查 emit 事件名、父组件监听名、deleteOrder 参数和 finally 复位。这样,问诊记录删除订单的主流程和“更多操作”分支就都闭环了。
本节目标与背景
本节对应“问诊记录-查看处方 composable”。在问诊室中已经实现过查看处方,现在问诊记录列表的“更多操作”里也要能查看处方。转写稿明确说:可以把查看处方逻辑直接导出成函数使用,但这里采用 composable 方式复用,是为了后期扩展,例如将来逻辑依赖响应式数据时更好维护。因此本节核心是:提取查看处方逻辑 → 封装 composable → 在问诊室复用 → 在问诊记录 consult-item 中复用,并处理“更多操作”的索引判断。
为什么需要封装成 composable
查看处方不是只在一个地方出现:问诊室 room-message 需要它,问诊记录 consult-item 也需要它。如果两处各写一遍,后续改接口、改提示、改展示逻辑就要改两次,容易漏改。转写稿里也解释了“直接导出函数也可以”,但封装成 composable 更利于后续扩展:现在是一个函数,将来可能需要 ref、loading、依赖数据,都可以收进 composable 内部。调用方只关心“我得到一个查看处方函数”,不关心内部怎么请求、怎么展示。
怎么做:提取查看处方逻辑
先到 room-message 中找到原来的查看处方逻辑,把它剪切到 composables/index.ts,定义一个以 use 开头的 composable。转写稿中函数名对应“使用查看处方逻辑”,返回真正用于显示处方的函数。结构可以写成:
ts
// composables/index.ts
export const useShowPrescription = () => {
const onShowPrescription = async (prescriptionId: string) => {
// 原问诊室中查看处方的逻辑:请求处方详情并展示
}
return onShowPrescription
}注意:原逻辑里依赖的 API 函数要一并导入,不要漏;最后 return 的是真正给模板调用的函数。将来如果要加响应式数据,也放在这个 composable 内部定义并返回。
在问诊室复用
回到 room-message,把原来直接写的查看处方逻辑替换为:
ts
const onShowPrescription = useShowPrescription()然后模板里原来调用查看处方的地方继续调用 onShowPrescription 即可。转写稿特别提醒:替换后要把不再使用的旧导入删掉,避免无用导入和命名冲突。这样问诊室的查看处方能力没有变化,只是逻辑来源变成了 composable。
在 consult-item 中复用
到 consult-item 顶部引入并调用:
ts
const onShowPrescription = useShowPrescription()普通“查看处方”按钮绑定点击事件,传入处方 ID:
vue
<button @click="onShowPrescription(props.item.prescriptionId)">
查看处方
</button>更关键的是“更多操作”。转写稿中 onSelect 会收到两个参数:第一个是当前点击的 action 对象,第二个 i 是选项索引。查看处方索引为 0,删除订单索引为 1。所以要在 onSelect 里补判断:
ts
const onSelect = (action: { text: string }, i: number) => {
if (i === 0) {
onShowPrescription(props.item.prescriptionId)
}
if (i === 1) {
// 删除订单逻辑
}
}这样点击“更多 → 查看处方”时,才会真正调用查看处方函数。注意传的是 prescriptionId,不是订单 id。
调试过程
转写稿中的验证方式是:刷新浏览器,进入问诊记录列表;先找“已完成”订单,点击“更多”,再点“查看处方”,确认处方能展示;再找“咨询中”的订单,点击普通“查看处方”按钮,确认同样生效。这个调试过程说明两处入口都要测:普通按钮和更多菜单。若只测按钮,容易漏掉 onSelect 的索引判断;若只测更多菜单,容易漏掉按钮绑定。
易错点
第一,onSelect 的索引别写反:0 是查看处方,1 是删除订单。第二,传给查看处方函数的是处方 ID,不是订单 ID。第三,composable 返回值要接住,命名不要和 API 函数混淆。第四,替换问诊室逻辑后要清理旧导入。第五,consult-item 中要用 props.item 取数据,别直接写 item。第六,更多操作里只处理删除订单不够,必须补上 i === 0 的查看处方分支。按转写稿完成这些后,问诊室和问诊记录两处查看处方就通过 composable 复用了。
本节目标与实现顺序
这一节要完成“问诊详情”页面的首次渲染。转写稿把任务拆得很清楚:先准备基础组件和路由规则;再准备骨架屏结构,用于数据未加载时占位;然后获取问诊详情数据并渲染页面;渲染时发现患病时长、是否就诊过等字段是数字,需要转成文字,于是把问诊室中类似“过滤器”的函数提取到 filter.ts 中复用。为什么需要按这个顺序?因为页面没有路由就进不来,没有骨架屏加载态会闪空,没有数据就无法渲染,数字不转文字用户就看不懂。怎么想?先把“能进入页面”跑通,再处理“加载中”,最后处理“有数据后怎么显示”。怎么做?按转写稿逐步落地,并在浏览器中刷新、看网络请求、切换不同订单状态验证。
先准备组件与路由
为什么先做组件和路由?因为问诊详情是一个独立页面,必须能从问诊记录列表点击进入。怎么想?组件负责页面结构与逻辑,路由负责地址与参数。转写稿中新建 consult-detail 组件,路由路径使用 user/consult/:id,通过地址栏参数知道当前查看哪条问诊,组件指向 consult-detail,标题为“问诊详情”。怎么做?在路由文件中复制一条规则,补上 :id 参数和对应组件;在浏览器刷新后点击任意记录进入详情,能看到 consult-detail 的内容,就说明路由配置成功。易错点:路径参数名要和后续 route.params.id 对上;组件名、路由 component 不要写错;标题别漏配。
用骨架屏处理加载态
为什么需要骨架屏?因为详情数据是异步获取的,未拿到数据时如果直接渲染内容区,会出现空白或字段报错。怎么想?用 item 是否有值来区分两种状态:有数据展示详情结构,没有数据展示骨架屏。怎么做?转写稿中先准备 item = ref<ConsultOrderItem>(),模板上方放详情结构,下方放骨架结构;骨架结构外层保持同样的 consult-detail-page,内部放导航标题“问诊详情”,再用 van-skeleton 组件,设置 title 和 :row="4",两个骨架块之间加 margin-top: 30px。条件上用 v-if="item" 展示详情,用 v-else 展示骨架。易错点:条件别写反;骨架屏也要有导航占位,避免加载时页面跳动;item 初始值可能为空,模板中访问深层字段前要先保证条件成立。
获取详情数据并渲染
为什么要在 onMounted 中取数据?因为页面挂载后才知道路由参数,才能请求对应订单详情。怎么想?从地址栏拿 id,调用问诊室已用过的“获取订单详情”接口,把结果赋给 item。怎么做?转写稿中使用 useRoute() 拿到路由,onMounted 中调用 getConsultOrderDetail(route.params.id as string),成功后 item.value = res.data。浏览器刷新后,骨架屏消失、详情出现,网络面板中该请求的响应有数据,说明获取成功。渲染时按转写稿绑定:应付款金额、状态文字、医生名称(未接诊时显示“待接诊”占位)、患者姓名/性别/年龄、患病时长、是否就诊过、病情描述、订单编号、创建时间、优惠券抵扣、积分抵扣、实付款等。状态文字要显示 statusValue,颜色类根据状态加 orange 或 green。易错点:route.params.id 是字符串,接口需要时做类型断言;doctorInfo 可能不存在,要占位;状态不要直接显示数字,要显示 statusValue。
数字转文字与 filter.ts 复用
为什么需要转文字?因为患病时长、是否就诊过在数据里是数字,直接渲染用户看不懂。怎么想?问诊室中已经有“根据数字取文字”的函数,详情页也要用,所以不能复制两份,应提取到公共文件。怎么做?转写稿中从 room-message 剪切两个函数,新建 src/utils/filter.ts,用 export 导出;在 room-message 和 consult-detail 中分别导入。详情模板中患病时长调用时间文字函数并传入 item.illnessTime,是否就诊过调用就诊标记文字函数并传入 item.consultFlag。调试时刷新页面,看到“一月内”“就诊过”,并切换咨询中、已完成等状态确认状态文字正常。易错点:Vue3 没有过滤器,要当普通函数调用;提取后记得清理原组件无用导入;函数名和参数别传错。
调试与易错点小结
转写稿的调试过程是:先刷新看到骨架屏,再看到详情结构;通过网络请求确认数据;发现状态、患病时长、就诊情况显示不对,再补 statusValue 和文字转换函数;最后切换不同状态订单验证。常见坑包括:路由参数未接、骨架屏条件写反、item 为空时访问深层字段、数字字段忘记转文字、函数提取后导入遗漏。按“组件路由→骨架屏→取数→渲染→抽函数复用”的顺序做,页面渲染就能稳定完成。
代码示例
示例 1:问诊记录 —— 删除订单(API + composable + 父子通信)
ts
// src/api/consult.ts
import request from '@/utils/request'
// 删除订单:不关心响应结果,请求成功即代表删除成功
export const deleteOrder = (id: string) =>
request({
url: `/consult/order/${id}`,
method: 'delete'
})ts
// src/composables/index.ts
import { ref } from 'vue'
import { deleteOrder } from '@/api/consult'
import { showSuccessToast, showFailToast } from 'vant'
// 删除订单逻辑(问诊记录、问诊详情都要用,所以抽成 composable)
export const useDeleteOrder = (cb?: () => void) => {
const loading = ref(false)
const delConsultOrder = async (item: { id: string }) => {
loading.value = true
try {
await deleteOrder(item.id)
showSuccessToast('删除成功')
// 删除成功后要做什么,由调用方决定(列表页通知父组件,详情页跳转)
cb?.()
} catch {
showFailToast('删除失败')
} finally {
// 成功、失败都要把 loading 关掉
loading.value = false
}
}
return { loading, delConsultOrder }
}vue
<!-- src/views/consult/consult-item.vue -->
<script setup lang="ts">
import { ref } from 'vue'
import { useDeleteOrder } from '@/composables'
const props = defineProps<{ item: ConsultOrderItem }>()
// 约定事件:删除成功通知父组件(列表数据在父组件里)
const emit = defineEmits<{
(e: 'onDelete', id: string): void
}>()
const showPopover = ref(false)
const { loading: deleteLoading, delConsultOrder } = useDeleteOrder(() => {
emit('onDelete', props.item.id)
})
// popover 的 select:i=0 查看处方,i=1 删除订单
const onSelect = (action: { text: string }, i: number) => {
if (i === 1) delConsultOrder(props.item)
}
</script>
<template>
<div class="consult-item">
<!-- 取消订单旁边的删除按钮 -->
<van-button :loading="deleteLoading" @click="delConsultOrder(item)">
删除订单
</van-button>
<!-- 已完成订单里的“更多操作”,里面同样有删除 -->
<van-popover
v-model:show="showPopover"
:actions="[{ text: '查看处方' }, { text: '删除订单' }]"
@select="onSelect"
>
<template #reference>
<van-button>更多</van-button>
</template>
</van-popover>
</div>
</template>vue
<!-- src/views/consult/consult-list.vue -->
<script setup lang="ts">
import { ref } from 'vue'
import ConsultItem from './consult-item.vue'
import { getConsultOrderList } from '@/api/consult'
const list = ref<ConsultOrderItem[]>([])
// 列表加载(分页),删空之后要靠它重新拉数据
const onLoad = async () => {
const res = await getConsultOrderList({ current: 1, pageSize: 5 })
list.value = res.rows
}
// 子组件删除成功后:从列表中移除该条订单
const onDelete = (id: string) => {
list.value = list.value.filter((item) => item.id !== id)
// 极端情况:一页 5 条全删完,列表空了要重新加载
if (list.value.length === 0) onLoad()
}
</script>
<template>
<consult-item
v-for="item in list"
:key="item.id"
:item="item"
@on-delete="onDelete"
/>
</template>关键行 / 易错点
deleteOrder只拼接 ID +method: 'delete',响应结果不需要处理,请求成功即删除成功。- 删除只是“发请求”,真正移除数据必须交给父组件——列表数据由父组件提供,就由父组件修改。子组件通过
emit('onDelete', id)只把 ID 抛上去。 - 事件名写成
onDelete,模板里监听写@on-delete是能对上的(Vue 内部会加on前缀并转驼峰,即onOnDelete),但别混用@onDelete这种写法。 loading一定要放在finally里关,否则请求报错后按钮会一直转圈。- 两个入口都要接删除逻辑:普通按钮的
@click,以及已完成订单里“更多操作”的@select(i === 1)。漏掉 popover 里的删除是常见疏漏。 - 父组件里
list.value.length === 0时重新onLoad(),避免删除后页面一片空白。
示例 2:问诊记录 —— 查看处方 composable(逻辑复用)
ts
// src/composables/index.ts
import { getPrescriptionPic } from '@/api/consult'
import { showImagePreview } from 'vant'
// 把问诊室里“查看处方”的逻辑抽成 composable,问诊室 / 问诊记录 / 问诊详情都能复用
export const useShowPrescription = () => {
const onShowPrescription = async (id?: string) => {
// 没有处方 ID 直接返回,避免拿空 ID 去请求
if (!id) return
const res = await getPrescriptionPic(id)
showImagePreview({ images: [res.url] })
}
return { onShowPrescription }
}vue
<!-- src/views/consult/consult-item.vue -->
<script setup lang="ts">
import { ref } from 'vue'
import { useShowPrescription, useDeleteOrder } from '@/composables'
const props = defineProps<{ item: ConsultOrderItem }>()
// 使用 composable 拿到查看处方函数
const { onShowPrescription } = useShowPrescription()
const { loading: deleteLoading, delConsultOrder } = useDeleteOrder(() => {
emit('onDelete', props.item.id)
})
const showPopover = ref(false)
const onSelect = (action: { text: string }, i: number) => {
if (i === 0) onShowPrescription(props.item.prescriptionId) // 查看处方
if (i === 1) delConsultOrder(props.item) // 删除订单
}
</script>
<template>
<div class="consult-item">
<!-- 咨询中:直接暴露的查看处方按钮 -->
<van-button @click="onShowPrescription(item.prescriptionId)">
查看处方
</van-button>
<!-- 已完成:更多操作里的查看处方 -->
<van-popover
v-model:show="showPopover"
:actions="[{ text: '查看处方' }, { text: '删除订单' }]"
@select="onSelect"
>
<template #reference>
<van-button>更多</van-button>
</template>
</van-popover>
</div>
</template>关键行 / 易错点
- 为什么不直接把函数
export出去?抽成useXxx的写法是为了后续能在里面加响应式依赖,可维护性更好——这也是转写稿里强调的点。 - 两个调用点都要接:独立的“查看处方”按钮传
item.prescriptionId,更多操作里是i === 0时调用。 if (!id) return是必要的防御,没有处方 ID 时不要发请求。- 复用方式:问诊室
room-message.vue里原来那段查看处方逻辑删掉,改成const { onShowPrescription } = useShowPrescription();改完记得清掉无用的 import(Ctrl/Command + Shift + O)。 - 原组件里剪走的 API 导入不要留在原地,否则会有无用导入告警。
示例 3:问诊详情 —— 路由 + 骨架屏 + 数据渲染 + filter 复用
ts
// src/utils/filter.ts
// 从问诊室抽出来的“数字转文字”函数,两处组件复用(Vue3 没有过滤器,当普通函数用)
export const getIllnessTimeText = (time: number) => {
const map: Record<number, string> = {
1: '一周内',
2: '一个月内',
3: '半年内',
4: '半年以上'
}
return map[time]
}
export const getConsultFlagText = (flag: number) =>
flag === 0 ? '没就诊过' : '就诊过'ts
// src/router/index.ts
{
path: '/consult/:id', // 通过地址栏参数知道看的是哪个问诊
component: () => import('@/views/consult/consult-detail.vue'),
meta: { title: '问诊详情' }
}vue
<!-- src/views/consult/consult-detail.vue -->
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { getConsultOrderDetail } from '@/api/consult'
import { getIllnessTimeText, getConsultFlagText } from '@/utils/filter'
const item = ref<ConsultOrderItem>()
const route = useRoute()
onMounted(async () => {
// 从地址栏拿 ID,注意断言成 string
const res = await getConsultOrderDetail(route.params.id as string)
item.value = res
})
</script>
<template>
<!-- 有数据才渲染详情结构 -->
<div class="consult-detail-page" v-if="item">
<cp-nav-bar title="问诊详情" />
<div class="detail-head">
<p>应付金额:{{ item.payment }}</p>
<p
:class="{
orange: item.status === OrderType.ConsultPay,
green: item.status === OrderType.ConsultChat
}"
>
{{ item.statusValue }}
</p>
</div>
<p>医生:{{ item.docInfo?.name || '待接诊' }}</p>
<p>
患者:{{ item.patientInfo.name }} {{ item.patientInfo.genderValue }}
{{ item.patientInfo.age }}岁
</p>
<!-- 数字要转成文字 -->
<p>患病时长:{{ getIllnessTimeText(item.illnessTime) }}</p>
<p>是否就诊过:{{ getConsultFlagText(item.consultFlag) }}</p>
<p>病情描述:{{ item.illnessDesc }}</p>
<p>订单编号:{{ item.orderNo }}</p>
<p>创建时间:{{ item.createTime }}</p>
<p>优惠券:{{ item.coupon }}</p>
<p>积分抵扣:{{ item.point }}</p>
<p>实付款:{{ item.actualPayment }}</p>
</div>
<!-- 没数据时展示骨架屏 -->
<div class="consult-detail-page" v-else>
<cp-nav-bar title="问诊详情" />
<van-skeleton title :row="4" />
<van-skeleton title :row="4" style="margin-top: 30px" />
</div>
</template>关键行 / 易错点
- 骨架屏和详情结构用同一个
v-if="item"/v-else切换:item有值前显示骨架屏,拿到数据后显示内容。 route.params.id的类型是string | string[],取出来要as string再传给接口。- 状态文字用
item.statusValue,颜色类名和问诊记录页保持一致:待付款加orange、咨询中加green。 - 医生信息在未接诊时是空的,用
item.docInfo?.name || '待接诊'兜底。 illnessTime、consultFlag这类字段是数字,直接渲染会显示成裸数字,必须用getIllnessTimeText/getConsultFlagText转成文字。- 两个 filter 函数从问诊室剪到
utils/filter.ts后要export,问诊室那边改成import引入,别留下两份重复定义。
常见错误
删完订单列表还在 现象:点击删除提示"删除成功",但列表条目原封不动,刷新后才消失。 原因:列表数据
list定义在父组件consult-list,子组件consult-item直接改了本地状态或只发了请求,没有回传删除意图。 修法:子组件defineEmits声明on-delete(id: string),删除成功后emit('on-delete', item.id);父组件接收后用list.value = list.value.filter(i => i.id !== id)更新。删到最后一条时页面变空白 / 数据错乱 现象:列表只剩 1 条时删除,页面空白且无任何提示,或分页信息与数据对不上。 原因:只做了本地 filter,没有考虑"当前页数据被删空"这一边界。 修法:filter 后判断
list.value.length === 0,成立则重新调用onLoad()拉取数据。更多操作点击"查看处方"却删了订单 现象:Popup 里点第一个选项执行了删除,点第二个反而去预览处方。 原因:把
select事件的第一个参数action(对象)当成索引使用,或索引与选项顺序没对齐。 修法:用第二个参数index: number判断,0→ 查看处方,1→ 删除订单;action只用于取text等展示信息。抽
consult-more组件后功能全失效 现象:组件渲染出来了,但"查看处方"永远禁用,点删除毫无反应。 原因:把原来依赖props.item.prescriptionId、emit、deleteConsultOrder的代码整段搬过去,父组件的业务被切断,只能先写死true和空函数。 修法:对外只保留props.disabled(父组件传!item.prescriptionId)与两个事件on-preview/on-delete,组件内只负责触发,不负责业务。复用 composable 后变量重名,模板绑错 loading 现象:删除按钮转圈时取消按钮也在转,或点击无反应。 原因:同一组件里多个 composable 都返回
loading,解构时未改名,后者覆盖前者。 修法:解构时重命名,如const { loading: deleteLoading, deleteConsultOrder } = useDeleteOrder(...)。详情页删除成功后没有跳回列表 现象:删除成功,页面停在原详情页,内容变成空白。 原因:
useDeleteOrder里写死了emit('on-delete', id),该逻辑只适用于列表页。 修法:把"成功后做什么"作为参数传入(callback?: () => void),列表页传 emit 逻辑,详情页传router.push('/user/consult')。详情页模板里
item.xxx报类型错误 现象:TS 提示ConsultOrderItem | null不能赋值/访问属性。 原因:ref初始值为null,虽然模板外层有v-if="item",但类型收窄不会跨到事件回调与子组件 props。 修法:在使用处加非空断言item!.id,或把v-if的判断值单独抽成computed后配合断言使用。数字状态直接渲染成 1 / 0 现象:患病时长显示
1,就诊情况显示0。 原因:接口返回的是枚举数字,未做文案映射。 修法:把getIllnessTimeText/getConsultFlagText提取到utils/filter.ts并export,两个组件分别 import 使用;提取后记得清理旧组件中失效的 import。
练习
给
useDeleteOrder增加一个可选的成功回调参数,写出它的完整签名与内部调用位置,并说明列表页、详情页分别传入什么。已完成订单的"写评价 / 查看评价"两个按钮共用一套样式,请写出根据
item.evaluationId切换按钮文案与tag类型的模板片段(含默认按钮与tag="default"的灰按钮)。为
consult-more组件写出完整的defineProps与defineEmits声明,并说明父组件在使用时如何传入"有无处方"的禁用状态。待支付倒计时区块需要满足两个条件才显示:状态为待付款、且
countDown有值。请写出该v-if表达式,并说明为什么不能只判断状态。详情页骨架屏与内容区的切换条件是什么?如果接口返回失败,当前实现会出现什么现象,你会如何补一行兜底处理?
课件图示






原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 问诊记录-删除订单 | 11:08 |
| 2 | 问诊记录-查看处方composable | 06:15 |
| 3 | 问诊详情-页面渲染 | 17:24 |
| 4 | 问诊详情-更多组件封装 | 11:32 |
| 5 | 问诊详情-操作按钮展示 | 09:28 |
| 6 | 问诊详情-取消订单composable | 06:37 |
| 7 | 问诊详情-删除订单composable | 11:21 |
小结
- 数据在哪,修改权就在哪:列表数据归父组件,子组件只能通过事件表达"我要删这条",绝不在子组件内改父级数据。
- 删除是"请求 + 本地更新 + 边界兜底"三件事,缺任何一件都会在极端场景(最后一条、最后一页)暴露问题。
- 同一个交互出现在两处,就该抽 composable;抽出的函数只保留通用流程,页面差异(emit / 跳转)一律以回调形式外置。
- 组件的边界由 props 和 emits 定义,抽组件时先想清楚"外部要告诉我什么、我要通知外部什么",再动代码,可以避免先写死再返工。
- UI 组件的禁用态应由外部数据驱动(有无处方 ID),而不是组件内部猜;组件只做展示与触发。
- 枚举数字转文案的函数放
utils,不放组件内,两处以上复用即视为公共资产。 - 详情页与列表页的删除语义不同:列表页是"移除一行",详情页是"移除后离开当前页",复用逻辑时必须显式区分。
- TS 的
null初始值会在模板、事件、子组件 props 三处反复出现,统一用非空断言或可选 props 处理,比到处加if更可控。