Skip to content

首页模块与极速问诊(二) ​

本页对应 首页模块与极速问诊,共 7 段视频,约 1 小时 18 分。

学习目标 ​

  • 能够将关注医生/文章逻辑抽取为 useFollow 组合式函数,支持 type 参数与默认值,并在医生卡片、文章卡片中复用,验证关注与取消关注请求参数正确。
  • 能够说明极速问诊完整流程,识别选择问诊类型、级别、科室、病情描述、就诊患者、支付等环节分别需要收集的数据。
  • 能够使用 enum 定义问诊类型、疾病时间等枚举,正确使用默认自增、指定初始值和字符串枚举,并区分枚举与字面量联合类型的适用场景。
  • 能够根据接口文档定义问诊信息对象及科室数据类型,使用 Partial、Required 转换可选/必选属性,并正确定义图片数组等嵌套字段。
  • 能够创建持久化问诊仓库,提供并调用记录问诊类型、问诊级别、科室 ID 的函数,通过本地存储验证数据记录结果。
  • 能够完成选择问诊级别与科室页面:配置路由、渲染一级科室、通过计算属性切换二级科室,并在点击二级科室时记录对应科室 ID。

知识点 ​

本节目标:把“关注医生”抽成可复用的关注逻辑 ​

本节要完成的是首页模块里的“关注文章”。上一节已经实现了“关注医生”,而关注医生和关注文章的核心逻辑一致:点击按钮后进入加载状态,调用关注/取消关注接口,再根据当前 likeFlag 切换状态。
为什么需要复用? 如果医生卡片和文章卡片各写一遍关注逻辑,后续改接口、改 loading、改取消关注都会重复修改。
怎么想? 先把组件里的关注逻辑抽成一个函数,再把它放到公共模块,最后通过参数区分“医生”和“文章”。
易错点: 不要一上来就复制两份逻辑;先抽函数并验证,再参数化。

先抽函数:从 doctor-card 里提取 loading 和 follow ​

在 doctor-card 组件中,关注需要两样东西:loading 用于按钮加载状态,follow 用于触发关注。
怎么做? 选中这两段逻辑,用编辑器提示“提取到函数”,先得到一个普通函数,返回 loading 和 follow。转写稿里说“楼顶是支持加载中状态的,弗洛是支持关注的”,这里对应就是 loading 和 follow。
为什么先抽函数? 先保证原有关注医生功能不变,验证通过后再移动位置,降低出错范围。
易错点: 抽完后要在浏览器刷新页面,点击“关注/取消关注”验证,不要直接大改。

ts
// 先形成函数雏形
const useFollow = () => {
  const loading = ref(false)
  const follow = async (item: { id: string; likeFlag: 0 | 1 }) => {
    // 调用关注接口,处理关注/取消关注
  }
  return { loading, follow }
}

函数名建议改成 useFollow。为什么? Vue3 里把“响应式数据 + 逻辑”封装成函数时,习惯用 use 开头,表示“使用某某逻辑”。这里就是“使用关注逻辑”。

移到公共模块:composables/index.ts ​

医生卡片能用还不够,文章卡片也要用,所以要把函数放到公共模块。转写稿里提到的 comppost、compposts,实际是 composables 文件夹。
怎么想? 这类“组合式函数”统一放到 composables 下,并新建 index.ts 导出。
怎么做? 把 useFollow 剪切到 composables/index.ts,用 export const useFollow = ... 导出,并补齐 ref、类型、接口函数等导入。然后回到 doctor-card 导入 useFollow,删除无用导入。
易错点: 忘记导出/导入;移动后原组件还保留旧逻辑;无用导入未清理。

ts
// composables/index.ts
import { ref } from 'vue'

export type FollowType = 'DOC' | 'knowledge'

export const useFollow = (type: FollowType = 'DOC') => {
  const loading = ref(false)

  const follow = async (item: { id: string; likeFlag: 0 | 1 }) => {
    // 调用项目已有接口:把 item.id 和 type 传给后台
  }

  return { loading, follow }
}

参数化:用 type 区分医生和文章 ​

