Skip to content

布局容器与家庭档案(二) ​

本页对应 布局容器与家庭档案,共 6 段视频,约 1 小时 10 分。

学习目标 ​

  1. 能够基于快捷工具数组数据渲染个人中心快捷工具列表,并实现点击跳转、动态图标及退出登录确认、清除用户信息并跳转登录页。
  2. 能够为家庭档案配置一级路由与患者页面组件,并正确接入静态页面结构。
  3. 能够定义患者及患者列表 TypeScript 类型,编写获取患者列表 API,并在组件挂载时请求数据完成列表渲染。
  4. 能够使用正则替换实现身份证号前 6 位和后 4 位保留、中间 8 位脱敏的展示效果。
  5. 能够说明 Vue3 中 v-model 对应的 modelValue 与 update:modelValue 机制,并封装支持动态选项、选中切换和双向绑定的单选按钮组件。
  6. 能够使用 Popup 组件实现右侧滑添加患者界面,扩展导航组件 back 属性关闭弹层,并完成添加患者表单静态结构搭建。

知识点 ​

快捷工具:用数据驱动七个入口 ​

为什么需要:个人中心里的“快捷工具”是多个功能入口的集合,例如我的问诊、家庭档案、地址管理等。如果每个入口都手写一份结构,模板会重复、维护成本高,后续增删入口也容易漏改。

怎么想:转写中先只准备一个单元格结构,再准备初始化数据,用 v-for 遍历渲染。每个快捷工具至少包含“标题”和“跳转地址”,因此数组项用 label 和 path 描述;图标按顺序命名,用索引拼接即可。

怎么做:先准备 tools 数组。已实现的路由写真实地址,未实现的路由按转写中的做法先回首页:

ts
const tools = [
  { label: '我的问诊', path: '/user/consult' },
  { label: '我的处方', path: '/' },
  { label: '家庭档案', path: '/user/patient' },
  { label: '地址管理', path: '/' },
  { label: '我的评价', path: '/' },
  { label: '官方客服', path: '/' },
  { label: '设置', path: '/' },
]

模板中遍历 tools,用 item.label 做唯一 key,标题渲染 item.label,跳转使用 item.path。转写中只明确提到图标文件按 01 到 07 排列,完整目录未展开,实际项目按已有图标目录填写前缀:

vue
<div class="tools">
  <p class="title">快捷工具</p>
  <div class="cell" v-for="(item, i) in tools" :key="item.label">
    <router-link :to="item.path">
      <!-- 图标路径按项目已有目录填写,文件名按 01、02…07 递增 -->
      <img :src="`【项目图标目录】0${i + 1}.png`" alt="快捷工具图标" />
      <span>{{ item.label }}</span>
    </router-link>
  </div>
</div>

易错点:key 要唯一,这里用 label;图标序号从 1 开始,拼接时注意 i + 1;未实现路由不要写不存在的地址,转写中统一回首页;跳转地址字段是 path,不要写成 pass。

跳转验证与图标检查 ​

为什么需要:快捷工具不是静态展示,点击后必须能进入对应页面;图标也要与入口一一对应,否则用户会误判功能。

怎么想:先验证路由跳转,再验证图标顺序。转写中的调试方式是刷新页面后逐个点击:我的问诊跳到 /user/consult,家庭档案跳到 /user/patient,其他未实现入口回首页;随后刷新观察不同单元格前的图标是否变化。

怎么做:在浏览器中打开个人中心,刷新后点击“我的问诊”和“家庭档案”,确认路由地址正确;再检查图标是否按 01 到 07 递增显示。若图标不显示,优先检查文件目录、文件名和拼接序号。

易错点:不要把 router-link 的 to 写成普通 href 后忘记处理路由;图标文件名若没有前导 0,0${i + 1} 会拼错;不要用重复标题做 key,当前数据里标题刚好不重复,但更稳妥可用唯一字段。

退出登录:确认框、删除用户、跳转登录页 ​

为什么需要:退出登录不能点击后立刻删除登录状态,否则用户误触会直接退出;需要先弹出确认框,确认后再清理用户信息并跳转登录页。

