主题
首页模板布局(三)
本页对应 首页模板布局,共 7 段视频,约 1 小时 1 分。
学习目标
- 能够独立封装患者列表查询 API,并在
onShow生命周期中调用,将返回数据渲染到患者列表,同时根据列表数量条件控制“添加患者”按钮显示。 - 能够实现患者侧滑删除:同时获取患者 ID 与索引值,调用删除接口移除后端数据,并使用
splice同步移除前端列表数据。 - 能够根据路由参数中的患者 ID 查询患者详情,将数据回显到表单,并通过
setNavigationBarTitle区分“添加患者”和“编辑患者”标题。 - 能够封装更新患者 API,并在提交表单时根据是否存在患者 ID 正确区分调用新增或更新逻辑,完成数据提交与页面返回。
- 能够通过增加页面显示控制变量,在患者列表数据请求完成后再显示页面内容,避免加载过程中按钮闪现导致的页面抖动。
- 能够复用或创建 Tabs、Sticky 组件,并完成首页模板结构与样式布局,使首页组件正常显示和交互。
知识点
先改入口:从家庭档案跳到患者列表
为什么需要:添加患者完成后,要能马上在列表里看到结果;如果入口还跳旧页面,调试和验收都不方便。
怎么想:先把“家庭档案”的点击目标改成 list,让添加页和列表页形成闭环,再在列表页拉数据。
怎么做:把家庭档案链接的跳转地址改为列表页;按讲义把患者列表模板放到 list 下,并新建 index.scss,把列表样式粘进去。转写稿里反复强调“写一部分就测一下”,所以先看页面结构和样式是否正常。
易错点:没有发生页面跳转时,返回上一页可能没有“上一步”;样式文件必须是 SCSS,模板和样式要放在对应分包目录下。
封装患者列表接口,并在 onShow 中调用
为什么需要:列表数据来自后端,不能写死;封装接口能让页面逻辑更干净。
怎么想:先看接口文档:GET 请求,登录后即可,不需要额外参数,返回姓名、身份证号、默认标识、性别、年龄等。调用步骤就是“封装 API → 页面调用 → 拿结果渲染”。
怎么做:在患者 API 文件中封装,路径以接口文档为准:
js
export const patientListAPI = () => http.get('患者列表接口路径')页面里:
js
import { ref } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { patientListAPI } from '@/api/patient'
const patientList = ref([])
const getPatientList = async () => {
const { code, data, message } = await patientListAPI()
if (code !== 10000) {
return uni.showToast({ title: message, icon: 'none' })
}
patientList.value = data
}
onShow(() => { getPatientList() })转写稿里特意对比生命周期:onLoad 只调一次,onShow 会来回调;从添加页返回列表时要刷新,所以用 onShow。
易错点:onShow 不是 Vue 的,要从 @dcloudio/uni-app 引入;接口返回要先判 code,再赋值。
渲染列表:默认标识、性别、年龄、按钮隐藏
为什么需要:数据拿到了要按业务展示;默认患者要高亮并显示“默认”,超过 6 条不能再添加。
怎么想:defaultFlag === 1 时加 active 类并显示“默认”;性别直接用后端给的 genderValue;年龄返回数字,模板里补“岁”。
怎么做:模板按讲义组件结构写,核心是:
html
<swipe-action-item v-for="(item, index) in patientList" :key="item.id">
<text :class="item.defaultFlag === 1 ? 'active' : ''">{{ item.name }}</text>
<text v-if="item.defaultFlag === 1">默认</text>
<text>{{ item.genderValue }}</text>
<text>{{ item.age }}岁</text>
<view @click="onDelete(item.id, index)">删除</view>
</swipe-action-item>
<button v-if="patientList.length < 6">添加患者</button>易错点:defaultFlag 条件要用两次;age 只返回数字,别把“岁”删掉;按钮显示条件按“少于 6 条”判断,达到 6 条隐藏。
删除患者:后端删 ID,前端删索引
为什么需要:删除要同时处理数据库和当前页面数组,否则页面会残留。
怎么想:后端根据 id 删除;前端数组根据索引用 splice 删除。所以点击侧滑删除时,必须同时拿到 id 和 index。
怎么做:先打印测试 id、index 是否能拿到,再封装删除 API:
js
export const removePatientAPI = (id) => http.delete(`删除接口路径/${id}`)页面:
js
const onDelete = async (id, index) => {
const { code, message } = await removePatientAPI(id)
if (code !== 10000) {
return uni.showToast({ title: message, icon: 'none' })
}
patientList.value.splice(index, 1)
}易错点:splice 不是 surprise;删除成功后再删前端数据;索引是当前项索引,传参别传错。
编辑回显、标题修改与防抖动
为什么需要:编辑页和添加页共用表单;有 ID 才是编辑,要回显数据并改标题。
怎么想:先判断 ID:没有 ID 直接 return,不请求、不改标题;有 ID 才请求详情。详情里 genderValue 和 age 不回填,其余用剩余参数赋给 formData。
怎么做:
js
if (!id) return
uni.setNavigationBarTitle({ title: '编辑患者' })
const { code, data, message } = await getPatientDetailAPI(id)
if (code !== 10000) {
return uni.showToast({ title: message, icon: 'none' })
}
const { genderValue, age, ...rest } = data
formData.value = rest转写稿调试时先报 HTTP 拼写错误,又发现没传 ID,修正后才请求成功。防抖动则加 showPage,数据请求完再显示:
js
const showPage = ref(false)
onShow(async () => { await getPatientList(); showPage.value = true })最外层 v-if="showPage",避免先闪“添加患者”按钮。
易错点:添加时不要请求详情;setNavigationBarTitle 是 uni API;更新接口用 PUT,参数要带 ID,成功后再 navigateBack。
首页模板布局铺垫
为什么需要:患者增删改查完成后,首页要复用基础阶段封装过的标签页和吸顶组件。
怎么想:直接拿现成组件,减少重复开发;先保证结构和样式能跑,再接接口。
怎么做:新建 customer-tabs、sticky 等同名组件目录,粘模板;首页替换练习代码并引入组件;缺样式就补 index.scss。
易错点:组件新增后项目可能要重启才生效;样式漏拷会显示异常,按讲义或仓库补全。
本节要完成什么
这一节的核心不是“点一下删除按钮”这么简单,而是要把删除做成一个完整闭环:用户在患者列表里侧滑,点击删除;前端拿到患者 id 和数组索引 index;调用后端删除接口,让数据库删掉对应记录;接口成功后再删除前端 patientList 中的那一条数据。转写稿里反复强调:删除要分两个地方做——后端数据库根据 id 删除,前端数组根据索引 splice 删除。只删一边,页面和数据库就会不一致。
为什么删除要分前后端
先想清楚数据在哪里。列表页展示的数据来自接口,保存在 patientList 这样的响应式数组里;数据库里的数据则由后端维护。用户点删除时,如果只调接口删数据库,前端数组还在,页面看起来“没删掉”;如果只删前端数组,刷新后数据又回来了。所以正确思路是:先请求后端,后端返回成功后再删前端数组。后端认 id,前端数组认索引,因此点击事件里必须同时把 id 和 index 传出来。
找到删除入口:侧滑组件的事件
侧滑删除按钮的文字和背景色在组件配置里设置,前面已经讲过。这里关键是监听点击事件。转写稿提醒:事件要加在侧滑项/action item 这个组件上,不是随便加到页面外层。模板里遍历患者列表时,要拿到每一项和索引,例如:
html
<swipe-action-item
v-for="(item, index) in patientList"
:key="item.id"
@click="onDelete(item.id, index)"
>
<!-- 患者信息 -->
</swipe-action-item>这里 item.id 给后端删除用,index 给前端 splice 用。写完后先不要急着写完整逻辑,先 console.log(patientId, index) 测一下,确认点击侧滑删除时两个值都能拿到。转写稿的调试过程就是先打印,看到 id 和索引都正常,再继续封装接口。
封装删除接口
删除接口按文档要求用 delete 方式请求,id 放在路径上,不再额外传其他参数。可以按转写稿整理成类似结构,路径以接口文档为准:
js
export const removePatientAPI = (id) => {
return http.delete(`接口文档中的删除路径/${id}`)
}这里易错点有两个:一是请求方法写成 http.delete,不要写成 http.get;二是 id 在路径里,要用模板字符串拼进去。转写稿里还出现过 HTTT、deelete 这类口误/同音问题,实际代码要写正确。
点击删除的处理流程
在页面中引入封装好的 removePatientAPI,写删除回调:
js
async function onDelete(patientId, index) {
console.log(patientId, index)
const res = await removePatientAPI(patientId)
if (res.code !== 10000) {
uni.showToast({
title: res.message,
icon: 'none'
})
return
}
patientList.value.splice(index, 1)
}逻辑是:调用接口 → 判断 code 是否成功 → 失败提示 message 并 return → 成功则 splice(index, 1) 删除前端数组中的一条。splice 不是 supprise,第二个参数 1 表示删除一个单元,不能漏。删除成功后,列表长度变化也会影响“添加按钮”的显隐条件,这是列表渲染逻辑自然带来的结果。
调试与易错点
调试时先打开网络面板,清空旧请求,再点侧滑删除。正常应看到一条删除请求;请求成功后,本地数组立刻少一条,页面同步更新;再刷新页面,数据库里那条也不在了。若页面没变化,先看 code 判断是否写反,再看 splice 是否执行;若刷新后数据还在,说明后端请求没成功或 id 没传对。常见错误:事件没加在侧滑项上;只传了 id 没传索引;只删后端没删前端;splice 第二个参数写错;失败时忘记 return 导致继续删前端。按“先打印、再请求、后删数组、最后刷新验证”的顺序排查,最稳。
这一节把“患者列表获取与渲染、删除、编辑回显、更新、首页模板布局”串起来。下面按实现顺序展开,代码只保留关键片段,接口地址按接口文档替换。
患者列表:onShow 拉取并渲染
为什么需要:从添加页返回列表时,要立刻看到新患者,不能只靠 onLoad 只执行一次。怎么想:列表数据要调接口,接口封装成方法;页面每次显示都可能需要刷新,所以用 onShow。怎么做:在 patient.js 中封装列表 API,在 onShow 中调用并赋值给 patientList;渲染时用 v-for,默认患者用 defaultFlag === 1 控制 active 类名和“默认”文字,性别用后端返回的 genderValue,年龄显示 age。少于 6 条时才显示添加按钮。易错点:onShow 不是 Vue 生命周期,要从 @dcloudio/uni-app 导入;http 不要拼成 HTTT;按钮条件写成 <= 6 会多显示一条。
js
import { onShow } from '@dcloudio/uni-app'
const res = await patientListApi()
if (res.code !== 1) return uni.showToast({ title: res.message })
patientList.value = res.data删除患者:后端按 ID,前端按索引
为什么需要:侧滑删除既要从数据库删掉,也要让当前页面列表立刻少一条。怎么想:后端删除需要患者 id,前端删除数组元素需要 index,所以点击删除时要同时拿到两者。怎么做:给侧滑操作项绑定点击事件,console.log(id, index) 确认参数;调删除 API 成功后执行 patientList.value.splice(index, 1)。调试时清空网络面板记录,点删除后确认请求发出且列表少一条,刷新后该数据也不在。易错点:只删后端不删前端,页面不更新;只删前端不删后端,刷新又回来;splice 第二个参数是删除个数,别漏写 1。
js
const res = await removePatientApi(id)
if (res.code !== 1) return uni.showToast({ title: res.message })
patientList.value.splice(index, 1)编辑回显:按 ID 查详情,过滤无关字段
为什么需要:编辑页要先把患者信息回填到表单,再在表单上修改。怎么想:有 props.id 才查详情;返回数据中 genderValue 不需要回填,性别靠选中状态区分,age 由身份证计算且不展示,应过滤掉。怎么做:封装详情 API,解构出 genderValue、age,其余用 ...rest 接收并赋给 formData.value。标题也要区分:无 ID 是添加,直接 return,不请求、不改标题;有 ID 才设置“编辑患者”。调试时第一次请求报错,检查发现 http 拼写和 ID 未传,修正后详情返回,回显成功。易错点:无 ID 仍发请求;把 genderValue、age 塞进表单;标题写死成“添加患者”。
js
if (!props.id) return
uni.setNavigationBarTitle({ title: '编辑患者' })
const res = await getPatientDetailApi(props.id)
const { genderValue, age, ...rest } = res.data
formData.value = rest列表防抖动:数据回来再显示页面
为什么需要:刷新时数据还没回来,添加按钮会先闪一下,够 6 条时又隐藏,体验不好。怎么想:加一个控制页面显示的布尔变量(转写中称“配置售”,可记作 pageShow),初始 false,最外层盒子 v-if,请求成功后再置 true。怎么做:请求成功后设置 pageShow.value = true。易错点:初始值写成 true 没效果;忘记在成功分支置 true 会白屏;失败分支也要考虑提示,避免一直空白。
html
<view v-if="pageShow">...</view>js
pageShow.value = true更新患者:PUT 带 ID,区分添加与更新
为什么需要:编辑保存要更新原数据,添加保存要新增数据,两者不能混。怎么想:更新接口用 PUT,参数比添加多一个 ID;把添加逻辑抽成 addPatient,更新逻辑抽成 updatePatient,提交时按 props.id 判断。怎么做:更新 API 传 id 和 formData.value;请求成功后把 uni.navigateBack() 放在成功分支里。调试时清空网络记录,改名字保存,看到 update 请求成功且列表数据更新;再换身份证测试添加。易错点:更新不带 ID;navigateBack 写在请求前;添加和更新逻辑混在一起。
js
if (props.id) await updatePatientApi(props.id, formData.value)
else await addPatientApi(formData.value)
uni.navigateBack()首页模板:创建组件、重启、补样式
为什么需要:后续首页要用标签页切换和吸顶交互,先把模板和组件搭好。怎么想:custom-tabs、sticky 作为全局组件创建,首页 index 引入;组件和样式可从讲义或仓库取。怎么做:新建同名目录组件,粘贴模板;项目重启让组件生效;首页替换练习代码,补 index.scss,隐藏头部。易错点:组件不生效未重启;样式漏拷导致页面缺样式;旧练习代码没清掉。
代码示例
一、接口封装 api/patient.js
js
// api/patient.js
import http from '@/utils/http.js'
// 1. 患者列表:GET 请求,只需登录态,不需要额外参数
export const patientListAPI = () => http.get('/patient/list')
// 2. 删除患者:DELETE 请求,id 拼在路径上(不是 query,也不是 body)
export const removePatientAPI = (id) => http.delete(`/patient/${id}`)
// 3. 患者详情:GET 请求,路径带 id
export const getPatientDetailAPI = (id) => http.get(`/patient/detail/${id}`)
// 4. 更新患者:PUT 请求,id 和表单数据一起放在请求体里
export const updatePatientAPI = (data) => http.put('/patient/update', data)要点解释 / 易错点
- 所有接口统一“先封装成方法、再在页面里调用”,页面里只关心
await出来的结果,不直接写uni.request。 removePatientAPI的 id 是拼在路径上的,所以必须用反引号 + 模板字符串`${id}`,写成普通引号就会把 id 当字面量发出去。updatePatientAPI的 id 是放在请求体 data 里的,和删除不一样,别混。- 每个方法都要
return,漏掉return的话页面里await拿到的是undefined,解构code会直接报错。
二、患者列表页 pages/patient/list.vue
vue
<template>
<!-- showContent 初始为 false,数据拿到后再显示,避免“先闪出添加按钮”的抖动 -->
<view class="patient-list" v-if="showContent">
<uni-swipe-action>
<uni-swipe-action-item
v-for="(patient, index) in patientList"
:key="patient.id"
:right-options="options"
@click="onDelete(patient.id, index)"
>
<view class="patient-item" @click="toEdit(patient.id)">
<view class="info">
<view class="name">
{{ patient.name }}
<!-- 默认患者才显示高亮的“默认”标签 -->
<text class="active" v-if="patient.defaultFlag === 1">默认</text>
</view>
<view class="id-card">{{ patient.idCard }}</view>
</view>
<view class="extra">
<text>{{ patient.genderValue }}</text>
<text class="age">{{ patient.age }}岁</text>
</view>
</view>
</uni-swipe-action-item>
</uni-swipe-action>
<!-- 满 6 条就隐藏添加按钮,不允许再添加 -->
<view class="add-btn" v-if="patientList.length < 6" @click="toAdd">
添加患者
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { patientListAPI, removePatientAPI } from '@/api/patient.js'
const patientList = ref([]) // 患者列表数据
const showContent = ref(false) // 页面是否渲染(防止抖动)
const options = [{ text: '删除', style: { backgroundColor: '#f56c6c' } }]
// 获取患者列表
const getPatientList = async () => {
const { code, data, message } = await patientListAPI()
if (code !== 10000) {
return uni.showToast({ title: message, icon: 'none' })
}
patientList.value = data
showContent.value = true // 数据到位了再显示页面
}
// 从添加/编辑页返回时要能看到最新数据,所以用 onShow
onShow(() => {
getPatientList()
})
// 侧滑删除:id 给后端,index 给前端
const onDelete = async (id, index) => {
const { code, message } = await removePatientAPI(id)
if (code !== 10000) {
return uni.showToast({ title: message, icon: 'none' })
}
// 后端删成功,前端数组也要同步删掉
patientList.value.splice(index, 1)
}
const toEdit = (id) => {
uni.navigateTo({ url: `/pages/patient/form?id=${id}` })
}
const toAdd = () => {
uni.navigateTo({ url: '/pages/patient/form' })
}
</script>
<style lang="scss">
.patient-list {
padding: 20rpx;
.patient-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx;
background: #fff;
border-bottom: 1rpx solid #eee;
.active {
color: #2fa7ff;
margin-left: 10rpx;
}
}
.add-btn {
margin-top: 30rpx;
height: 88rpx;
line-height: 88rpx;
text-align: center;
color: #2fa7ff;
border: 1rpx solid #2fa7ff;
border-radius: 44rpx;
}
}
</style>要点解释 / 易错点
onShow必须从@dcloudio/uni-app引入,Vue 里没有onShow。onLoad只执行一次,从表单页返回不会重新执行,只有onShow每次显示都会触发,这是“添加完返回列表能看到新数据”的关键。- 删除要删两处:后端数据库靠
id删,前端数组靠index删,所以侧滑事件里两个参数都要传。 splice(index, 1)第二个参数是“删除几个”,漏写会把后面所有数据全删掉。v-for的:key用patient.id,不要用 index。- 添加按钮的
v-if必须绑在patientList.length上,数据没请求回来时长度是 0,按钮会先闪出来再消失 —— 这就是showContent存在的意义,初始false、请求成功后置true。 - 性别直接用后端返回的
genderValue,年龄直接用age,前端不需要自己算;defaultFlag === 1才加高亮类名和“默认”文字。
三、编辑回显 + 提交 pages/patient/form.vue
vue
<template>
<view class="form">
<view class="item">
<text>姓名</text>
<input v-model="formData.name" placeholder="请输入姓名" />
</view>
<view class="item">
<text>身份证号</text>
<input v-model="formData.idCard" placeholder="请输入身份证号" />
</view>
<button class="save" @click="submit">保存</button>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import {
getPatientDetailAPI,
addPatientAPI,
updatePatientAPI
} from '@/api/patient.js'
const patientId = ref('') // 有 id = 编辑,无 id = 添加
const formData = ref({
name: '',
idCard: '',
gender: 0
})
// 获取患者详情并回显
const getPatientDetail = async () => {
// 没有 id 说明是添加操作:不改标题、不发请求、不回显
if (!patientId.value) return
const { code, data, message } = await getPatientDetailAPI(patientId.value)
if (code !== 10000) {
return uni.showToast({ title: message, icon: 'none' })
}
// genderValue 靠选中状态区分、age 由身份证号推算,这两个不需要回填
const { genderValue, age, ...rest } = data
formData.value = rest
// 动态修改导航栏标题
uni.setNavigationBarTitle({ title: '编辑患者' })
}
onLoad((options) => {
patientId.value = options.id || ''
getPatientDetail()
})
// 添加患者
const addPatient = async () => {
const { code, message } = await addPatientAPI(formData.value)
if (code !== 10000) {
return uni.showToast({ title: message, icon: 'none' })
}
uni.navigateBack() // 必须在请求成功之后再返回
}
// 更新患者
const updatePatient = async () => {
const { code, message } = await updatePatientAPI({
id: patientId.value,
...formData.value
})
if (code !== 10000) {
return uni.showToast({ title: message, icon: 'none' })
}
uni.navigateBack()
}
// 根据 id 区分:有 id 走更新,没有走添加
const submit = () => {
patientId.value ? updatePatient() : addPatient()
}
</script>
<style lang="scss">
.form {
padding: 20rpx;
.item {
display: flex;
align-items: center;
padding: 30rpx;
background: #fff;
margin-bottom: 20rpx;
text { width: 160rpx; }
}
.save {
margin-top: 40rpx;
background: #2fa7ff;
color: #fff;
}
}
</style>要点解释 / 易错点
- 页面是“添加 / 编辑”共用的,靠
onLoad里的options.id判断:有 id 才发详情请求、才改标题,没 id 直接return,这样添加时页面干净、不会白发一次请求。 - 回显要过滤字段:
const { genderValue, age, ...rest } = data,把不要的两个解构出来,rest就是真正要赋给formData的数据(性别看选中状态、年龄由后端按身份证号推算,都不需要回填)。 uni.setNavigationBarTitle参数是对象{ title: '编辑患者' },直接传字符串不生效。- 更新接口要带
id:updatePatientAPI({ id: patientId.value, ...formData.value }),漏了 id 后端不知道改哪条。 uni.navigateBack()必须写在请求成功的分支里;写在请求外面会出现“还没保存成功就跳走”的问题。- 提交时用三元
patientId.value ? updatePatient() : addPatient()复用同一套表单逻辑,添加和修改共用formData。 - 封装 http 方法时注意拼写(
http不是httt),路径写错、id 忘传是这一节最常见的两个低级错误。
常见错误
1. onShow 从 vue 里引入
- 现象:
import { onShow } from 'vue'报 undefined,或写了 onShow 但页面数据永远不刷新;从添加页返回列表页看不到新数据。 - 原因:onShow 是小程序页面生命周期,不属于 Vue 的 Composition API。
- 修法:改为
import { onShow } from '@dcloudio/uni-app';列表页的取数逻辑写在 onShow 里而不是 setup 顶层或 onLoad 里。
2. 删除只做了一半
- 现象:点了侧滑删除,接口 200,但当前页面上那条数据还在;或者页面上没了,刷新一下又回来。
- 原因:只调了删除接口(删库)或只做了
splice(删本地数组),漏掉另一半。 - 修法:接口成功后,再执行
patientList.value.splice(index, 1);并且删除按钮的点击回调必须同时拿到id(给后端)和index(给前端),两个参数一起传。
3. 删除接口把 id 拼进路径写错
- 现象:请求地址里出现字面量
${id},或 404 / 请求到了错误的资源。 - 原因:用了单引号或普通字符串拼接,模板字符串没生效。
- 修法:路径用反引号
http.delete(`/xxx/${id}`);同时在网络面板确认实际请求 URL 里是真实数字。
4. 回显时把 genderValue、age 一起塞进 formData
- 现象:编辑页提交后,后端返回的年龄被前端传的旧值覆盖;或表单里多出用不到的字段,校验/提交数据不干净。
- 原因:把详情接口返回的整个对象直接赋给了
formData,没有排除派生字段。 - 修法:解构排除后再赋值,如
const { genderValue, age, ...rest } = data; formData.value = rest;;age 由身份证号推导、性别由选中态决定,两者都不回填。
5. 添加与编辑共用表单页却没做 id 判断
- 现象:从"添加患者"进来也发了一次详情请求(或报错),标题还显示"添加患者";从列表编辑进来标题没变。
- 原因:
props.id有无未做分支,详情请求、setNavigationBarTitle都无条件执行。 - 修法:在取详情的方法开头判断,无 id 直接
return(不回显、不发请求、不改标题);有 id 时才改标题为"编辑患者"并发请求。
6. 列表首屏抖动
- 现象:刷新时先闪一下"添加患者"按钮,数据到了按钮才消失。
- 原因:
v-if="patientList.length < 5"在初始空数组时立即为 true,先渲染出按钮。 - 修法:加
showPage变量,初始 false,绑到最外层容器的v-if;接口成功后再置 true,让"未加载"和"已加载"两态互斥。
7. 封装 API 忘记 return
- 现象:
await getPatientList()拿到 undefined,解构 code/data 时报"Cannot destructure property"。 - 原因:函数体里调了
http.get(...)但没有 return。 - 修法:
return http.get(url),并保证export后在页面里 import 名字与导出名一致。
8. 新增/更新代码没有抽方法,导致两套逻辑缠在一起
- 现象:提交时既走添加又走更新,或
navigateBack写在请求回调外面,跳转早于保存完成。 - 原因:添加逻辑直接堆在提交回调里,后来加更新时靠三元硬拼;跳转语句位置不对。
- 修法:拆成
addPatient()和updatePatient()两个函数,提交回调里用props.id ? updatePatient() : addPatient()分发;navigateBack放进各自接口成功分支内。
练习
练习 1(列表首屏防抖) 给患者列表页加 showPage,实现:数据未回来时整页空白(不显示"添加患者"按钮),数据回来后再整页显示。验收:把网络调成 Slow 3G 反复刷新,看不到按钮闪现;把数据删到 6 条以上时按钮依然不出现。
练习 2(删除闭环 + 边界) 实现侧滑删除,要求:接口失败时提示 message 且本地数组不变;接口成功时 splice 掉对应项;删除后若条数从 6 降到 5,"添加患者"按钮自动重新出现。验收:删一条后不刷新页面按钮即出现,刷新后该条确实不在。
练习 3(编辑回显字段过滤) 从列表点进编辑页,把详情接口返回的数据回显到表单,但 genderValue、age 不得进入 formData。验收:在提交前的 console.log(formData) 里只看到姓名、身份证号、性别、默认标记等可编辑字段。
练习 4(添加/编辑共用页) 在同一个表单页里实现:无 id 时标题为"添加患者"、不发详情请求;有 id 时标题改为"编辑患者"、发详情请求并回显。验收:两条入口各走一遍,网络面板里"添加"入口零请求。
练习 5(提交分流) 把提交逻辑拆成 addPatient 和 updatePatient,用 id 判断走哪一个,两者成功后都要 navigateBack。验收:编辑改完姓名保存,返回列表立刻能看到新名字(依靠 onShow 刷新);新增一条也能立刻出现。
课堂画面