现在函数只支持医生,因为调用接口时类型是医生。关注文章时,后台需要知道当前关注的是文章,所以要让 useFollow 支持第二个参数 type。
怎么想? 医生卡片不传时默认 DOC;文章卡片传 knowledge。这样医生卡片不用改调用方式,文章卡片也能复用。
怎么做? 给 useFollow 增加参数 type: FollowType = 'DOC',调用接口时把 type 一起传给后台。
易错点: 文章卡片忘记传 knowledge,后台仍按医生处理;默认值不要丢,否则医生卡片要处处传参。

item 类型收窄:只要 id 和 likeFlag ​

医生对象和文章对象字段不同,但关注逻辑只用到 id 和 likeFlag。转写稿中特别强调:doctor 里有 id、likeFlag,knowledge 里也有 id、likeFlag,所以函数参数只需要约束这两个属性。
为什么? 避免把整个医生类型或文章类型写死,才能同时兼容两类对象。
怎么做? 参数写成 { id: string; likeFlag: 0 | 1 }。调用时传整个 item 也可以,因为结构兼容。
易错点: 不要把参数写成某一个具体医生类型;不要使用未用到的字段。

文章卡片复用与调试 ​

在 knowledge-card 中导入 useFollow,调用 useFollow('knowledge') 得到 loading 和 follow,按钮绑定 loading,点击时执行 follow(item)。
调试过程: 刷新页面后点击医生关注,观察请求里 type 为 DOC;再点击文章关注/取消关注,观察请求里 type 为 knowledge。转写稿中“发的呢就是 knowledge”就是验证点。
易错点: 按钮没绑定 loading;点击事件没传当前 item;文章卡片调用时漏写 knowledge;取消关注和关注共用逻辑时没根据 likeFlag 正确处理。

小结 ​

本节的核心不是写两套关注,而是:先把关注逻辑抽成 useFollow,再放到 composables/index.ts,最后用 type 参数和通用的 { id, likeFlag } 支持医生与文章。这样医生卡片和文章卡片都只负责调用 useFollow,关注逻辑只维护一份。

极速问诊功能演示:先建立流程全景 ​

为什么需要:极速问诊不是一个页面能完成的功能,而是从首页入口开始,经过问诊级别、科室、病情描述、就诊患者,最后到支付页的一条完整链路。如果不先看全景,直接写页面,很容易出现“页面做了、数据接不上”的问题。

怎么想:转写稿里把流程拆得很清楚:第一步选择问诊类型,第二步选择问诊级别,第三步选择科室,第四步填写病情描述,第五步选择就诊患者,最后进入支付页选择支付方式。每个环节只收集一部分信息,最终汇总成问诊订单。

怎么做:演示时从首页点击“极速问诊”,进入急速问诊页面;选择三甲图文问诊或普通图文问诊;再选择科室;填写患病时间、是否就诊过、上传化验单或片子等病情描述;点击下一步选择家庭档案中的患者;进入支付页,勾选协议后点击立即支付,选择支付宝完成支付。转写稿明确说支付环节暂不完整演示,走到支付即认为流程完整走完;支付完成后会跳到问诊室。

易错点:不要把“极速问诊模块”和“急速问诊这种问诊类型”混为一谈;问诊方式实际有三种:问医生、急速问诊、开药问诊,本节选的是急速问诊流程来演示。另注意“二甲主治医生或以上”“三甲医生”等文案不要写成同音错字。

选择问诊类型与问诊级别:先记录,再跳转 ​

为什么需要:用户点“急速问诊”时,系统必须知道本次问诊类型;点“三甲/普通”时,必须知道问诊级别。否则最后生成订单时信息不完整。

怎么想:转写稿的推导是准备一个问诊仓库,每个环节往仓库里记录一部分数据。第一个环节记录问诊类型,第二个环节记录问诊级别。这样后续页面不用重复传参,最后支付前统一读取。

怎么做:演示中先点击“极速问诊”,页面进入急速问诊组件;再点击“三甲图文问诊”或“普通图文问诊”,进入科室选择页。后续实现调试时可观察到:点击极速问诊后,本地存储的 consult 对象中出现 type=2,表示急速问诊;点击三甲或普通后,仓库记录对应问诊级别。

易错点:问诊类型用 1、2、3 表示时语义很差,转写稿因此引入枚举;问诊级别 0/1 相对容易判断,口述中没有强行用枚举。记录函数要返回给组件使用,否则页面点击后无法写入仓库。

选择科室:一级切换二级,点击二级记录 ID ​

为什么需要:科室数据分一级科室和二级科室。用户点“外科”时,右侧要切换出外科下的二级科室;点“消化内科”等二级科室时,要记录具体科室 ID。