怎么想:转写中使用 Vant 的 showConfirmDialog 弹出确认框。它返回 Promise:点击确认后继续执行,点击取消会进入 reject,后面的删除和跳转不会执行。因此可以用 await 等待确认结果。

怎么做:模板中在快捷工具下方加退出入口,使用 javascript:; 防止默认跳转,并绑定点击事件:

vue
<a href="javascript:;" class="logout" @click="onLogout">退出登录</a>

脚本中导入确认框、用户仓库和路由:

ts
import { showConfirmDialog } from 'vant'
import { useUserStore } from '@/stores/user'
import { useRouter } from 'vue-router'

const userStore = useUserStore()
const router = useRouter()

const onLogout = async () => {
  await showConfirmDialog({
    title: '温馨提示',
    message: '您是否确认退出优一问诊?',
  })

  userStore.delUser()
  router.push('/login')
}

易错点:showConfirmDialog 要正确导入;点击取消时 Promise 会 reject,若不做 try/catch,控制台会出现错误提示,但后续代码不会执行;删除用户信息要在跳转前完成;跳转登录页用 router.push('/login'),不要写成 router.coch 这类同音误写。

调试与收尾 ​

转写中的验证过程是:刷新页面后点击“退出登录”,先点“取消”,确认不会删除登录信息;再点“确认”,检查本地存储中的登录信息被删除,并跳转到登录页。最后提交代码,提交信息可写“个人中心-快捷工具和退出登录”。这一节的关键不是写多少结构,而是把快捷工具做成“数据驱动渲染”,把退出登录做成“确认后再执行”的流程。

本节要完成什么 ​

这一节的目标不是把患者列表数据渲染出来,而是先为“家庭档案”功能打地基:新建患者页面组件,配置对应路由,再把笔记里准备好的静态页面结构接进去,最后在浏览器里验证“点击家庭档案能进入患者页面”。动态数据、患者类型、API、身份证脱敏等内容属于后续小节,本节先不做,避免范围铺得太大。

为什么需要先做路由和组件?因为家庭档案是一个新的页面功能。没有路由,入口点击后无法切换页面;没有组件,路由也没有可渲染的页面。怎么想?按“先通路、再页面、后数据”的顺序推进:先让页面能到达,再让静态结构显示出来,最后才做动态渲染。怎么做?在 views/user 下新建 patient-page.vue,在路由文件中补规则,再复制静态结构。易错点是把这一步和列表渲染混在一起,导致路由还没通就急着写接口。

新建患者页面组件 ​

为什么需要单独建组件?家庭档案属于用户模块下的页面,不能把结构直接塞到个人中心里。怎么想?转写稿里明确说“家庭档案其实也是用户下面的一个小模块”,所以组件应放在 views/user 目录下,命名为患者页面。怎么做?新建 patient-page.vue,先初始化页面基本结构,组件名可对应为 PatientPage。这一步只保证组件文件存在、能被路由引用。易错点是文件名和路由引用不一致,比如把 patient 拼成 pasient、paient 等;转写稿里出现的这些读音相近词,实际都应统一为 patient。

配置一级路由 ​

为什么需要判断路由层级?因为家庭档案页面是完整内容切换,底部没有 tabbar 栏,所以它不是某个 tab 下的二级页面,而是和登录等页面同级的一级路由。怎么想?先观察页面形态:如果进入后整页切换、没有底部导航,就按一级路由处理;如果有 tabbar 包裹,才考虑二级路由。怎么做?在 router/index.ts 中,把家庭档案路由写在登录路由下方,路径指向 user/patient,组件指向 views/user/patient-page.vue,并把标题改为“家庭档案”。按转写稿整理的关键配置如下:

ts
// router/index.ts:放在登录路由下方,具体写法沿用项目现有风格
{
  path: '/user/patient',
  component: () => import('@/views/user/patient-page.vue'),
  meta: { title: '家庭档案' }
}

易错点有三个:一是把家庭档案误配成二级路由;二是路径和组件路径大小写、拼写不一致;三是只改路径忘记改标题。转写稿中的“root”应理解为 router,“tableable 栏”应理解为 tabbar 栏。

接入静态结构并验证 ​

