主题
首页模板布局(二)
本页对应 首页模板布局,共 7 段视频,约 56 分钟。
学习目标
- 能够独立创建分包页面并正确配置路由,避免将分包页面误注册到主包。
- 能够使用
v-model获取姓名、身份证号、性别等表单数据,并通过:model传给uni-forms组件。 - 能够通过
switch的change事件读取event.detail.value,将其转换为后端要求的0/1,并设置默认选中状态。 - 能够为姓名、身份证号、性别配置表单校验规则,并通过
formRef.value.validate()完成提交前校验。 - 能够使用
validateFunction根据身份证号第 17 位奇偶性校验性别是否一致,并返回对应错误提示。 - 能够封装添加患者 API,在保存时提交表单数据,根据返回
code判断成功并返回患者列表页。
知识点
一、先处理分包页面:不要直接“新建页面”就注册
为什么需要:添加患者页属于分包页面,页面必须注册到分包配置里;如果按 HBuilderX 的“新建页面”并勾选“在 pages.json 中注册”,它容易把页面注册到主包,后面分包结构就乱了。
怎么想:分包目录手动建更灵活,页面名想叫 index 就叫 index,想叫 A 就叫 A。如果仍用“新建页面”,要取消“在 pages.json 中注册”,并注意“同名目录”选项。
怎么做:右键新建文件夹(如 test),在文件夹里新建 .vue 文件作为页面;或者新建页面时取消自动注册。转写中临时建的 test 目录后来删掉了,说明目录名可按项目需要调整。
易错点:勾选自动注册导致主包多出页面;把“新建页面”和“新建文件夹+新建 .vue”混用后找不到注册位置。调试时先看 pages.json 的分包配置,再确认页面路径。
二、先做添加患者页,再连列表入口
为什么需要:列表页要先有患者数据才能看到效果,所以开发顺序应是“先添加患者,再查询列表”。但正常入口确实应跳列表页。
怎么想:为了调试方便,先把入口链接临时改到添加患者页,等功能通了再改回列表页。
怎么做:把入口地址临时改成添加患者页;把表单模板放到 form/index.vue,并新建/引入 SCSS 样式文件。页面本质就是一堆表单项:姓名、身份证号、性别、是否默认就诊人等。
易错点:忘记把入口改回列表页;样式文件没引入导致页面难看;表单项结构不完整。转写中特别提醒:这里没有登录页的验证码、Tab 切换,核心就是“表单验证 + 保存调接口”。
三、获取表单数据:v-model、uni-data-checkbox、switch
为什么需要:验证前必须先拿到表单数据。小程序原生表单双向绑定不方便,uni-data-checkbox 封装后支持 v-model,所以性别用它可以方便取值。
怎么想:姓名、身份证号、性别直接绑定到 formData;switch 是原生组件,不支持 v-model,要通过 change 事件拿 e.detail.value。
怎么做(转写中提到的关键片段,字段名以项目为准):
html
<uni-forms ref="formRef" :rules="rules">
<uni-forms-item name="name">
<input v-model="formData.name" />
</uni-forms-item>
<uni-forms-item name="idCard">
<input v-model="formData.idCard" />
</uni-forms-item>
<uni-forms-item name="gender">
<uni-data-checkbox v-model="formData.gender" :localdata="genderOptions" />
</uni-forms-item>
<switch
:checked="formData.defaultFlag === 1"
@change="onSwitchChange"
/>
</uni-forms>js
function onSwitchChange(e) {
// 后端要 0/1,不要布尔值
formData.value.defaultFlag = e.detail.value ? 1 : 0
}易错点:转写中曾把身份证字段写错,检查后改回 idCard;switch 拿到的是 true/false,必须转成 1/0;uni-data-checkbox 的选项用 localdata,选中颜色可自定义,但不要和原生 radio/checkbox 混用。
四、写验证规则并传给 uni-forms
为什么需要:姓名、身份证号、性别要校验;中文姓名和身份证号都有固定规则,默认就诊人可不验证。
怎么想:rules 里每个字段的 name 要和表单项的 name 对应;中文姓名用 Unicode 范围 \u4e00-\u9fa5,限制 2 到 5 个字;身份证号用讲义/工具生成的固定正则,不必背;性别只做必填。
怎么做:
js
const rules = {
name: {
rules: [{ required: true }, { pattern: /^[\u4e00-\u9fa5]{2,5}$/, errorMessage: '请输入中文姓名' }]
},
idCard: {
rules: [{ required: true }, { pattern: /身份证号固定正则(以讲义为准)/, errorMessage: '身份证号格式不正确' }]
},
gender: {
rules: [{ required: true }]
}
}模板上通过 :rules="rules" 传给 uni-forms。
易错点:表单项 name 和规则字段不一致;转写中曾把 validateFunction 放到 rules 平级,正确应放到 rules 数组里对应对象下;默认就诊人没验证,所以后面 validate() 的返回值里不会包含它。
五、调用 validate 并调整错误提示样式
为什么需要:点保存时先验证;默认错误提示位置不好看,要按 UI 稿调整。
怎么想:用 ref 找到表单组件,调 validate();失败用 try/catch 捕获;错误提示类名是 uni-forms-item__error,可改绝对定位、left: -220rpx、right: 0、top: auto,去掉 transition 小动画。
怎么做:
js
const formRef = ref()
async function onFormSubmit() {
try {
await formRef.value.validate()
// 验证通过
} catch (e) {
// 验证失败
}
}易错点:ref 名和模板不一致;忘记 await;样式调整要按 UI 稿,不要死记 -220rpx;转写中强调“这里不是永远这样写”。
六、自定义校验:身份证第 17 位判断性别
为什么需要:用户可能身份证是男却选女,要以身份证为准;密码/确认密码也适合这种组合校验。
怎么想:身份证第 17 位奇数为男、偶数为女;在 validateFunction 中取第 17 位,与表单性别比较。
怎么做:
js
validateFunction: function (rule, value, data, callback) {
const idCard = formData.value.idCard
const sexBit = Number(idCard.slice(16, 17)) // 第17位从索引16开始
const idCardGender = sexBit % 2 === 0 ? 0 : 1
if (idCardGender !== value) {
callback('与身份证号性别不一致')
}
return true
}易错点:slice 起点写错;validateFunction 层级放错;callback 用来提示错误;测试要用符合规则的身份证号,乱编过不了正则。转写中调试时先 log 参数,确认 value 是当前性别值。
七、封装并调用添加患者接口
为什么需要:验证通过后提交数据,成功返回列表页。
怎么想:患者相关接口单独放 service/patient.js;封装函数要 return 请求结果;页面中 await 验证后提交,判断 code。
怎么做(路径和地址以项目为准):
js
import http from '项目工具目录/http'
export function addPatientAPI(data) {
return http.post('接口地址(以文档为准)', data)
}js
const res = await addPatientAPI(formData.value)
if (res.code !== 10000) {
uni.showToast({ title: res.message })
return
}
uni.navigateBack()易错点:封装函数忘记 return;接口参数不要用 validate() 的返回值,因为它只包含被验证字段,默认就诊人不在里面,转写中改为用表单数据;成功判断 code,失败提示 message;navigateBack 依赖入口从列表页跳过来,链接关系要改回列表页。
一、先准备一个可调试的表单页面
为什么需要:添加患者要先有表单页面,才能谈“获取表单数据”。转写稿里先创建分包页面,再把入口链接临时从列表页改到添加页,目的是方便调试。
怎么想:正常流程是列表页 → 添加页,但开发时直接进添加页更快。创建分包页面时不要依赖 HBuilderX 的“新建页面”自动注册,因为它可能把页面注册到主包 pages 下,造成分包配置混乱。转写稿里的“HB there”指 HBuilderX,“点view”指 .vue 文件。
怎么做:右键新建文件夹,再在文件夹里新建 .vue 页面;入口链接先临时指向添加页。静态模板粘贴到 form/index.vue,样式文件改成 scss 并引入。
易错点:新建页面时误勾“在 pages.json 中注册”;页面名与目录名混用;忘记引入样式导致表单排版不对。
二、普通表单项用 v-model 获取数据
为什么需要:姓名、身份证号等数据必须进入 formData,后续验证和提交才有数据源。
怎么想:转写稿明确用 v-model 把输入框和 formData 字段绑定。先定义 formData,再让每个表单项绑定对应字段。
怎么做:
html
<input v-model="formData.name" />
<input v-model="formData.idCard" />js
const formData = reactive({
name: '',
idCard: '',
gender: '',
defaultFlag: 0
})调试时在页面里随便填姓名、身份证号,手动刷新后看数据是否变化。转写稿中曾把 idCard 写成 idcard,测试发现取不到值,改正后才正常。
易错点:字段名拼错;v-model 写成 v-mode;只写输入框不写 formData 初始字段。
三、uni-data-checkbox 为什么能 v-model
为什么需要:性别是单选,原生单选/多选在小程序里取数据麻烦,而 uni-data-checkbox 封装后支持 v-model。
怎么想:转写稿解释,小程序没有真正意义上的双向绑定,原生表单取值不方便,所以用 uni-data-checkbox 简化。它的选项通过 localdata 配置,选中颜色也可自定义。
怎么做:
html
<uni-data-checkbox v-model="formData.gender" :localdata="genderOptions" />测试时切换男/女,观察 formData.gender 是否变为 0/1。
易错点:把 localdata 写成 localData;忘记给 formData.gender 初始值;误以为原生 radio 也能直接 v-model。
四、switch 用 change 事件取布尔值并转 0/1
为什么需要:默认就诊人开关是原生 switch,不支持 v-model,必须通过事件拿值。
怎么想:转写稿先打印事件对象,发现值在 event.detail.value,是 true/false;但接口要求 number 的 0/1,所以用三元转换。
怎么做:
html
<switch :checked="formData.defaultFlag === 1" @change="onDefaultChange" />js
function onDefaultChange(e) {
formData.defaultFlag = e.detail.value ? 1 : 0
}调试时分别打开、关闭开关,看控制台 detail.value 是否为 true/false。
易错点:写成 @change="onDefaultChange()" 导致事件对象丢失;忘记把布尔值转 0/1;checked 前漏写冒号。
五、把数据交给 uni-forms,并注意验证返回值
为什么需要:uni-forms 验证需要 :model 提供数据,:rules 提供规则,validate() 触发验证。
怎么想:转写稿强调三步:取数据 → :model 传给组件 → 定义 rules → 调 validate。验证成功后返回的是“被验证字段”的数据,默认就诊人没写规则,就不会出现在返回值里,所以提交时改用 formData 本身。
怎么做:
html
<uni-forms ref="formRef" :model="formData" :rules="rules">js
try {
await formRef.value.validate()
await addPatientApi(formData)
uni.navigateBack()
} catch (err) {
// 处理验证或接口错误
}易错点:rules 里的 name 与 uni-forms-item 的 name 不一致;validate 拼错;把验证返回值当成完整表单数据。
先判断:switch 为什么不能直接 v-model
为什么需要:本页既有 uni-data-checkbox,也有原生 switch。前者是 uni-app 封装组件,支持 v-model,所以性别能直接绑定;switch 是原生组件,转写稿明确说它“根本就不支持 v-model”。如果照搬 v-model,数据不会按预期更新。
怎么想:遇到表单组件先判断它是否支持双向绑定。支持就用 v-model;不支持就监听组件事件,从事件对象里取值。
怎么做:switch 用 @change 监听切换,不要写 v-model。同页的 uni-data-checkbox 可用 v-model 绑定性别,localdata 配选项,选中颜色可按 UI 自定义。
易错点:不要以为“表单组件都能 v-model”。uni-data-checkbox 能,是因为 uni-app 做了封装;原生 switch 不能。拼写也别把 change 写成 chan。
用 change 事件读取 switch 的值
为什么需要:switch 的开关状态不在 v-model 里,必须通过事件拿。转写稿的调试方式是先打印事件对象,再找值。
怎么想:切换开关会触发 change,回调参数是事件对象。关键值在 event.detail.value,它是布尔值:打开为 true,关闭为 false。
怎么做:先写监听并打印,打开、关闭各试一次:
html
<switch @change="onSwitchChange" />js
onSwitchChange(ev) {
console.log(ev)
console.log(ev.detail.value)
}控制台展开后确认:detail.value 为 true 表示选中,false 表示未选中。
易错点:不要取 ev.value,转写稿明确是 ev.detail.value;也不要只看一次,必须分别测试打开和关闭,否则容易把布尔值方向写反。
布尔值要转成后端需要的 0/1
为什么需要:switch 给的是 true/false,但转写稿里后端接收“默认就诊人”要求 number,并且是 0 和 1。直接传布尔值会导致参数类型不符合接口要求。
怎么想:事件里拿到布尔值后立刻转换,不要等到提交时再补。打开转 1,关闭转 0。
怎么做:
js
onSwitchChange(ev) {
this.formData.defaultFlag = ev.detail.value ? 1 : 0
}调试时先打印 ev.detail.value,再打印转换后的 formData.defaultFlag,确认打开为 1、关闭为 0。
易错点:三元表达式方向别写反;字段名统一用 formData,转写稿里的 formDate 是口误/笔误。还要注意后端要数字,不是字符串 "1"。
用 :checked 设置默认开关状态
为什么需要:添加患者时,“默认就诊人”可能需要初始为开或关。只靠用户手动切换不够,需要把表单数据回显到开关上。
怎么想:switch 的选中状态用 :checked 绑定一个布尔表达式。转写稿用 formData.defaultFlag:等于 1 时打开,等于 0 时关闭。
怎么做:
html
<switch
@change="onSwitchChange"
:checked="formData.defaultFlag === 1"
/>测试时把初始值设为 1,开关应默认打开;改成 0,应默认关闭。
易错点::checked 前有冒号,表示绑定表达式;不要写成普通字符串。表达式要返回布尔值,formData.defaultFlag === 1 比直接写数字更稳。
与表单验证、提交整合时的易错点
为什么需要:switch 数据虽然拿到了,但提交时容易漏。转写稿视频7发现:validate() 通过后的返回值只包含“参与验证”的字段,默认就诊人没有写规则,所以不在返回值里。
怎么想:提交数据不要只依赖验证返回值;应使用表单本身的 formData,它包含姓名、身份证号、性别、默认就诊人等完整数据。
怎么做:验证通过后,用 formData 调添加患者接口;switch 的值已在 change 中写入 formData.defaultFlag,提交时自然带上。
易错点:不要因为 validate() 成功就以为所有字段都在返回值里;未参与 rules 的 switch 字段要额外确认。调试时先打印 formData,确认 defaultFlag 是 0/1 再提交。
代码示例
1. 添加患者表单页(pages/patient/form/index.vue)——模板 + 数据获取 + 验证规则 + 提交
vue
<template>
<view class="form-page">
<uni-forms ref="formRef" :model="formData" :rules="rules" label-width="90px">
<uni-forms-item label="患者姓名" name="name">
<input v-model="formData.name" placeholder="请输入患者姓名" />
</uni-forms-item>
<uni-forms-item label="身份证号" name="idCard">
<input v-model="formData.idCard" placeholder="请输入身份证号" />
</uni-forms-item>
<uni-forms-item label="患者性别" name="gender">
<uni-data-checkbox
v-model="formData.gender"
:localdata="genderOptions"
selectedColor="#27ba9b"
/>
</uni-forms-item>
<uni-forms-item label="默认就诊人">
<switch
:checked="formData.defaultFlag === 1"
@change="onSwitchChange"
/>
</uni-forms-item>
</uni-forms>
<button type="primary" @click="onFormSubmit">保存</button>
</view>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { addPatientAPI } from '@/services/patient'
// 表单组件实例,用来调 validate 方法
const formRef = ref()
// 表单数据:name / idCard / gender / defaultFlag
const formData = reactive({
name: '',
idCard: '',
gender: 1,
defaultFlag: 0
})
// uni-data-checkbox 的选项数据(localdata)
const genderOptions = [
{ text: '男', value: 1 },
{ text: '女', value: 0 }
]
// switch 没有 v-model,只能监听 change,从事件对象里取值
const onSwitchChange = (e) => {
// 接口要求 0/1 的数字,而不是 true/false
formData.defaultFlag = e.detail.value ? 1 : 0
}
// 自定义校验:身份证第 17 位(索引 16)奇数为男、偶数为女
const validateGender = (rule, value, data, callback) => {
const genderByIdCard = Number(data.idCard.slice(16, 17)) % 2 === 0 ? 0 : 1
if (genderByIdCard !== value) {
callback('与身份证号中的性别不一致')
} else {
callback()
}
}
const rules = {
name: {
rules: [
{ required: true, errorMessage: '请输入患者姓名' },
{ pattern: /^[\u4e00-\u9fa5]{2,5}$/, errorMessage: '姓名须为2-5位中文' }
]
},
idCard: {
rules: [
{ required: true, errorMessage: '请输入身份证号' },
{
pattern: /^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$/,
errorMessage: '身份证号格式不正确'
}
]
},
gender: {
rules: [
{ required: true, errorMessage: '请选择性别' },
{ validateFunction: validateGender }
]
}
// defaultFlag 不是必填项,不写规则
}
const onFormSubmit = async () => {
try {
// 校验通过才会往下走
await formRef.value.validate()
// 注意:用 formData 本身,不要用 validate() 的返回值
const res = await addPatientAPI(formData)
if (res.code !== 10000) {
uni.showToast({ title: res.message, icon: 'none' })
return
}
// 添加成功,返回上一页(患者列表页)
uni.navigateBack()
} catch (err) {
console.log('表单校验失败', err)
}
}
</script>要点:
- 表单验证三件套:
:model="formData"把数据交给uni-forms;:rules="rules"定义规则;formRef.value.validate()触发校验,缺一不可。 uni-data-checkbox支持v-model,所以性别直接v-model="formData.gender";原生switch不支持v-model,必须@change拿e.detail.value。switch的e.detail.value是布尔值,而接口要0/1数字,所以用三元转一次;回显用:checked="formData.defaultFlag === 1"。- 每个
uni-forms-item的name必须和rules里的键、formData里的字段名三者一致,否则规则不生效(最常见错误)。 validateFunction要和普通规则一样放进对应字段的rules数组里,不能平级写到rules对象下;函数必须调用callback()(通过)或callback('提示文字')(不通过)。- 提交时传
formData而不是validate()的返回值:validate()只会返回“被验证过的字段”,defaultFlag没写规则,就不会出现在返回值里,会漏参。 - 姓名正则
[\u4e00-\u9fa5]是 Unicode 汉字编码范围,身份证第 17 位用slice(16, 17)取(索引从 0 开始)。
2. 患者相关接口封装(services/patient.js)
js
import { http } from '@/utils/http'
// 添加患者:POST 提交四个参数,成功返回新患者 id
export const addPatientAPI = (data) => {
return http({
url: '/patient/add',
method: 'POST',
data
})
}要点:
- 按业务分文件:用户相关放
user.js,患者相关新建patient.js,别全塞一个文件。 - 方法名以
API结尾,调用处一眼能看出是在发请求。 - 一定要
return,否则页面里await addPatientAPI(...)拿不到结果。 url以接口文档为准,method必须是POST,参数通过data传。
3. 错误提示样式调整(pages/patient/form/index.scss)
scss
.form-page {
padding: 30rpx;
// 覆盖 uni-forms 内置的错误提示盒子
::v-deep .uni-forms-item__error {
position: absolute;
left: -220rpx; // 左边界对齐到整行最左侧
right: 0; // 右边也贴边,相当于占满整行
top: auto;
color: #f56c6c;
font-size: 24rpx;
// transition: all 0.3s; // 注释掉即去掉淡入动画
}
}要点:
- 类名
.uni-forms-item__error是组件内部类,需要::v-deep才能穿透 scoped 样式生效。 left: -220rpx是量出来的偏移量,用来把提示文字从“标签右侧”拉到整行最左边,具体数值随 UI 稿调整。- 去掉过渡动画就把
transition那行注释掉,不是删掉别的东西。 - 这段样式是按设计稿改的,换项目要按自己的 UI 结果重新量,不是固定写法。
常见错误
- 现象:新建分包页面后被注册到主包
pages.json。→ 原因:使用 HBuilderX“新建页面”时勾选了“在配置中注册”,工具按主包页面处理。→ 修法:分包页面改为右键新建文件夹,再手动新建.vue页面文件,或取消自动注册后手动配置到对应分包。 - 现象:表单字段能输入但取不到值,或提交数据字段名不对。→ 原因:
v-model绑定字段拼写与数据对象不一致,如ID card、gender、defaultFlag等未对齐。→ 修法:逐项核对formData字段名,并用控制台即时验证每个字段。 - 现象:
switch获取到true/false,接口却要求0/1。→ 原因:直接使用原生switch的change事件值,未按后端类型转换。→ 修法:监听change,从e.detail.value取值后转换为数字1/0,并同步到表单数据。 - 现象:验证规则不生效或提示字段错位。→ 原因:
rules中字段名未与表单项name对应,或把自定义校验写到了rules数组外。→ 修法:确保每个待校验项设置正确name,并把自定义校验对象放回rules数组内。 - 现象:自定义校验函数报错或不提示错误。→ 原因:
validateFunction未return校验结果,或失败时未调用callback传入提示语。→ 修法:通过返回布尔值控制通过/失败,失败时用callback('提示信息')明确提示。 - 现象:提交时表单数据缺少“默认就诊人”。→ 原因:直接使用
validate()的返回值,而该返回值只包含参与校验的字段。→ 修法:提交时使用表单组件当前完整数据或原formData,不要误以为验证返回值等于完整表单数据。 - 现象:接口封装后页面调用拿不到返回值。→ 原因:封装的 API 方法内部发起请求但未
return。→ 修法:在 API 方法中return http(...),页面侧再await接收结果并判断code。
练习
- 在一个分包中手动创建
form/index.vue页面,并说明为什么不使用“新建页面”的自动注册;完成后检查pages.json中该页面是否只出现在分包配置内。 - 完成添加患者表单四个核心数据的获取:姓名、身份证号、性别、默认就诊人;要求控制台分别验证
switch切换前后得到0/1,而不是true/false。 - 为姓名、身份证号、性别配置验证规则,并补一个自定义校验:身份证号第 17 位表示的性别必须与所选性别一致;分别用男、女测试数据验证错误提示。
- 封装“添加患者”API,并在保存按钮中完成:表单验证 → 提交完整表单数据 → 判断
code→ 成功后返回上一页;要求接口方法必须return请求结果。 - 排查一个模拟问题:提交时数据缺少
defaultFlag,请定位是验证返回值导致还是switch数据未同步导致,并写出修复步骤。
课堂画面






