Skip to content

首页模板布局(三) ​

本页对应 首页模板布局,共 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 刷新);新增一条也能立刻出现。

课堂画面 ​

课堂画面 01

课堂画面 02

课堂画面 03

课堂画面 04

课堂画面 05

课堂画面 06

原视频对照 ​

#视频时长
1优医咨询-患者列表16:04
2优医咨询-删除患者09:55
3优医咨询-编辑患者-208:38
4优医咨询-编辑患者-305:12
5优医咨询-患者列表防止抖动02:56
6优医咨询-更新患者数据08:27
7优医咨询-首页模板布局09:49

小结 ​

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