怎么想:一级科室数据里包含 id、name 和 child;二级科室包含 id、name。页面默认激活第一个一级科室,再根据当前激活项计算二级科室列表。

怎么做:演示时进入科室页,默认展示第一个一级科室下的二级科室;点击“外科”可切换右侧列表;点击“消化内科”“儿科—新生儿科”等二级科室时,仓库记录对应科室 ID。转写稿调试中能看到本地存储里出现 depId 的变化。

易错点:一级科室数据还没返回时,直接用 allDept[active.value].child 可能报错,需要可选链或判空;科室 ID 要记录二级科室的 ID,不是一级科室 ID。

病情描述、患者与支付:把零散信息收拢到订单 ​

为什么需要:病情描述包含患病时间、是否就诊、图片等;患者来自家庭档案;支付页还要处理协议和支付方式。它们都属于订单信息的一部分。

怎么想:因为这些信息不是在一个组件里收集完的,所以问诊信息对象不能一开始就要求所有字段必填,否则每个页面都会类型报错。转写稿用 Partial 把对象属性变成可选,后续再逐步补充。

怎么做:演示中填写患病时间、是否就诊过,上传图片;点击下一步选择患者;进入支付页勾选协议,点击立即支付,弹出支付方式后选择支付宝,进入支付页面。转写稿说明支付环节暂不演示,流程到支付完成即结束。

易错点:不要把“问诊信息对象”定义成所有字段必选;图片字段是数组,里面包含图片 ID 和地址;支付页的协议勾选、支付方式选择不能漏。

演示验证与调试观察 ​

本节笔记未附图片与代码,因此不额外插入素材;实现代码在后续各环节展开。演示验证按转写稿走:首页点极速问诊 → 选级别 → 选科室 → 填病情 → 选患者 → 支付页。调试时重点看页面跳转是否正确、仓库是否逐步记录 type、问诊级别、科室 ID 等信息;若点击后无记录,先检查点击事件是否调用仓库函数,再检查仓库是否通过统一入口导出。

为什么这一节先学枚举 ​

在实现“极速问诊”时,第一步要选择问诊类型:问医生、极速问诊、开药问诊。后台约定分别传 1、2、3。如果直接用 1 | 2 | 3 约束类型,代码能跑,但语义很差:看到 2 不能立刻知道它代表极速问诊。枚举就是为这种“一组命名常量”准备的语法。

为什么需要:数字可选值语义弱,容易写错、读错。
怎么想:把 1、2、3 命名成有含义的常量,再用枚举约束参数。
怎么做:用 enum 定义一组常量,通过 枚举名.成员 取值。
易错点:枚举不是普通对象,成员既有名字也有值;不要把它和字面量联合类型混用。

枚举的基本定义与使用 ​

转写稿中先新建 en/index.ts,并先写 export {} 解除“必须是一个模块”的报错。然后定义方向枚举:

ts
export {}

export enum Direction {
  up = 0,
  down = 1,
  left = 2,
  right = 3
}

接着定义改变方向的函数,并用枚举约束参数:

ts
export const changeDirection = (direction: Direction) => {
  console.log(direction)
}

调用时不要写 3,而是写 Direction.right:

ts
changeDirection(Direction.right)

在 main.ts 中导入测试:

ts
import './en'

浏览器打印 3,说明 Direction.right 的值就是 3。

为什么需要:调用方只关心“右”,不关心右到底是几。
怎么想:参数类型用 Direction,可选范围就被限制在枚举成员内。
怎么做:定义枚举 → 函数参数标注枚举类型 → 调用时点出成员。
易错点:Direction.right 是值;direction: Direction 是类型位置,二者不要写反。

默认值与自增规则 ​

枚举成员可以不给初始值:

ts
export enum Direction {
  up,
  down,
  left,
  right
}

它默认从 0 开始,依次为 0、1、2、3,和上面手写值效果一致。若给 up = 5,后面的成员会继续自增:up=5、down=6、left=7、right=8。转写稿中通过浏览器验证,right 打印为 8。

为什么需要:连续数字很常见,省略初始值能减少重复。
怎么想:只给第一个值,后续让 TS 自增。
怎么做:up = 5,其余不写值。
易错点:自增只适用于数字枚举;不要以为任意位置断开后还能自动推出合理语义。