为什么先接静态结构?因为路由和组件只解决“能不能到”,静态结构解决“到了以后长什么样”。怎么想?先复制笔记里准备好的家庭档案静态结构,替换掉 patient-page.vue 里的占位内容,不急着绑数据。怎么做?把结构粘贴到组件模板中,然后在浏览器刷新项目,点击“家庭档案”,观察地址是否跳到 user/patient,页面是否显示患者页面组件。转写稿中的验证结果是:顶部导航出现“患者”,页面里有两个患者卡片、一个“添加患者”按钮,以及“最多可添加6人”的提示。易错点是复制结构后没有检查样式和层级,或者看到页面能显示就误以为列表数据已经完成;本节只验证静态结构和路由跳转。

调试顺序与易错点 ​

调试时按“路由是否跳转 → 组件是否渲染 → 静态结构是否完整”的顺序排查。为什么?因为如果路由没通,改模板没有意义;如果组件没渲染,查样式也容易跑偏。怎么做?先点击入口看地址,再看页面是否出现组件内容,最后核对顶部导航、患者卡片、添加按钮和提示文案。易错点包括:组件目录放错、路由写在错误层级、路径拼写错误、标题未改、静态结构复制不完整。完成这些后,本节任务就结束了,下一节再基于这个页面做患者类型、API 和列表渲染。

本节要解决什么问题 ​

上一节已经把路由和患者列表的静态结构搭好了,页面上能看到两个写死的患者卡片、一个“添加患者”按钮和一行“最多可添加 6 人”的提示。本节要做的,是把这份静态结构换成真实数据驱动的列表。

为什么需要:静态结构只是骨架。真实项目里患者数量、姓名、身份证号都是接口返回的,写死两个既不能增删,也无法复用到问诊选患者的场景。

怎么想:先有类型,再有接口函数,然后在组件挂载时取数,最后用 v-for 渲染。这个顺序不能乱——类型定义了,取数才有提示;接口函数定义了,组件里才有东西可调。

怎么做:四步——定义患者类型 → 定义获取患者列表的 API 函数 → 组件挂载时调用 → 渲染并做脱敏与条件渲染。

易错点:很多人一上来就写 v-for,结果 item 是 any,字段名拼错也不报错,排查半天。

第一步:定义患者类型 ​

为什么需要:接口返回的是 JSON,TypeScript 不知道里面有什么字段。不定义类型,模板里写 item.nmae 都不会提示。

怎么想:转写稿明确说,类型要参考接口文档,接口文档通常会生成一版类型,拷贝过来按项目习惯改一下字段名即可。因为患者也属于用户模块,所以放在 types/user.d.ts 里。

怎么做:字段含义按转写稿的解释——id 主键、name 患者名称、idCard 身份证号、defaultFlag 是否默认患者(0 不默认、1 默认)、gender 性别(0 和 1)、genderValue 性别对应的中文文字、age 年龄。页面展示的是一个列表,所以再补一个列表类型。

ts
// types/user.d.ts
export interface Patient {
  id: number
  name: string
  idCard: string
  defaultFlag: number // 0 不默认,1 默认
  gender: number // 0 女,1 男
  genderValue: string
  age: number
}

export type PatientList = Patient[]

易错点:gender 存的是 0/1,模板里直接渲染会显示数字,要渲染 genderValue。另外 defaultFlag 是数字不是布尔,判断时写 === 1。

第二步:定义获取患者列表的 API 函数 ​

为什么需要:组件里直接写 request.get(...) 会把接口地址散落在各处,将来地址一变就要全局搜。

怎么想:转写稿里说得很清楚,去 api/user.ts 文件最下方追加一个函数,通过 export 导出,内部用 request 发请求,再用泛型把响应类型定为 PatientList。

怎么做:翻接口文档确认——地址是 patient/myList,请求方式是 GET,没有请求参数,直接返回响应数据。GET 可以省略不写。

ts
// api/user.ts
import type { PatientList } from '@/types/user'

// 获取患者列表
export const getPatientList = () => request.get<PatientList>('/patient/myList')

易错点:泛型千万别漏。漏了的话,第三步拿到的 res.data 就是 any,前面定义类型等于白做。

第三步:组件挂载时获取数据 ​

