主题
布局容器与家庭档案(二)
本页对应 布局容器与家庭档案,共 6 段视频,约 1 小时 10 分。
学习目标
- 能够基于快捷工具数组数据渲染个人中心快捷工具列表,并实现点击跳转、动态图标及退出登录确认、清除用户信息并跳转登录页。
- 能够为家庭档案配置一级路由与患者页面组件,并正确接入静态页面结构。
- 能够定义患者及患者列表 TypeScript 类型,编写获取患者列表 API,并在组件挂载时请求数据完成列表渲染。
- 能够使用正则替换实现身份证号前 6 位和后 4 位保留、中间 8 位脱敏的展示效果。
- 能够说明 Vue3 中
v-model对应的modelValue与update:modelValue机制,并封装支持动态选项、选中切换和双向绑定的单选按钮组件。 - 能够使用 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。
常见错误
快捷工具图标不随项变化或首项图标缺失
现象:7 个快捷工具图标重复、错位,或第一项找不到图片。
原因:v-for未取index,或图片编号从01开始但拼接时未补零/未+1。
修法:使用v-for="(item, index) in tools",按稳定规则映射图标;涉及01编号时显式补零,并验证首尾项。退出登录点“取消”仍执行退出或控制台报未捕获异常
现象:确认框点取消后仍清用户信息/跳登录,或出现Unhandled rejection。
原因:ShowConfirmDialog返回 Promise,取消会 reject,未await/未处理 reject 分支。
修法:处理函数改为async,先await确认框;取消分支直接return,确认后再执行清理与跳转。退出后刷新又回到登录态
现象:退出后已跳登录页,但刷新后仍有登录信息。
原因:只清了 Pinia/store,未清 localStorage 等持久化数据,或清理 action 未覆盖持久化字段。
修法:统一调用 store 的退出/清理 action,确认持久化插件配置;验收时检查应用本地存储和刷新后的路由守卫。身份证脱敏前后保留位数或星号数量错误
现象:脱敏后前 6 位/后 4 位不对,星号数量不对,短字符串异常。
原因:正则分组或replace替换串写错,只测了正常长度。
修法:用分组正则替换并补边界测试,例如idCard.replace(/^(.{6}).*(.{4})$/, '$1********$2')。Vue3 子组件写了
v-model但父组件值不更新
现象:点击单选/计数器后子组件显示变化,父组件数据不变或事件未触发。
原因:子组件仍用value/input,或emit名称与v-model约定不匹配。
修法:统一为modelValue+emit('update:modelValue', val);使用v-model:count时对应update:count。仅展示单选按钮时因
modelValue必填报错
现象:表单里只渲染CP-RadioButton不绑v-model就出现类型错误。
原因:组件把modelValue定义为必填 prop。
修法:改为可选 prop 并提供默认值/条件判断,区分“展示模式”和“双向绑定模式”。弹层里点返回按钮跳路由而不是关闭弹层
现象:添加患者弹层中点击顶部返回,页面路由变化,弹层未按预期关闭。
原因:通用导航组件默认执行路由返回,未支持业务覆盖。
修法:导航组件增加可选back函数 prop,存在时优先执行并return;调用方传入关闭弹层的函数。患者列表
key使用索引导致选中态/默认标签错位
现象:删除、切换或更新列表后,默认标签或选中状态跑到其他项。
原因:key使用index,列表变化后复用节点不稳定。
修法:使用业务唯一值,如:key="item.id";同时检查条件渲染边界,如list.length < 6。
练习
快捷工具数据驱动渲染
准备 7 条快捷工具数据(标题、路径),用v-for渲染并完成图标编号映射。验收:每项图标不重复、首项为01、点击可跳转,key不使用索引。退出登录分支处理
实现点击退出后弹出确认框:取消不产生副作用,确认后清用户信息、清持久化数据并跳登录页。验收:取消无报错、确认后刷新仍为未登录态。身份证脱敏函数与边界测试
抽出maskIdCard函数,保留前 6 位和后 4 位,中间替换为 8 个星号。验收:正常 18 位、短字符串、空值均有明确结果,不出现运行时报错。单选按钮组件双向绑定
封装CP-RadioButton,支持动态options、选中切换和v-model;同时支持不传modelValue的纯展示场景。验收:父组件值随点击更新,仅展示时不报类型错误。弹层返回行为覆盖
在添加患者弹层中使用CP-Nav,点击返回只关闭弹层,不改变当前路由。验收:打开弹层、点击返回、再次打开均正常,浏览器地址栏路由不变。
课件图示






原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 个人中心-快捷工具和退出登录 | 13:33 |
| 2 | 家庭档案-展示患者列表-01-路由与组件结构 | 05:19 |
| 3 | 家庭档案-展示患者列表-02-完成渲染 | 12:46 |
| 4 | 家庭档案-添加患者-v-model语法糖 | 12:43 |
| 5 | 家庭档案-单选按钮组件封装 | 12:53 |
| 6 | 家庭档案-添加患者界面和侧滑 | 13:40 |
小结
- 列表渲染要先稳定数据契约,再处理图标、
key、条件渲染等展示细节。 - 异步确认框的关键不在弹框本身,而在取消/确认两条分支的副作用隔离。
- 退出登录必须按“清状态、清持久化、导航”闭环验收,不能只看页面跳转。
- 类型定义和接口返回类型是列表渲染的前置保障,先类型后渲染能减少联调成本。
- 封装
v-model组件时先跑通父子契约,再用语法糖简写;事件名和 prop 名必须成对。 - 可复用组件要允许“展示模式”和“绑定模式”共存,避免可选场景被必填 prop 卡住。
- 弹层场景下,通用导航的默认行为应可注入/覆盖,业务组件不要被默认路由返回绑死。
- 脱敏、格式化等字符串处理应封装并覆盖边界用例,不能只验证正常数据。