原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 优医咨询-患者列表 | 16:04 |
| 2 | 优医咨询-删除患者 | 09:55 |
| 3 | 优医咨询-编辑患者-2 | 08:38 |
| 4 | 优医咨询-编辑患者-3 | 05:12 |
| 5 | 优医咨询-患者列表防止抖动 | 02:56 |
| 6 | 优医咨询-更新患者数据 | 08:27 |
| 7 | 优医咨询-首页模板布局 | 09:49 |
小结
- 增删改查这一套里,真正容易出错的不是"怎么调接口",而是"调完之后本地状态有没有同步"——删除是双删,编辑是回填后回写。
- 页面级数据刷新只认 onShow,别用 onLoad 或 setup 顶层,否则从表单页返回时列表永远是旧数据。
- 详情接口返回的字段不等于表单需要的字段,回显前必须做一次字段裁剪,派生字段(年龄、性别文案)一律不回填。
- 添加和编辑共用一个表单页是常态,判断依据就是路由参数里的 id 有没有值;判断要早做,无 id 就该直接短路,省掉请求也省掉标题错乱。
- 首屏抖动本质是"空数据"和"加载中"两种状态没区分开,一个布尔开关就能解决,属于接口联调后必做的体验收尾。
- 接口封装保持"一个动作一个方法、方法必须 return",页面里只负责调用与状态更新,逻辑才拆得开、加功能才不打架。
- 跳转类语句(navigateBack)要放在请求成功的分支里,放外面会出现"还没存完就返回"的时序 bug。
- 每写一小段就测一次(哪怕只是
v-for加个:key),比写完全部再排查成本低得多;无关文件及时关掉,减少看错文件导致的低级错误。