主题
uniCloud 与支付组件(二)
本页对应 uniCloud 与支付组件,共 8 段视频,约 52 分钟。
学习目标
- 能够基于既有患者列表模板完成“选择患者”页面结构与样式搭建,并通过接口获取患者列表数据完成列表渲染。
- 能够使用正则表达式完成身份证号脱敏处理,并正确解释
.、量词、分组及替换在脱敏逻辑中的作用。 - 能够使用索引值控制患者项高亮状态,并实现点击切换高亮项的交互逻辑。
- 能够通过计算属性根据当前索引获取选中患者 ID,并将患者 ID 记录到 Pinia 且完成跨页面持久化使用。
- 能够新增预付订单页面、配置路由跳转,并封装和调用预付订单接口,正确传入必传参数
type后渲染订单金额等信息。
知识点
先顺业务线:为什么一定要“选择患者”
上一节我们做完了病情描述,页面里已经用到了云开发、数据缓存等能力。但流程走到“下一步”之前,有个问题必须先想清楚:到底是谁在看病?
很多同学的第一反应是“我登录的,当然是我看病”。可现实里不是这样——你可能是帮家里的老人挂号,也可能是替朋友咨询。也就是说,登录账号 ≠ 就诊人。如果不把就诊人明确下来,后面的问诊记录、处方、订单都会挂错人。
所以这一步的逻辑是:点“下一步”之前,先从患者列表里选一个人,是给张三看还是给李四看。而这个“患者列表”,其实就是我们之前做过的家庭档案模块——两个模块在这里就关联起来了。
想清楚这一点,再动手写代码,将来面试介绍项目时你才能把流程讲顺:选问诊类型 → 选医院 → 选科室 → 描述病情 → 选就诊人 → 付费。
搭页面模板:把家庭档案的列表搬过来
怎么做:在 patient 目录下新建页面,把之前家庭档案列表的模板结构复制过来,样式文件 index.scss 一并复制并引入。复制完先跑起来看效果,页面应该和家庭档案列表长得一样,只是多了“选择”的交互。
易错点:复制模板时容易漏掉样式文件的引入,页面结构在、但样式全丢,看起来像“没生效”,其实只是没 @import。
拉取患者列表数据
为什么:模板是死的,数据得从接口来。这里要调的接口我们之前封装过(patientListAPI),可以直接复用。
怎么做:在页面里封装一个获取列表的方法,用 ref 接住返回的数组。
js
const patientList = ref([])
async function getPatientList() {
const { code, data, message } = await patientListAPI()
if (code !== 10000) return uni.showToast({ title: message, icon: 'none' })
patientList.value = data
}模板里用 v-for 渲染,key 用数据里的 id:
html
<view
class="patient-card"
v-for="(item, index) in patientList"
:key="item.id"
>
<!-- 患者姓名、身份证号等 -->
</view>调试过程:保存后打开网络面板,确认确实发出了一次请求、返回的是数组;再到 Vue 组件里看 patientList 是否有值。如果返回是空数组,不是代码错,是你账号下还没添加过就诊人,自己先添两条数据再验证。
高亮状态:靠索引,不靠 defaultFlag
这里我一开始讲错了一处,必须纠正:defaultFlag 只表示“是否默认就诊人”,用来控制那个“默认”文字标签,不是用来控制选中高亮的。高亮是“点谁谁亮”,属于标准的 Tab 切换交互,应该按索引值来判断。
怎么做:定义一个记录当前选中索引的响应式数据,默认 0(第一项高亮),用 :class 做条件绑定,再补点击事件切换索引。
js
const patientIndex = ref(0)
function onClickPatient(index) {
patientIndex.value = index
}html
<view
class="patient-card"
:class="{ active: patientIndex === index }"
v-for="(item, index) in patientList"
:key="item.id"
@click="onClickPatient(index)"
>易错点:把 active 写成常量 true,结果所有项一起亮;或者忘了给 v-for 加 (item, index) 的第二参数,导致 index 未定义。
计算属性拿到真正的患者 ID
为什么:高亮只是视觉,业务要的是患者 ID。索引不具备唯一性,切换一下索引就变了,所以索引绝不能当 ID 用。
怎么想:索引是 2,就要拿数组第 2 项的 id;索引变成 1,就拿第 1 项的 id——这不就是“根据一个数据算出另一个数据”吗?天然适合计算属性。
js
const patientId = computed(() => patientList.value[patientIndex.value]?.id)易错点:这里必须加可选链 ?.。如果列表还没回来、数组为空,直接 .id 会报错。这是很常见的坑,凡是“访问可能为空的数组元素再取属性”,都要防一手。
验证方式很简单:切换不同患者,看 patientId 是否跟着变。
补一节正则:身份证号脱敏
列表里的身份证号中间是一串星号,这是用正则替换出来的。
先看基础:. 代表任意字符;{6} 是量词,表示重复 6 次;+ 表示一个或多个;* 表示零个或多个;? 表示零个或一个。括号 () 是分组,分组之后可以用 $1、$2 依次引用。
以 /^(.{6})(.+)(.{4})$/ 为例:开头 6 位任意字符,中间至少一位任意字符,结尾 4 位任意字符。^ 和 $ 分别表示开头和结尾。因为只加了两个括号,所以只有 $1 和 $2:
js
const masked = idCard.replace(/^(.{6}).*(.{4})$/, '$1********$2')前 6 位保留、后 4 位保留,中间一律换成星号。
易错点:量词记混(把 + 当成零个或多个)、括号加了却没用到 $n、或者 ^ $ 漏写导致匹配位置错乱。
记录患者 ID 并跳转到预付订单
为什么:选好的患者 ID 要带到下一个页面去用,而且这个 ID 将来要提交给后端,字段名不能自己随便起,得按后端约定叫 patientId。
怎么做:在 consult 的 store 里新增 patientId,初始为空字符串,return 出去,并做持久化(因为要跨页面取)。
js
const patientId = ref('')在“下一步”按钮上绑点击事件,跳转前先赋值:
js
function onNextStep() {
consult.patientId = patientId.value
uni.navigateTo({
url: '/subpkg/consult/payment/index'
})
}易错点:路径末尾写成 index.vue 是文件路径不是路由路径;另外 pages.json 里新增页面配置后,如果页面文件还没建,会报错——可以先建文件再贴配置。
验证:切到 Vue 面板看 store 里的 patientId,点击不同患者再点下一步,确认存进去的是对应那条的 ID。
预付订单页:先搭壳,再调接口
新建 payment 页面(注意这是新增页面,分包已经有了,只复制单条页面配置即可)。页面里会展示医生信息、金额、优惠券与积分折扣、实付金额、患者信息与病情。这里要调两个接口:上方订单信息、下方患者信息。
以订单接口为例,type 是必传的(1 找医生、2 极速问诊、3 开药问诊),而这个 type 正是首页选择时存进 Pinia 的,跨了好几个页面才用到——这就是跨页面取值的典型场景。封装时把必传的 type 放第一个参数,其余可选项收进 options 对象,调用时展开:
js
export const preOrderAPI = (type, options = {}) =>
request({ url: '/xxx/preOrder', method: 'GET', data: { type, ...options } })js
async function createPreOrder() {
const { code, data, message } = await preOrderAPI(consult.type)
if (code !== 10000) return uni.showToast({ title: message, icon: 'none' })
}这样封装的好处是参数再多也不啰嗦:想传就传个对象,不想传就省略。返回数据里 actualPayment 是实付、couponDeduction 是优惠券折扣、pointsDeduction 是积分折扣、payment 是原价,自己对着字段渲染即可。注意 Deduction 这个词就是“折扣/抵扣”,别和支付金额搞混。
业务背景:为什么要在问诊前选择患者
为什么需要:用户登录账号不等于实际就诊人。家里老人、朋友都可能使用同一个账号问诊,所以病情描述完成后必须确认“这次给谁看病”。
怎么想:把“选择患者”与之前的“家庭档案”关联起来,家庭档案列表就是患者数据源。
怎么做:在问诊流程的病情描述页之后增加患者选择页,页面加载时查询患者列表,用户点选后记录患者 ID,再进入预付订单页。
易错点:不要把当前登录用户直接当成就诊人;也不要把“默认就诊人”当成“当前选中患者”。
页面模板与接口复用
为什么需要:患者选择页与之前的患者列表页功能高度一致,重复造轮子会增加维护成本。
怎么想:先复用页面模板和样式,再复用已封装的 patient list API;但不要大段盲目粘贴,最好顺着思路写,避免粘错。
怎么做:在 patient 目录下建立页面与 index.scss,把列表模板、样式迁移过来;在逻辑中封装 getPatient,调用患者列表接口,用 ref 保存 patientList。
易错点:接口返回后要判断 code 是否成功,失败时 reject(message);列表为空时先检查网络请求和返回数组,不要先怀疑模板。调试时可在网络面板确认请求,再在组件中确认 patientList 是否有两个单元。
列表渲染与默认标记
为什么需要:接口数据必须渲染到页面,用户才能选择。
怎么想:用 v-for 遍历 patientList,以患者 ID 作为 key。
怎么做:渲染患者信息;默认就诊人由 defaultFlag 控制“默认”字样,而不是控制高亮。
易错点:defaultFlag === 1 只说明默认就诊人;高亮状态要另用索引控制。身份证号脱敏不要手写截取,按转写稿用正则替换:保留前 6 位和后 6 位,中间统一替换为星号。
正则脱敏的推导
为什么需要:身份证号不能完整展示,需要脱敏。
怎么想:点号表示任意字符,\d 表示数字,{6} 表示 6 位,+ 表示至少一个,? 表示零个或一个,* 表示零个或多个,$ 表示结尾;括号表示分组,可用 $1、$2、$3 引用。
怎么做:把前 6 位、中间部分、后 6 位分组,替换时保留 $1 和 $3,中间替换为星号。
易错点:量词含义要背熟;分组后才知道 $1/$2/$3 对应哪一段;不需要的中间段可不加括号。
高亮状态切换
为什么需要:用户点谁谁亮,才能明确当前选择。
怎么想:这是标准 tab 切换,用索引值控制类名,而不是用 defaultFlag。
怎么做:定义 patientIndex = ref(0),给卡片加 :class="{ active: patientIndex === index }";点击时传入 index,在 onClick 中修改 patientIndex.value。
易错点:初始值设为 0 使第一项高亮;点击事件要传索引;不要把默认就诊人标志和高亮混用。调试时点不同患者,观察高亮是否跟随索引变化。
用计算属性获取患者 ID
为什么需要:高亮只是交互,真正提交给后端的是患者 ID,索引不具唯一性。
怎么想:根据当前索引从 patientList 中取对应患者的 ID,这正是计算属性场景。
怎么做:用 computed 返回 patientList.value[patientIndex.value]?.id;访问空数组单元时加可选链,避免 undefined 再取属性报错。
易错点:计算属性不要忘 return;数组要取 .value;索引变化后 ID 应自动变化。调试时切换患者,观察患者 ID 是否跟着变。
跳转预付订单并记录患者 ID
为什么需要:下一步要进入付款确认页,并且后续页面还要使用患者 ID。
怎么想:按钮用点击事件,不用链接跳转;跨页面数据放 Pinia,并做持久化。
怎么做:在 consult store 中定义 patientId,初始为空字符串并 return;点击下一步时执行 uni.navigateTo 跳到 payment 页面,同时把计算出的患者 ID 写入 consult.patientId。
易错点:路径写到 index,不要写 index.vue;先建页面再配路由,避免配置报错;Pinia 数据要持久化,否则跨页面或刷新后丢失。调试时可在 Pinia 中查看 patientId 是否从空变为所选患者 ID。
预付订单接口封装与调用
为什么需要:订单页要展示医生信息、费用、优惠券折扣、积分折扣、实付金额和患者信息,需要调接口。
怎么想:上部分调预付订单接口,下部分调患者信息接口;type 是问诊类型且必传,之前已存在 Pinia 中,跨页面取用。
怎么做:在 service/consult 中封装方法,第一个参数为必传 type,其余可选参数用对象接收并展开;页面加载时调用 createPreOrder,用 async/await 取 code、data、message,失败 reject(message)。
易错点:type 必传,极速问诊对应值为 2;可选参数用对象接收后调用更灵活;解构 Pinia 时注意是否需要 .value;返回数据中 payMoney 是实付,couponDiscount、pointDiscount 是折扣,别把字段对应错。
本节定位:从“选择患者”引出正则补充
这一节的主线是“选择患者”,但真正需要补充的知识点是正则表达式。转写中先完成患者列表渲染、选中高亮、记录患者 ID,再跳转“预付订单”页面;在渲染身份证号时,需要对中间部分做星号脱敏,于是补充正则的分组、量词、开始/结束符等基础。本节笔记没有图片资源,因此不虚构图片;下面保留转写中涉及的正则片段与页面逻辑代码片段,并纠正“政则/刀拉/所引值/高量”等转写同音字为“正则/$1/索引值/高亮”。
为什么需要正则:身份证脱敏不能靠手写截取
选择患者列表里要展示身份证号,但不能明文展示。为什么需要正则?因为身份证长度、前后保留位数、替换规则可能变化,用字符串截取虽然能做,但表达不清;正则可以把“前 6 位、中间任意字符、后若干位”写成规则,再用分组保留需要展示的部分。怎么想?先把需求翻译成“匹配整体,再分组保留前后,中间统一替换为星号”。怎么做?转写中先讲 . 表示任意字符,\d 表示数字,{6} 表示 6 位,+ 表示至少一个,{4} 表示 4 位,^ 与 $ 表示开始和结束,括号表示分组。对应片段可整理为:
js
// 转写中涉及的正则片段(非完整源码)
// . 任意字符
// \d 数字
// {6} 6 位
// + 至少一个
// {4} 4 位
// ^...$ 整体开始与结束
// (...) 分组,可用 $1、$2 引用
/^(.{6}).+(.{4})$/替换时可写成类似 str.replace(正则, '$1****$2') 的思路:$1 保留前 6 位,$2 保留后 4 位,中间替换为星号。易错点:转写后段口述出现过“后面是 6 位”,但前面示例用的是 .{4},二者不一致;实际应以当前脱敏需求和调试结果为准,不能把口述笔误直接当代码。另一个易错点是 replace 返回新字符串,不会自动改原变量。
怎么想量词与分组:先拆规则,再拼整体
为什么容易混?因为 .、?、*、+ 都是正则基础,但含义不同。怎么想?把量词当成“数量修饰符”:? 是零个或一个,* 是零个或多个,+ 是一个或多个,{n} 是恰好 n 个。转写中反复强调这些要背熟。怎么做?例如 . {6} 可理解为“6 个任意字符”,.+ 是“至少一个任意字符”,.{4} 是“4 个任意字符”。括号的作用不是改变匹配字符,而是“分组”,让 $1、$2、$3 能引用。转写中把三段分别括起来后,第一组是前 6 位,第二组是中间段,第三组是后 4 位;如果不需要中间段,就不必给中间加括号,否则 $2 的指向会变化。易错点:括号数量一变,$1/$2/$3 全部要重新核对;^ 和 $ 是边界,不是普通字符;. 默认不匹配换行,但在身份证场景通常够用。
怎么做页面联动:高亮看索引,患者 ID 看计算属性
为什么不能把默认就诊人标志当选中状态?转写中专门纠正:default flag 只用于显示“默认”字样,不代表当前高亮;高亮应跟随用户点击,用索引值控制。怎么想?列表用 v-for 渲染,给每一项加 :class="{ active: patientIndex === index }",点击时把 index 传给事件,更新 patientIndex。怎么做?转写中的关键逻辑是:
js
// 高亮索引,初始让第一个高亮
const patientIndex = ref(0)
// 点击切换
function onChangePatient(index) {
patientIndex.value = index
}真正要提交的是患者 ID,不是索引。索引只控制高亮,ID 才有唯一性。于是用计算属性根据索引取当前患者 ID,并注意空数组保护:
js
const patientId = computed(() => {
return patientList.value[patientIndex.value]?.id
})易错点:直接访问空数组元素再取属性可能报错,转写中建议加 ?.;跳转预付订单页时,要把患者 ID 存入 Pinia,并持久化以便跨页面使用。调试时可在网络面板确认列表接口返回数组,在页面确认 v-for 渲染数量,再点击不同患者观察高亮和 ID 是否变化。这样,正则补充不是孤立语法,而是服务于“患者信息脱敏展示”这一真实业务点。
代码示例
示例一:正则表达式补充 —— 身份证脱敏(可直接在浏览器运行)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>正则补充:身份证脱敏</title>
<style>
body { font-family: sans-serif; padding: 20px; }
.box { background: #f5f5f5; padding: 12px; border-radius: 6px; margin-bottom: 10px; }
.hl { color: #e64340; font-weight: bold; }
</style>
</head>
<body>
<div class="box">原始:<span id="origin"></span></div>
<div class="box">脱敏:<span id="masked" class="hl"></span></div>
<div class="box">分组:<span id="groups"></span></div>
<script>
const idCard = '110101199003072316'
// 6位任意字符 + 至少1位任意字符 + 4位任意字符结尾
const reg = /^(.{6})(.+)(.{4})$/
document.getElementById('origin').textContent = idCard
// $1 保留前 6 位,中间统一替换成星星,$3 保留后 4 位
document.getElementById('masked').textContent = idCard.replace(reg, '$1********$3')
const m = idCard.match(reg)
document.getElementById('groups').textContent = m
? `$1 = ${m[1]} ;$2 = ${m[2]} ;$3 = ${m[3]}`
: '未匹配'
// 量词对照:? 0或1,* 0或多个,+ 1或多个,{6} 恰好6位
console.log('? ', /^a?b$/.test('b')) // true
console.log('* ', /^a*b$/.test('aaab')) // true
console.log('+ ', /^a+b$/.test('b')) // false
</script>
</body>
</html>要点解释
.代表任意字符(数字、字母、下划线等都在内),所以.{6}就是“6 位任意字符”。- 量词必须背熟:
?是 0 个或 1 个,*是 0 个或多个,+是一个或多个,{n}是恰好 n 位。 ^是开头、$是结尾,不写这两个锚点,正则可能只匹配字符串的一部分。- 括号是分组,加了括号才有
$1 / $2 / $3(或match返回的m[1] / m[2] / m[3]);不加括号,$2就取不到。 - 易错点:
replace(reg, '$1********$3')中$2是被整体丢掉的中间段,所以中间几位不管是什么,最后都变成星星。
示例二:选择患者列表 —— 高亮切换 + 计算属性取患者 ID
vue
<template>
<view class="patient-page">
<view
class="patient-card"
v-for="(item, index) in patientList"
:key="item.id"
:class="{ active: patientIndex === index }"
@click="onClick(index)"
>
<view class="patient-name">
{{ item.name }}
<!-- defaultFlag 只控制“默认”这个字,不控制高亮 -->
<text v-if="item.defaultFlag === 1" class="default-tag">默认</text>
</view>
<view class="patient-id">{{ maskIdCard(item.idCard) }}</view>
</view>
<button class="next-btn" @click="onNext">下一步</button>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { patientListAPI } from '@/services/patient'
import { useConsultStore } from '@/stores/consult'
const patientList = ref([])
const patientIndex = ref(0) // 高亮显示的索引值,默认第一个亮
const consultStore = useConsultStore()
// 点谁谁亮:用索引值做 tab 切换
function onClick(index) {
patientIndex.value = index
}
// 计算属性:根据索引值拿到被选中患者的 ID(索引值没有唯一性,ID 才有)
const patientId = computed(() => {
return patientList.value[patientIndex.value]?.id
})
// 身份证脱敏
function maskIdCard(str) {
if (!str) return ''
return str.replace(/^(.{6})(.+)(.{4})$/, '$1********$3')
}
async function getPatientList() {
const { code, data, message } = await patientListAPI()
if (code !== 10000) return uni.showToast({ title: message, icon: 'none' })
patientList.value = data
}
// 记录患者 ID 到 pinia,再跳转预付订单页
function onNext() {
consultStore.consult.patientId = patientId.value
uni.navigateTo({ url: '/subpkg/consult/payment/index' })
}
onMounted(() => getPatientList())
</script>
<style lang="scss" scoped>
.patient-page { padding: 30rpx; }
.patient-card {
background: #fff;
border-radius: 12rpx;
padding: 30rpx;
margin-bottom: 24rpx;
border: 2rpx solid transparent;
&.active { border-color: #16c2a3; background: #f2fffc; }
}
.patient-name { font-size: 32rpx; color: #333; }
.default-tag {
font-size: 20rpx; color: #fff; background: #16c2a3;
padding: 0 8rpx; border-radius: 4rpx; margin-left: 8rpx;
}
.patient-id { font-size: 26rpx; color: #999; margin-top: 10rpx; }
.next-btn {
margin-top: 40rpx; background: #16c2a3; color: #fff; border-radius: 44rpx;
}
</style>要点解释
- 接口
patientListAPI之前已经封装过,这里直接复用,取到数据后patientList.value = data存起来再v-for渲染。 :class="{ active: patientIndex === index }"是高亮的关键:只有索引值相等的那一项才会加上active。- 易错点:高亮不是根据
defaultFlag判断的。defaultFlag === 1只用来显示“默认”这两个字,选中状态是索引值控制的 tab 切换。 - 易错点:
patientList.value[patientIndex.value]?.id里的?必须加,数组为空或索引越界时直接点.id会报错。 - 易错点:
v-for的:key要用item.id,不能用索引;索引值只用于高亮和取值,不代表具体某个患者。
示例三:预付订单 —— 接口封装(可选参数)+ 页面调用
js
// services/consult.js
import request from '@/utils/request'
// type 必传;其余参数(优惠券、积分等)都可选,统一用 options 收集
export const getPreOrderAPI = (type, options = {}) =>
request({
url: '/consult/order/pre',
method: 'GET',
data: { type, ...options }
})js
// pages/consult/payment/index.vue 的 script 部分
import { ref, onMounted } from 'vue'
import { getPreOrderAPI } from '@/services/consult'
import { useConsultStore } from '@/stores/consult'
const consultStore = useConsultStore()
const preOrderInfo = ref({})
async function createPreOrder() {
// type 在首页就存进了 pinia:1 找医生 / 2 极速问诊 / 3 开药问诊
const { code, data, message } = await getPreOrderAPI(consultStore.consult.type)
if (code !== 10000) return uni.showToast({ title: message, icon: 'none' })
preOrderInfo.value = data
}
onMounted(() => createPreOrder())要点解释
type是必传参数,所以放在第一个形参;后面一堆可选参数用options = {}收,调用时传对象就展开进data,不传就是空对象,等于什么都没带。- 这样封装的好处:避免
(type, a, b, c, d)五个形参,调用方不传时也不会乱序;传参只是getPreOrderAPI(2, { patientId })。 type的值来自 pinia,是跨页面取的:首页存2,中间经过病情描述页、选择患者页,到这里才用上,所以 store 里必须return出去才能被拿到。- 易错点:
if (code !== 10000)要写return,否则失败时还会继续执行下面的赋值。 - 接口返回里的
actualPayment(实付)、couponDeduction(优惠券折扣)、pointDeduction(积分折扣)、payment(原价)都要对上字段名再渲染,不要自己造名字。
常见错误
1. 默认就诊人一进来就高亮,点别人却换了"默认"标识
- 现象:页面初始只有
defaultFlag === 1的那条被选中,但点击其他患者时,高亮没跟手,或者"默认"字样跟着跑。 - 原因:把"是否默认就诊人"这个业务字段当成了选中态去驱动
active类,两个语义混成一个变量。 - 修法:拆成两套。
defaultFlag只控制"默认"标签的显隐;高亮用独立索引patientIndex(初值 0),:class="{ active: index === patientIndex }",点击时只改索引。
2. 计算属性取 ID 时偶发 Cannot read property 'id' of undefined
- 现象:列表接口还没回来或返回空数组,控制台直接报错,页面白屏。
- 原因:
patientList.value[patientIndex].id在数组为空时是undefined.id。 - 修法:
patientList.value[patientIndex]?.id,或patientList.value.length ? ... : '';同时给patientList初值给[]而不是null。
3. 身份证号脱敏后变成 ****** 或 $1 原样输出
- 现象:渲染出来要么整串被星号吃掉,要么页面上直接显示
$1****$2。 - 原因:分组括号写多了/写少了导致
$1$2错位;或者把替换串写进了模板插值里被当成普通文本。 - 修法:脱敏逻辑放到 JS(computed 或 filter)里算,模板只渲染结果。正则用
str.replace(/^(.{6}).*(.{4})$/, '$1****$2'),只保留两个捕获组,中间不分组。
4. 跳转预付订单页报"页面不存在"
- 现象:点下一步没反应,或提示找不到页面。
- 原因:三种常见情况——
pages.json加了配置但目录/文件没建;uni.navigateTo的 url 写成了/subpkg/consult/payment/index.vue;路径少了分包前缀。 - 修法:url 写
/subpkg/consult/payment/index(不带后缀,index不能省);先建payment/index.vue再改配置;改完配置手动刷新编译,别指望热更新兜底。
5. 跳到下一页后 consult.patientId 是空的
- 现象:选好了患者,下一页读
consult.patientId拿到''。 - 原因:只做了页面跳转,没在跳转前写值;或 Pinia 里定义了 state 却忘了
return,外部根本拿不到;或没开持久化,刷新即丢。 - 修法:点击事件里先赋值
consult.patientId = patientId.value再navigateTo;store 里return { patientId };需要跨页面/刷新保活的字段加持久化插件配置。
6. 调用预付订单接口提示缺少参数 type
- 现象:接口 400/参数校验失败,network 里请求 payload 里没有
type。 - 原因:封装时把
type定成第一必传参,调用时忘了传;或从 store 解构type时漏了.value(用storeToRefs时则不能带.value)。 - 修法:调用处显式
preOrder(consult.type, options);统一用storeToRefs解构,模板和逻辑里都不再加.value,避免两种风格混用。
7. v-for 渲染列表没写 :key 或 key 用了 index
- 现象:切换高亮时高亮框"串位",或删除后渲染错乱。
- 原因:
v-for缺:key,或:key="index"在列表变动时失效。 - 修法:
:key="item.id",业务唯一 ID 优先,index 只在纯静态列表里用。
练习
练习 1(脱敏) 给一段 18 位身份证号 110101199003078888,用正则输出 110101********8888。要求:写出正则与替换串,并说明 $1、$2 分别对应哪一段;再回答如果把中间段也加上括号,替换串要怎么改才不出错。
练习 2(高亮切换) 基于 patientList 与 patientIndex,补全列表项:点击任意一条切换高亮、首条默认高亮、"默认"标签只由 defaultFlag 控制。要求用一行 :class 完成,并说明为什么不能用 defaultFlag 做高亮判断。
练习 3(计算属性取 ID) 写一个 computed patientId,根据 patientIndex 从 patientList 中取 id。要求:列表为空时不报错、切换索引时自动更新。再写一句说明:为什么这个值不能直接存进 ref 而在点击事件里同步赋值?
练习 4(跳转 + 存值) 实现"下一步":把选中的 patientId 存入 Pinia 的 consult 模块(字段名 patientId,需持久化),然后跳转到 /subpkg/consult/payment/index。要求写出 store 侧(state + return + 持久化)和页面侧(引入、赋值、跳转)的完整代码,并列出漏掉任一步会出现的现象。
练习 5(接口封装) 把 preOrder 封装成"必传 type + 可选 options 对象"的形式,调用时用展开运算符合并参数。写完后回答两个问题:① 如果 options 不传,最终发给后端的 payload 是什么?② 相比 preOrder(type, couponId, point, patientId, ...) 的写法,这种封装在调用侧和后期加字段时各有什么好处?
课堂画面






原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 优医咨询-选择患者模板 | 03:20 |
| 2 | 优医咨询-选择患者列表 | 06:01 |
| 3 | 正则表达式补充 | 09:27 |
| 4 | 优医咨询-选择患者高亮状态切换 | 04:16 |
| 5 | 优医咨询-选择患者获取患者ID | 05:15 |
| 6 | 优医咨询-预付订单页面模板 | 04:59 |
| 7 | 优医咨询-记录患者ID | 08:12 |
| 8 | 优医咨询-预付订单信息 | 11:25 |
小结
- 选中态和数据态必须分离:
active由索引驱动,defaultFlag只管业务标签。一个变量担两种语义,早晚出错——本次课上讲师自己就先踩了一遍再纠正。 - 正则的括号就是分组,分组就是
$n:捕获组数量决定$1/$2/$3的指向,多一个括号整串替换就错位。.?*+{n}这些量词必须形成肌肉记忆,别再回头查。 - 访问数组单元属性前先想"它可能是空的":
?.和默认值[]是成本最低的防御,比事后加 try/catch 划算得多。 - 跨页面数据走 Pinia,不走参数拼接:
patientId、type这类要在多个页面反复用的值,统一进 store + 持久化;navigateTo的 query 只适合一次性、非敏感的短参数。 - 页面三件套缺一不可:
pages.json配置、目录与.vue文件、.scss样式文件。三者任意一个缺失,报错信息都指向"页面不存在",容易误判方向。 - 接口封装优先"必传 + 可选对象":参数超过 3 个就收进 options,用展开运算符合并。调用侧干净,后端加字段时改动面也小。
- 这类"标签页式高亮切换"在本项目已出现三次以上,属于可复用交互模式,值得抽成通用逻辑而不是每次重写。
- 业务线要能一句话讲清:选问诊类型 → 选医院 → 选科室 → 描述病情 → 选患者 → 确认预付订单 → 支付。面试讲项目讲的是这条线,不是某个 API 的名字。