字符串枚举与定义位置 ​

枚举也可以是字符串:

ts
export enum Direction {
  up = 'up',
  down = 'down',
  left = 'left',
  right = 'right'
}

但字符串枚举不能像数字枚举那样自动递增,所以每个成员都要给初始值。转写稿中验证,Direction.right 打印为字符串 'right'。

另一个细节:枚举建议定义在 .ts 文件中,而不是 .d.ts。因为枚举包含值,使用枚举时通常会用到这些值,值要参与运行;.d.ts 只是类型声明,不参与运行。

为什么需要:有些枚举值本身就是有语义的字符串。
怎么想:字符串枚举每个成员显式赋值;需要运行的值放 .ts。
怎么做:成员全部写字符串初始值;文件用 index.ts。
易错点:字符串枚举漏写初始值会报错;把带值枚举放进 .d.ts 不符合运行需求。

字面量联合类型还是枚举 ​

转写稿最后给了判断场景:如果可选值是 'up' | 'down' | 'left' | 'right' 这种本身有语义的字符串,用字面量联合类型更直接;如果是一组数字,比如 1 代表问医生、2 代表极速问诊、3 代表开药问诊,数字本身语义差,就更适合用枚举。

为什么需要:不是所有可选值都适合枚举。
怎么想:先看值是否“自解释”。
怎么做:有语义的字符串用联合类型;数字且语义弱用枚举。
易错点:不要为了用枚举而用枚举,也不要让 1、2、3 裸奔在业务代码里。

调试与验证 ​

转写稿的调试路径很清楚:在 en/index.ts 写枚举和函数,在 main.ts 导入,浏览器看打印值;再分别验证默认自增、指定初始值、字符串枚举。这样能把“枚举成员的值是什么”验证清楚,而不是只停留在语法记忆上。

易错点:改完枚举后要刷新页面看最新打印;验证默认值时先注释掉手写值,避免两套定义混在一起。

代码示例 ​

代码一:用 useFollow 组合式函数实现“关注医生与关注文章”的逻辑复用 ​

ts
// ---------- types/consult.ts ----------
/** 关注类型:医生 / 文章(知识) */
export type FollowType = 'doc' | 'knowledge'

/** 关注逻辑只用到 id 和 likeFlag,所以只要满足这两个属性的对象都能传入 */
export type FollowItem = {
  id: string
  likeFlag: 0 | 1
}
ts
// ---------- composables/index.ts ----------
import { ref } from 'vue'
import type { FollowType, FollowItem } from '@/types/consult'
import { followDoctor } from '@/api/consult' // 关注接口

/**
 * 使用关注逻辑(组合式函数,命名规范:use 开头)
 * @param type 关注类型,默认 'doc' 关注医生,传 'knowledge' 关注文章
 */
export const useFollow = (type: FollowType = 'doc') => {
  // 加载中状态:按钮 loading 用
  const loading = ref(false)

  // 关注 / 取消关注
  const follow = async (item: FollowItem) => {
    loading.value = true
    try {
      await followDoctor(item.id, type) // 第二个参数告诉后台关注的是医生还是文章
      item.likeFlag = item.likeFlag === 1 ? 0 : 1 // 切换关注状态
    } finally {
      loading.value = false
    }
  }

  return { loading, follow }
}
vue
<!-- ---------- DoctorCard.vue:关注医生 ---------- -->
<script setup lang="ts">
import { useFollow } from '@/composables'
const { loading, follow } = useFollow() // 不传参,默认关注医生
</script>

<template>
  <button :loading="loading" @click="follow(item)">关注</button>
</template>
vue
<!-- ---------- KnowledgeCard.vue:关注文章 ---------- -->
<script setup lang="ts">
import { useFollow } from '@/composables'
const { loading, follow } = useFollow('knowledge') // 传 knowledge,关注文章
</script>

<template>
  <button :loading="loading" @click="follow(item)">关注</button>
</template>

要点解释

  • 先把 doctor-card 里“loading + 关注”两段逻辑抽到一个函数里,再提到公共模块 composables/index.ts,这就是逻辑复用。
  • 函数名用 use 开头(useFollow),因为它是组合式函数:把响应式数据和逻辑封装进函数,所以放 composables 文件夹。
  • follow 的参数用 FollowItem 而不是 Doctor:医生对象和文章对象都有 id、likeFlag,逻辑里也只用到这两个属性,所以能同时兼容两种对象。
  • type 参数必须有默认值 'doc',否则 DoctorCard 不传参时就会出问题。