为什么需要:数据要在页面一进来就展示,所以取数时机是组件挂载完毕。

怎么想:转写稿特意强调,取数动作要单独抽成 loadList 函数,而不是直接写在 onMounted 里。原因是将来删除、编辑成功后还要重新拉一次列表,抽出来才能复用。

怎么做:用 ref 创建 list 并指定 PatientList 类型,初始为空数组;loadList 里 await 调用接口,把 res.data 赋给 list.value;onMounted 里调用 loadList。

ts
import { ref, onMounted } from 'vue'
import { getPatientList } from '@/api/user'
import type { PatientList } from '@/types/user'

const list = ref<PatientList>([])

const loadList = async () => {
  const res = await getPatientList()
  list.value = res.data
}

onMounted(() => {
  loadList()
})

调试过程:转写稿里的验证方式是打开浏览器刷新页面,在 Network 面板看到 myList 请求发出并带回响应数据;再用 Vue Devtools 查看组件实例,list 里确实拿到了三条数据。两步都通过,才说明取数没问题。

易错点:list.value = res.data 别写成 list = res.data,ref 必须走 .value。

第四步:渲染列表 ​

为什么需要:数据有了,要把静态的两张卡片换掉,否则页面上永远只显示两个。

怎么想:静态结构里本来就写了两个 item,把其中一个删掉,剩下的用 v-for 遍历 list,key 用 item.id。

怎么做:先遍历出三个患者卡片,验证条数对得上,再逐个把姓名、身份证、性别、年龄替换成字段插值。

html
<div class="patient-item" v-for="item in list" :key="item.id">
  <span class="name">{{ item.name }}</span>
  <span class="id">{{ item.idCard }}</span>
  <span class="gender">{{ item.genderValue }}</span>
  <span class="age">{{ item.age }}</span>
</div>

身份证脱敏:保留前六后四 ​

为什么需要:身份证号完整展示属于敏感信息,界面上要打码。

怎么想:转写稿给的思路是——取前 6 位、取后 4 位,中间 8 位(年月日那一段)换成星号。用 replace 配合正则和捕获组实现:第一个括号是前 6 位,第二个括号是后 4 位,中间用 .* 匹配掉,替换时 $1 保留第一组、$2 保留第二组,中间写 8 个星号。

html
<span class="id">{{ item.idCard.replace(/(.{6}).*(.{4})/, '$1********$2') }}</span>

调试过程:回到浏览器刷新,能看到前 6 位和后 4 位保留,中间的 8 位数字变成了 8 个星号,脱敏生效。

易错点:星号个数要跟被替换掉的位数对应,写少了会露出数字。转写稿还提示了这个思路可以迁移——手机号脱敏就是把第一组改成 3 位、第二组改成 4 位,中间替换 4 个星号。

条件渲染:默认标签与添加按钮 ​

为什么需要:只有默认患者才该显示“默认”标签;患者满 6 人后,“添加患者”按钮就失去意义,不该再出现。

怎么想:两处都用 v-if 做条件渲染。默认标签判断 item.defaultFlag === 1;添加按钮判断 list.length < 6,与页面上“最多可添加 6 人”的提示呼应。

html
<div class="tag" v-if="item.defaultFlag === 1">默认</div>

<div class="add-btn" v-if="list.length < 6">添加患者</div>

易错点:长度判断要用 < 6 而不是 <= 6,否则第 6 个患者加完按钮还在。另外 v-if 和 v-for 不要写在同一元素上。

到这里,患者列表的数据展示就完整跑通了:类型 → 接口函数 → 挂载取数 → 渲染 → 脱敏 → 条件渲染,后续的添加、编辑、删除都可以复用 loadList 这一个动作。

代码示例 ​

1. 个人中心:快捷工具列表渲染 + 退出登录(Vue3 + Vant) ​

vue
<!-- views/user/profile-page.vue -->
<script setup lang="ts">
import { useRouter } from 'vue-router'
import { showConfirmDialog } from 'vant'
import { useUserStore } from '@/stores/user'

const router = useRouter()
const store = useUserStore()