原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 优医咨询-添加患者模板 | 05:25 |
| 2 | 优医咨询-获取表单数据 | 05:07 |
| 3 | 优医咨询-switch数据获取 | 08:55 |
| 4 | 优医咨询-表单验证规则 | 07:28 |
| 5 | 优医咨询-调用验证方法 | 06:37 |
| 6 | 优医咨询-验证身份证号中的性别 | 11:10 |
| 7 | 优医咨询-添加患者 | 11:56 |
小结
- 分包页面创建方式要明确:自动新建页面容易误注册到主包,手动建文件夹和页面更可控。
- 添加患者功能的主线是:先做添加页,再联调列表;调试阶段可临时改跳转入口。
- 表单验证仍按“数据绑定 → 规则定义 → 调用验证”推进,但字段
name对齐是前提。 uni-data-checkbox的价值在于配合v-model简化选择数据获取;原生switch则需监听事件并手动同步。- 自定义校验适合处理组合关系,如身份证性别与所选性别一致性;规则位置和回调写法必须正确。
- 提交数据应使用完整表单数据,不能把
validate()返回值直接等同于全部业务字段。 - 接口封装要返回请求结果,页面侧要判断业务
code,成功后按产品流程返回列表页。 - 错误提示样式需按 UI 稿调整;理解类名覆盖、定位和过渡效果的作用即可,不必死记固定样式。