易错点

  • 调用接口时一定要把 type 传进去(followDoctor(item.id, type)),否则后台不知道你关注的是医生还是文章。
  • loading 必须用 try/finally 复位,否则接口报错后按钮会一直转圈。
  • 抽完函数记得删掉组件里残留的旧导入,否则会有未使用变量的报错。

代码二:TS 枚举语法(enum)定义问诊类型与患病时间 ​

ts
// ---------- src/enum/index.ts ----------
export {} // 解除“必须是一个模块”的错误

// 一组可选常量,默认从 0 开始自增:Up=0, Down=1, Left=2, Right=3
export enum Direction {
  Up,
  Down,
  Left,
  Right
}

// 值也可以手动指定,后面依旧依次自增
export enum Direction2 {
  Up = 5, // 5
  Down, // 6
  Left, // 7
  Right // 8
}

// 字符串枚举:无法自增,每一项都必须给初始值
export enum Direction3 {
  Up = 'up',
  Down = 'down',
  Left = 'left',
  Right = 'right'
}

/** 问诊类型:1 找医生、2 极速问诊、3 开药问诊 */
export enum ConsultType {
  Doctor = 1,
  Fast = 2,
  Medication = 3
}

/** 患病时间:1 一周内、2 一个月内、3 半年内、4 半年以上 */
export enum IllnessTime {
  Week = 1,
  Month, // 2
  HalfYear, // 3
  More // 4
}

// 使用:枚举既能当值,也能当类型
export const changeDirection = (direction: Direction) => {
  console.log(direction)
}

changeDirection(Direction.Right) // 打印 3,不用关心 3 代表什么

要点解释

  • enum 用来定义“一组命名常量”,解决纯数字不够语义化的问题:看到 ConsultType.Fast 就知道是极速问诊,看到 2 什么也不知道。
  • 不写初始值时,从 0 开始依次加一;写了一个数字初始值,后面的值继续在其基础上加一(Up = 5 时 Right 就是 8)。
  • 枚举既可以当类型约束参数范围(direction: Direction),也可以当值使用(Direction.Right)。
  • 枚举包含运行时的值,所以要定义在 index.ts 里,而不是只做类型声明的 .d.ts 文件里。

易错点

  • 字符串枚举不能自增,每一项都必须写初始值。
  • 像 0/1/2/3 这种纯数字且无语义的场景用枚举;像 'doc' | 'knowledge' 这种本身就有语义的,仍用字面量联合类型更合适。

代码三:问诊仓库(Pinia)+ Partial 可选属性 + 首页选择问诊类型 ​

ts
// ---------- types/consult.ts ----------
import type { ConsultType, IllnessTime } from '@/enum'

export type Image = {
  id: string
  url: string
}

/** 问诊信息对象(生成问诊订单时提交) */
export type Consult = {
  type: ConsultType // 问诊类型
  illnessType: 0 | 1 // 问诊级别:0 普通 / 1 三甲
  depId: string // 科室 id
  illnessDesc: string // 病情描述
  illnessTime: IllnessTime // 患病时间
  consultFlag: 0 | 1 // 是否就诊过
  pictures: Image[] // 化验单/图片数组
  patientId: string // 就诊患者 id
  couponId?: string // 优惠券 id
}

/** 分步收集,所以全部转成可选属性 */
export type PartialConsult = Partial<Consult>
ts
// ---------- stores/modules/consult.ts ----------
import { ref } from 'vue'
import { defineStore } from 'pinia'
import type { PartialConsult } from '@/types/consult'
import type { ConsultType } from '@/enum'

export const useConsultStore = defineStore(
  'consult',
  () => {
    // 问诊信息仓库,默认空对象,每个环节往里塞一部分
    const consult = ref<PartialConsult>({})

    // 记录问诊类型
    const setType = (type: ConsultType) => {
      consult.value.type = type
    }
    // 记录问诊级别:0 普通 / 1 三甲
    const setIllnessType = (type: 0 | 1) => {
      consult.value.illnessType = type
    }
    // 记录问诊科室 id
    const setDep = (depId: string) => {
      consult.value.depId = depId
    }

    return { consult, setType, setIllnessType, setDep }
  },
  { persist: { key: 'consult' } } // 持久化存储
)
vue
<!-- ---------- views/Home/index.vue:选择极速问诊 ---------- -->
<script setup lang="ts">
import { useConsultStore } from '@/stores'
import { ConsultType } from '@/enum'