// 初始化快捷工具数据:每个工具 = 标题 + 跳转地址
const tools = [
  { label: '我的问诊', path: '/user/consult' },
  { label: '我的处方', path: '/' },
  { label: '家庭档案', path: '/user/patient' },
  { label: '地址管理', path: '/' },
  { label: '我的评价', path: '/' },
  { label: '官方客服', path: '/' },
  { label: '设置', path: '/' },
]

const onLogout = async () => {
  // showConfirmDialog 返回 Promise:确认 resolve,取消 reject
  await showConfirmDialog({
    title: '温馨提示',
    message: '您是否确认退出优医问诊?',
  })
  // 能执行到这里说明点了「确认」,点「取消」时 Promise 抛错会中断
  store.delUser()          // 删除本地用户信息
  router.push('/login')    // 跳转登录页
}
</script>

<template>
  <!-- 快捷工具 -->
  <div class="tool">
    <p class="tool-title">快捷工具</p>
    <router-link
      class="tool-cell"
      v-for="(item, i) in tools"
      :key="item.label"
      :to="item.path"
    >
      <img :src="`/src/assets/images/tool/0${i + 1}.png`" alt="" />
      <span>{{ item.label }}</span>
    </router-link>
  </div>

  <!-- 退出登录 -->
  <a href="javascript:;" class="logout" @click="onLogout">退出登录</a>
</template>

要点与易错点:

  • 快捷工具用数组 tools + v-for 渲染,key 用 label(各工具标题唯一),不要用 index。
  • 图标按顺序命名 01~07,用模板字符串 0${i + 1} 动态拼接,index 从 0 开始,必须 +1 才能对上图片序号。
  • 跳转用 router-link 的 :to,不要写死 href;未实现的路由统一指向 /(首页)兜底。
  • showConfirmDialog 必须 await:点「取消」时 Promise 会 reject,后面的删除和跳转就不会执行;如果不用 await,无论确认取消都会执行退出逻辑。
  • 退出顺序:先 delUser() 清本地信息,再 router.push('/login'),顺序颠倒会导致登录页还残留用户状态。

2. 家庭档案:患者列表渲染 + 身份证脱敏 ​

vue
<!-- views/user/patient-page.vue -->
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { getPatientList } from '@/api/user'
import type { PatientList } from '@/types/user'

// 患者列表,初始为空数组
const list = ref<PatientList>([])

// 获取数据的动作单独抽成函数,方便编辑/删除成功后复用
const loadList = async () => {
  const res = await getPatientList()
  list.value = res.data
}

// 组件挂载完毕时获取数据
onMounted(() => {
  loadList()
})
</script>

<template>
  <div class="patient-page">
    <div class="patient-item" v-for="item in list" :key="item.id">
      <div class="info">
        <span class="name">{{ item.name }}</span>
        <!-- 身份证脱敏:保留前 6 位 + 后 4 位,中间 8 位换星号 -->
        <span class="id">
          {{ item.idCard.replace(/^(\d{6})\d{8}(\d{4})$/, '$1********$2') }}
        </span>
        <span class="gender">{{ item.genderValue }}</span>
        <span class="age">{{ item.age }}岁</span>
      </div>
      <van-tag v-if="item.defaultFlag === 1" type="primary">默认</van-tag>
    </div>

    <!-- 最多 6 人,满 6 人就不再显示添加按钮 -->
    <div class="patient-add" v-if="list.length < 6">
      <span>添加患者</span>
    </div>
    <p class="tip">最多可添加6人</p>
  </div>
</template>

要点与易错点:

  • ref<PatientList>([]) 泛型不能漏,否则模板里 item.name 等字段没有类型提示。
  • 取数逻辑一定封装成 loadList 函数再在 onMounted 里调用,后面添加/编辑/删除成功后可直接复用。
  • 脱敏正则 ^(\d{6})\d{8}(\d{4})$ 中 $1 是第一个括号(前 6 位)、$2 是第二个括号(后 4 位),中间 8 位不捕获直接替换成 8 个 *;星号数量要与被替换的位数一致。
  • 同一正则可推广:手机号脱敏改成 ^(\d{3})\d{4}(\d{4})$ + '$1****$2'。
  • v-if="item.defaultFlag === 1" 中的 1 是数字,接口返回若是字符串 '1' 需转换,否则默认标签不显示。
  • v-for 的 key 用 item.id,不要用 index,避免删除后渲染错位。

3. 通用单选按钮组件封装(支持 Vue3 的 v-model) ​

vue
<!-- components/CPRadioButton.vue -->
<script setup lang="ts">
// 选项由父组件传入,支持动态设置(男/女、其他场景的选项)
const props = defineProps<{
  options: { label: string; value: number | string }[]
  modelValue?: number | string   // 可选,仅展示时可以不传
}>()

// 约定自定义事件类型,名字必须是 update:modelValue
const emit = defineEmits<{
  (e: 'update:modelValue', value: number | string): void
}>()
</script>

<template>
  <div class="cp-radio-btn">
    <a
      href="javascript:;"
      class="item"
      v-for="item in options"
      :key="item.value"
      :class="{ active: item.value === modelValue }"
      @click="emit('update:modelValue', item.value)"
    >
      {{ item.label }}
    </a>
  </div>
</template>
vue
<!-- 父组件中使用 -->
<script setup lang="ts">
import { ref } from 'vue'

const gender = ref(1)   // 1=男 0=女
const options = [
  { label: '男', value: 1 },
  { label: '女', value: 0 },
]
</script>

<template>
  <CPRadioButton v-model="gender" :options="options" />
</template>

要点与易错点:

  • Vue3 里 v-model="gender" 会被拆成两部分::modelValue="gender" 传值 + @update:modelValue="gender = $event" 改值。所以子组件必须同时用 modelValue 接收、用 update:modelValue 事件回传,两个名字都不能写错(少一个冒号或写成 update:model-value 都失效)。
  • 选中态用 :class="{ active: item.value === modelValue }" 判断,注意用 === 严格比较,接口值是数字时别写成字符串。
  • 点击时把「当前按钮的值」item.value 传出去,而不是传 modelValue,否则永远选不中别的项。
  • modelValue 声明成可选(modelValue?:),这样只做展示、不绑定 v-model 的场合(如添加患者表单里的静态展示)才不会报“必填属性缺失”。
  • defineProps / defineEmits 只能在 <script setup> 顶层使用,不能写在函数或条件里。
  • Vue3 没有 .sync 修饰符,v-model:count 这种扩展写法等价于 Vue2 的 .sync。

常见错误 ​

  1. 快捷工具图标不随项变化或首项图标缺失
    现象:7 个快捷工具图标重复、错位,或第一项找不到图片。
    原因:v-for 未取 index,或图片编号从 01 开始但拼接时未补零/未 +1。
    修法:使用 v-for="(item, index) in tools",按稳定规则映射图标;涉及 01 编号时显式补零,并验证首尾项。

  2. 退出登录点“取消”仍执行退出或控制台报未捕获异常
    现象:确认框点取消后仍清用户信息/跳登录,或出现 Unhandled rejection。
    原因:ShowConfirmDialog 返回 Promise,取消会 reject,未 await/未处理 reject 分支。
    修法:处理函数改为 async,先 await 确认框;取消分支直接 return,确认后再执行清理与跳转。

  3. 退出后刷新又回到登录态
    现象:退出后已跳登录页,但刷新后仍有登录信息。
    原因:只清了 Pinia/store,未清 localStorage 等持久化数据,或清理 action 未覆盖持久化字段。
    修法:统一调用 store 的退出/清理 action,确认持久化插件配置;验收时检查应用本地存储和刷新后的路由守卫。

  4. 身份证脱敏前后保留位数或星号数量错误
    现象:脱敏后前 6 位/后 4 位不对,星号数量不对,短字符串异常。
    原因:正则分组或 replace 替换串写错,只测了正常长度。
    修法:用分组正则替换并补边界测试,例如 idCard.replace(/^(.{6}).*(.{4})$/, '$1********$2')。

  5. Vue3 子组件写了 v-model 但父组件值不更新
    现象:点击单选/计数器后子组件显示变化,父组件数据不变或事件未触发。
    原因:子组件仍用 value/input,或 emit 名称与 v-model 约定不匹配。
    修法:统一为 modelValue + emit('update:modelValue', val);使用 v-model:count 时对应 update:count。

  6. 仅展示单选按钮时因 modelValue 必填报错
    现象:表单里只渲染 CP-RadioButton 不绑 v-model 就出现类型错误。
    原因:组件把 modelValue 定义为必填 prop。
    修法:改为可选 prop 并提供默认值/条件判断,区分“展示模式”和“双向绑定模式”。

  7. 弹层里点返回按钮跳路由而不是关闭弹层
    现象:添加患者弹层中点击顶部返回,页面路由变化,弹层未按预期关闭。
    原因:通用导航组件默认执行路由返回,未支持业务覆盖。
    修法:导航组件增加可选 back 函数 prop,存在时优先执行并 return;调用方传入关闭弹层的函数。

  8. 患者列表 key 使用索引导致选中态/默认标签错位
    现象:删除、切换或更新列表后,默认标签或选中状态跑到其他项。
    原因:key 使用 index,列表变化后复用节点不稳定。
    修法:使用业务唯一值,如 :key="item.id";同时检查条件渲染边界,如 list.length < 6。