const store = useConsultStore()
</script>

<template>
  <!-- 点击极速问诊,把问诊类型记录到仓库 -->
  <div @click="store.setType(ConsultType.Fast)">极速问诊</div>
</template>

要点解释

  • 问诊流程分很多步(选类型 → 选级别 → 选科室 → 填病情 → 选患者 → 支付),信息是分步收集的,所以仓库里存 PartialConsult(所有属性可选),避免初始化时就要求填满所有字段。
  • Partial<T> 是 TS 内置类型,把对象所有属性一次性转成可选,比手动一个个加 ? 方便;反向的 Required<T> 可把属性全部转回必选。
  • defineStore 第三个参数 { persist: { key: 'consult' } } 让仓库持久化,刷新页面数据不丢。
  • setType 的参数类型用 ConsultType 枚举约束,调用时传 ConsultType.Fast,语义清晰。

易错点

  • 仓库里 consult 必须用 ref<PartialConsult>({}) 声明类型,否则 .type、.depId 都会报类型错误。
  • 枚举要定义在 .ts 里(参与运行),从 @/enum 导入;如果放进 .d.ts 只做类型声明,运行时取值会拿不到。
  • 每次新增仓库都要在 stores/index.ts 统一导出,页面里才能 import { useConsultStore } from '@/stores'。

常见错误 ​

1. 组合式函数建在组件目录里,别的模块引用不到

  • 现象:knowledge-card 里 import { useFollow } from './useFollow' 一路往上找,路径报红;或者复制一份逻辑到文章卡片里,改一处忘一处。
  • 原因:只在 doctor-card 内部"提取到函数",没有真正提取到公用模块。
  • 修法:统一放到 composables/index.ts 并 export,组件只负责调用,不负责实现。

2. 关注文章时请求仍发 type: 'doc'

  • 现象:点文章的关注,Network 里 like 请求的 type 还是 doc,后端把记录记到了医生身上。
  • 原因:useFollow 加了第二个参数但调用时没传,或默认值写在了错误的位置(写成了必填)。
  • 修法:type: FollowType = 'doc' 给默认值,文章卡片显式传 useFollow('knowledge')。

3. item 参数类型写死成 Doctor,文章卡片调用报类型错误

  • 现象:useFollow 里 item: Doctor,knowledge-card 传 Knowledge 时 TS 报"缺少 likeFlag/属性不匹配"。
  • 原因:按具体业务对象建模,而不是按逻辑实际用到的字段建模。
  • 修法:只声明真正用到的字段 { id: string; likeFlag: 0 | 1 },两种对象自然都满足。

4. 枚举写在 .d.ts 里,运行时取到 undefined

  • 现象:ConsultType.Fast 打印出来是 undefined,接口参数变成 type=undefined。
  • 原因:.d.ts 只是类型声明,不参与编译产出,枚举是要带值参与运行的。
  • 修法:枚举放到普通 .ts(如 enums/index.ts),.d.ts 只放纯类型。

5. 字符串枚举漏写初始值

  • 现象:enum Direction { Up = 'up', Down, Left } 编译报错。
  • 原因:字符串成员不能自增,只有数字枚举才支持省略后续值。
  • 修法:字符串枚举每个成员都写全;数字枚举才可以只写第一个。

6. 问诊信息对象保持全必选,分步收集时报类型错误

  • 现象:consult.value.type = ... 之后 TS 提示"缺少 deptId / illnessTime 等属性"。
  • 原因:一个对象被拆到多个页面收集,任何一步都不完整,用完整类型约束必然报错。
  • 修法:仓库里声明为 Partial<Consult>,只在最终提交订单前做一次完整性校验(或断言成 Consult)。

7. 二级科室计算属性在数据回来前就取索引

  • 现象:首屏刷新控制台报 Cannot read properties of undefined (reading 'child'),或页面白屏一瞬。
  • 原因:allDept.value[active.value].child 在 allDept 还是空数组时直接取 [0]。
  • 修法:allDept.value[active.value]?.child ?? [],保证计算属性永远返回数组。

8. 仓库里赋值漏 .value,页面不更新

  • 现象:点了三甲,本地存储里 illnessType 一直是旧值/空,页面数据没变。
  • 原因:consult 是 ref,写成 consult.type = type。
  • 修法:consult.value.type = type;同时确认 persist: true 已配置。

练习 ​

1. 扩展关注类型 现在 useFollow 支持 doc 和 knowledge。要求再支持"关注药品"(type: 'medicine'),且药品对象字段是 { id, likeFlag }。写出 FollowType、useFollow 签名、以及药品卡片里的调用代码。思考:这次改动动了几个文件?如果当初没抽组合式函数,要动几个文件?

2. 枚举 vs 字面量联合类型 下面三组值,分别选择"枚举"还是"字面量联合类型"并说明理由:

  • A:'pending' | 'paid' | 'cancelled' | 'refunded'
  • B:1 | 2 | 3 | 4(1=一周内,2=一个月内,3=半年内,4=半年以上)
  • C:'male' | 'female'

3. 类型体操 已知 interface Consult { type: ConsultType; deptId: string; patientId: string; pictures: Image[] }。写出两个类型别名,分别表示"全部可选"和"在全部可选基础上再把 deptId 恢复为必选"。后者只能用 Partial / Required / 交叉类型组合完成。

4. 修复下面这段科室组件

ts
const active = ref(0)
const allDept = ref<TopDept[]>([])
const subDept = computed(() => allDept.value[active.value].child)
onMounted(async () => { const res = await getAllDept(); allDept.value = res.data })

指出至少 2 处会导致运行时或首屏问题的写法并改正。

5. 补全仓库consult.ts 里现在有 setType 和 setIllnessType,请补 setDept(deptId: string),并说明:

  • 参数为什么用 string 而不是 Dept 对象;
  • 在 consult-dept.vue 里点击二级科室时如何调用;
  • 如何在浏览器里验证写入成功。

课件图示 ​

课件第28页:极速问诊-选择问诊类型 选择极速问诊 选择问诊级别 选择科室 填写病情描述 选择

课件第21页:极速问诊-流程分析 选择极速问诊 选择问诊级别 选择科室 填写病情描述 选择就诊

课件第26页:极速问诊-问诊信息类型定义 问诊类型 问诊时间类型 问诊记录类型 全部可选属性

课件第29页:极速问诊-选择问诊类型 定义问诊仓库 提供记录函数 记录问诊类型

课件第31页:极速问诊-选择问诊级别 准备路由组件 提供记录函数 记录问诊级别

课件第33页:极速问诊-选择问诊科室 准备组件 类型&科室API 渲染&切换 记录问诊科室

原视频对照 ​

#视频时长
1首页模块-逻辑复用关注文章12:59
2极速问诊-功能演示04:04
3极速问诊-枚举语法13:40
4极速问诊-问诊信息类型定义11:30
5极速问诊-选择问诊类型10:01
6极速问诊-选择问诊级别07:52
7极速问诊-选择问诊科室18:00

小结 ​

  1. 逻辑复用的分界线不是"这段代码像",而是"这段代码用到的字段和分支是否一致"——useFollow 之所以能同时服务医生和文章,是因为它只依赖 id 和 likeFlag 两个字段。
  2. 组合式函数以 use 开头、放 composables,本质是"响应式状态 + 逻辑"的打包,不是普通工具函数;判断标准是它内部有没有 ref/computed/生命周期。
  3. 类型参数化的关键动作是"把差异抽成参数":type 抽成入参并给默认值,item 从具体对象收窄成最小结构,一次改动就同时满足了两种业务。
  4. 数字型可选值用枚举,语义型可选值用字面量联合类型;判断依据是"看这个值能不能一眼读懂"。
  5. 枚举必须住在 .ts 里——它带值、参与运行;.d.ts 只描述形状。这条决定了"类型放哪"这类问题的一半答案。
  6. 分步收集的对象一律先声明成 Partial<T>,完整性校验推迟到提交那一刻;反过来,只有真正需要全部字段的地方才用 Required。
  7. 仓库(store)在问诊流程里扮演"跨页面收集器",每个环节只提供 setXxx 一个入口、只写自己那一段,页面不直接改 state——这条纪律比函数写得好不好更重要。
  8. 计算属性必须对"数据还没回来"这一状态负责,可选链加兜底数组是标配,而不是靠"用户不会那么快点击"来赌。