练习 ​

  1. 快捷工具数据驱动渲染
    准备 7 条快捷工具数据(标题、路径),用 v-for 渲染并完成图标编号映射。验收:每项图标不重复、首项为 01、点击可跳转,key 不使用索引。

  2. 退出登录分支处理
    实现点击退出后弹出确认框:取消不产生副作用,确认后清用户信息、清持久化数据并跳登录页。验收:取消无报错、确认后刷新仍为未登录态。

  3. 身份证脱敏函数与边界测试
    抽出 maskIdCard 函数,保留前 6 位和后 4 位,中间替换为 8 个星号。验收:正常 18 位、短字符串、空值均有明确结果,不出现运行时报错。

  4. 单选按钮组件双向绑定
    封装 CP-RadioButton,支持动态 options、选中切换和 v-model;同时支持不传 modelValue 的纯展示场景。验收:父组件值随点击更新,仅展示时不报类型错误。

  5. 弹层返回行为覆盖
    在添加患者弹层中使用 CP-Nav,点击返回只关闭弹层,不改变当前路由。验收:打开弹层、点击返回、再次打开均正常,浏览器地址栏路由不变。

课件图示 ​

课件第39页:家庭档案-添加患者 v-model语法糖 需要封装支持 v-model 的组件

课件第18页:个人中心-快捷工具和退出登录 准备基础结构 初始化数据 完成渲染 准备按钮 弹窗

课件第22页:家庭档案-展示患者列表 路由和组件结构 定义患者类型 患者列表API 获取数据渲

课件第16页:个人中心-用户信息展示 准备基础结构 定义API函数 获取数据 完成渲染

课件第21页:家庭档案 患者列表 添加患者 编辑患者 删除患者

课件第31页:家庭档案-添加患者-侧滑弹层 用popup组件 nav支持关闭 准备表单

原视频对照 ​

#视频时长
1个人中心-快捷工具和退出登录13:33
2家庭档案-展示患者列表-01-路由与组件结构05:19
3家庭档案-展示患者列表-02-完成渲染12:46
4家庭档案-添加患者-v-model语法糖12:43
5家庭档案-单选按钮组件封装12:53
6家庭档案-添加患者界面和侧滑13:40

小结 ​

  1. 列表渲染要先稳定数据契约,再处理图标、key、条件渲染等展示细节。
  2. 异步确认框的关键不在弹框本身,而在取消/确认两条分支的副作用隔离。
  3. 退出登录必须按“清状态、清持久化、导航”闭环验收,不能只看页面跳转。
  4. 类型定义和接口返回类型是列表渲染的前置保障,先类型后渲染能减少联调成本。
  5. 封装 v-model 组件时先跑通父子契约,再用语法糖简写;事件名和 prop 名必须成对。
  6. 可复用组件要允许“展示模式”和“绑定模式”共存,避免可选场景被必填 prop 卡住。
  7. 弹层场景下,通用导航的默认行为应可注入/覆盖,业务组件不要被默认路由返回绑死。
  8. 脱敏、格式化等字符串处理应封装并覆盖边界用例,不能只验证正常数据。