uni-app(优医咨询)项目实战 - 第4天


title: uni-app(优医咨询)项目实战 - 第4天
series: uni-app(优医咨询)项目实战
abbrlink: 99b9bd2a
date: 2024-04-17 10:59:42

uni-app(优医咨询)项目实战 - 第4天

学习目标:

  • 掌握登录权限验证的实现方法
  • 能够动态设置导航栏标题
  • 能够动态设置tabBar角标文字
  • 知道验证身份证号的正则表达式
  • 掌握uni-swipe-action侧滑组件的使用方法

一、权限验证

此处的权限验证是指服务端接口验证码 token 是否存在或有效,这就需要我们在调用接口时将 token 以自定义头信息的方式发送给服务端接口,如果 token 不存在或者 token 过期了,则接口会返回状态码的值为 401。

关于权限验证的逻辑我们做如下的处理:

  1. 配置请求拦截器,读取 Pinia 中记录的 token 数据
  2. 检测接口返回的状态码是否为 401,如果是则跳转到登录页面
  3. 在登录成功后跳转回原来的页面

我们按上述的步骤分别来实现:

1.1 配置拦截器
// utils/http.js
// 导入模块
import Request from 'luch-request'
import { useUserStore } from '@/stores/user.js'

// 接口白名单
const whiteList = ['/code', '/login', '/login/password']

// 实例化网络请求
const http = new Request({
  // 接口基地址
  baseURL: 'https://consult-api.itheima.net/',
  custom: {
    loading: true,
  },
})

// 请求拦截器
http.interceptors.request.use(
  function (config) {
    // 显示加载状态提示
    if (config.custom.loading) {
      uni.showLoading({ title: '正在加载...', mask: true })
    }

    // 用户相关的数据
    const userStore = useUserStore()

    // 全局默认的头信息(方便以后扩展)
    const defaultHeader = {}
    // 判断是否存在 token 并且不在接口白单当中
    if (userStore.token && !whiteList.includes(config.url)) {
      defaultHeader.Authorization = 'Bearer ' + userStore.token
    }
		// 合并全局头信息和局部头信息(局部优先级高全局)
    config.header = {
      ...defaultHeader,
      ...config.header,
    }
    return config
  },
  function (error) {
    return Promise.reject(error)
  }
)
// 响应拦截器
http.interceptors.response.use(
	// ...
)
// 导出配置好的模网络模块
export { http }

注意事项:在组件之外调用 useXXXStore 时,为确保 pinia 实例被激活,最简单的方法就是将 useStore() 的调用放在 pinia 安 装后才会执行的函数中。

在【我的】页面中调用一个接口测试发起请求时,有没有自定义头信息 Authorization

<!-- pages/my/index.vue -->
<script setup>
  // 测试的代码,将来会被删除
  import { http } from '@/utils/http.js'
  // 调用接口
  http.get('/patient/myUser')
</script>

测试两种情况:一是登录成功后,另一种是未登录时,观察是否存在请求头 Authorization

1.2 检测状态码

调用接口后服务端检测到没有传递 token 或者 token 失效时,状态码会返回 401(后端人员与前端人员约定好的,也可以是其它的数值),在响应拦截器读取状态码。

// utils/http.js
// 导入模块
import Request from 'luch-request'
import { useUserStore } from '@/stores/user.js'
// 实例化网络请求
const http = new Request({
  // 接口基地址
  baseURL: 'https://consult-api.itheima.net/',
  custom: {
    loading: true,
  },
})
// 请求拦截器
http.interceptors.request.use(
 // ...
)
// 响应拦截器
http.interceptors.response.use(
  function ({ statusCode, data, config }) {
    // 隐藏加载状态提示
    uni.hideLoading()

    // 解构出响应主体
    return data
  },
  function (error) {
    // 隐藏加载状态提示
    uni.hideLoading()
    // 后端约定 token 过期(失效)时,状态码值为 401
    if (error.statusCode === 401) reLogin()
    return Promise.reject(error)
  }
)
// 引导用户重新登录
function reLogin() {
  // 跳转到登录页面
  uni.redirectTo({
    url: `/pages/login/index`,
  })
}
// 导出配置好的模网络模块
export { http }

在此还有一点优化的空间,就是在请求前判断是否有 token ,如果没有的则不发起请求。

1.3 重定向页面

在用户登录成功后需要跳回登录前的页面,要实现这个逻辑就要求在跳转到登录页之前读取到这个页面的路径(路由),然后在登录成功后再跳转回这个页面,分成两个步骤来实现:

  1. 获取并记录跳转登录前的页面地址

在登录页面获取到登录前的页面地址,通常有两种方式实现:一是通过 URL 参数传递,另一种是通过 Pinia 状态管理,但由于小程序中借助地址传参时存在局限性,因此我们只能选择用 Pinia 状态管理实现。

// stores/user.js
import { ref } from 'vue'
import { defineStore } from 'pinia'

export const useUserStore = defineStore(
  'user',
  () => {
    // 记录用户登录状态
    const token = ref('')
    // 记录登录成功后要路转的地址(默认值为首页)
    const redirectURL = ref('/pages/index/index')
    // 跳转地址时采用的 API 名称
    const openType = ref('switchTab')
    
    return { token, redirectURL,openType }
  },
  {
    persist: {
      // redirectURL 和 openType 也要持久化存储
      paths: ['token', 'redirectURL', 'openType'],
    },
  }
)

小程序提供了多种路由路转的 API,如 uni.switchTabuni.redirectTouni.navigateTo等,大家应该还记得 tabBar 的页面跳转时只能使用 uni.switchTab,因此登录成功后进行跳转时需要判断页面地址是否为 tabBar 页面,如果是则用 uni.switchTab 跳转,否则用 uni.redirectTo 跳转。

小程序规定 tabBar 页面最多只能有 5 个,因此我们可以事先将 tabBar 中定义好的页面路径定义在一个数组件,然后根据数组方法 includes 来判断是否为 tabBar 的页面路径。

// utils/http.js
// 导入模块
import Request from 'luch-request'
import { useUserStore } from '@/stores/user.js'

// tabBar页面路径
const tabBarList = [
  'pages/index/index',
  'pages/wiki/index',
  'pages/notify/index',
  'pages/my/index',
]

// 省略前面小节的代码...

// 引导用户重新登录
function reLogin() {
  // 动态读取当前页面的路径
  const pageStack = getCurrentPages()
  const currentPage = pageStack[pageStack.length - 1]
  // 完整的路由(包含地址中的参数)
  const redirectURL = currentPage.$page.fullPath
  // 是否为 tabBar 中定义的路径
  const openType = tabBarList.includes(currentPage.route) ? 'switchTab' : 'redirectTo'
  // 用户相关数据
  const userStore = useUserStore()

  // 将来再跳转回这个页面
  userStore.redirectURL = redirectURL
  // 页面(路由)跳转方式
  userStore.openType = openType
  // 跳转到登录页面
  uni.redirectTo({ url: `/pages/login/index` })
}

// 导出配置好的模网络模块
export { http }

注意事项:在小程序中 /pages/login/index?name=xiaoming?a=1 这种格式的页面地址(地址中出现两个 ?)在跳转时会自动的将参数过滤掉,变成 /pages/login/index?name=xiaoming,这个特点大家要记住。

  1. 登录成功后,跳回到登录前的页面

接下来在登录成功后读取 redirectURLopenType,然后跳转加这个页面(路由)

<!-- pages/login/components/mobile.vue -->
<script setup>
  import { ref } from 'vue'
  import { loginByMobileApi, verifyCodeApi } from '@/services/user'
  import { useUserStore } from '@/stores/user'
  // 用户相关的数据
  const userStore = useUserStore()

  // 省略前面小节代码...

  // 提交表单数据
  async function onFormSubmit() {
    // 判断是否勾选协议
    if (!isAgree.value) return uni.utils.toast('请先同意协议!')

    // 调用 uniForms 组件验证数据的方法
    try {
      // 验证通过后会返回表单的数据
      const formData = await formRef.value.validate()
      // 提交表单数据
      const { code, data, message } = await loginByMobileApi(formData)
      // 检测接口是否调用成功
      if (code !== 10000) return uni.utils.toast(message)

      // 持久化存储 token
      userStore.token = data.token
      // 跳转到登录前的页面
      uni[userStore.openType]({
        url: userStore.redirectURL,
      })
    } catch (error) {
      console.log(error)
    }
  }

 // 省略前面小节代码...
</script>

<template>
	...
</template>

二、我的

我的,即个人中心页面,这个页面中包含了用户的基本信息、数据统计和一些功能模块入口。

2.1 页面布局

在该页面当中使用到了多色图标,并且是配合 uni-list 组件来使用的,还要注意的是这个页面使用的是自定义导航栏。

{
  "pages": [
    {
      "path": "pages/my/index",
      "style": {
        "navigationBarTitleText": "我的",
        "enablePullDownRefresh": false,
        "navigationStyle": "custom"
      }
    }
  ]
}
2.1.1 scroll-page

在手机屏幕中常常要处理不同类型的屏幕,比如异形屏(浏海屏)需要处理好安全区域内容的展示,为此我们来专封装一个组件,在该组件统一进行处理,要求该组件满足:

  1. 页面可以滚动
  2. 适配安全区域
  3. 自定义底部 tabBar 边框线
  4. 支持下拉刷新和上拉加载

首先按照 easycom 规范新建组件 scroll-page

  1. 使用内置组件 scroll-view 保证页面可以滚动,并且 scroll-view 的高度为视口的高度
<!-- /components/scroll-page.vue -->
<script setup>
  // 读取页面视口的高度
  const { windowHeight } = uni.getSystemInfoSync()
</script>

<template>
  <scroll-view
    :style="{ height: windowHeight + 'px'}"
    scroll-y
  >
    <view></view>
  </scroll-view>
</template>

<style lang="scss"></style>
  1. 适配安全区域
<!-- /components/scroll-page.vue -->
<script setup>
  // 读取页面视口的高度
  const { windowHeight } = uni.getSystemInfoSync()
</script>

<template>
  <scroll-view :style="{ height: windowHeight + 'px' }" scroll-y>
    <view class="scroll-page-content">
      <slot></slot>
    </view>
  </scroll-view>
</template>

<style lang="scss">
  .scroll-page-content {
    padding-bottom: env(safe-area-inset-bottom);
  }
</style>
  1. 自定义底部 tabBar 边框线

小程序中底部 tabBar 的边框线只能定义黑色或白色,在开发中非常不实用,我们来给 scroll-page 添加底部边框线的方式来模拟实现 tabBar 边框线的效果。

<!-- /components/scroll-page.vue -->
<script setup>
  // 读取页面视口的高度
  const { windowHeight } = uni.getSystemInfoSync()

  // 自定义组件属性
  const scrollPageProps = defineProps({
    borderStyle: {
      type: [String, Boolean],
      default: false,
    },
  })
</script>

<template>
  <scroll-view
    :style="{
      height: windowHeight + 'px',
      boxSizing: 'border-box',
      borderBottom: scrollPageProps.borderStyle,
    }"
    scroll-y
  >
    <view class="scroll-page-content">
      <slot></slot>
    </view>
  </scroll-view>
</template>

<style lang="scss">
  .scroll-page-content {
    padding-bottom: env(safe-area-inset-bottom);
  }
</style>
  1. 基于内置组件 scroll-view 实现下拉刷新交互
<script setup>
  // 读取页面视口的高度
  const { windowHeight } = uni.getSystemInfoSync()

  // 自定义组件属性
  const scrollPageProps = defineProps({
    borderStyle: {
      type: [String, Boolean],
      default: false,
    },
    refresherEnabled: {
      type: Boolean,
      default: false,
    },
    refresherTriggered: {
      type: Boolean,
      default: false,
    },
  })

  // 自定义事件
  defineEmits(['refresherrefresh', 'scrolltolower'])
</script>

<template>
  <scroll-view
    :style="{
      height: windowHeight + 'px',
      boxSizing: 'border-box',
      borderBottom: scrollPageProps.borderStyle,
    }"
    scroll-y
    :refresherEnabled="scrollPageProps.refresherEnabled"
    :refresherTriggered="scrollPageProps.refresherTriggered"
    @refresherrefresh="$emit('refresherrefresh', $event)"
    @scrolltolower="$emit('scrolltolower', $event)"
  >
    <view class="scroll-page-content">
      <slot></slot>
    </view>
  </scroll-view>
</template>

<style lang="scss">
  .scroll-page-content {
    padding-bottom: env(safe-area-inset-bottom);
  }
</style>

自定义组件 scroll-page 本质上就是对内置组件 scroll-view 进行的二次封装。

2.1.2 custom-section

为了保证统一的页面风格,我们需要封装一个自定义组件 custom-section 通过这个组件来统一布局页面中的不同版块,该组件定义成全局组件并符合 easycom 组件规范,该组件要求满足:

  1. 自定义标题
  2. 自定义样式
  3. 右侧是否显示箭头
<!-- /components/custom-section/custom-section.vue -->
<script setup>
  const sectionProps = defineProps({
    title: {
      type: String,
      default: '',
    },
    showArrow: {
      type: Boolean,
      default: false,
    },
    customStyle: {
      type: Object,
      default: {},
    },
  })
</script>

<template>
  <view class="custom-section" :style="{ ...sectionProps.customStyle }">
    <view class="custom-section-header">
      <view class="section-header-title">{{ sectionProps.title }}</view>
      <view class="section-header-right">
        <slot name="right" />
        <uni-icons
          v-if="sectionProps.showArrow"
          color="#c3c3c5"
          size="16"
          type="forward"
        />
      </view>
    </view>
    <slot />
  </view>
</template>

<style lang="scss">
  .custom-section {
    padding: 40rpx 30rpx 30rpx;
    margin-bottom: 20rpx;
    background-color: #fff;
    border-radius: 20rpx;
  }

  .custom-section-header {
    display: flex;
    justify-content: space-between;
    line-height: 1;
    margin-bottom: 20rpx;
  }

  .section-header-title {
    font-size: 32rpx;
    color: #333;
  }

  .section-header-right {
    display: flex;
    align-items: center;
    font-size: 26rpx;
    color: #c3c3c5;
  }
</style>
2.1.2 布局模板
<!-- pages/my/index.vue -->
<script setup></script>
<template>
  <scroll-page background-color="#F6F7F9">
    <view class="my-page">
      <!-- 用户资料(头像&昵称) -->
      <view class="user-profile">
        <image
          class="user-avatar"
          src="/static/uploads/doctor-avatar.jpg"
        ></image>
        <view class="user-info">
          <text class="nickname">用户907456</text>
          <text class="iconfont icon-edit"></text>
        </view>
      </view>
      <!-- 用户数据 -->
      <view class="user-data">
        <navigator hover-class="none" url=" ">
          <text class="data-number">150</text>
          <text class="data-label">收藏</text>
        </navigator>
        <navigator hover-class="none" url=" ">
          <text class="data-number">23</text>
          <text class="data-label">关注</text>
        </navigator>
        <navigator hover-class="none" url=" ">
          <text class="data-number">230</text>
          <text class="data-label">积分</text>
        </navigator>
        <navigator hover-class="none" url=" ">
          <text class="data-number">3</text>
          <text class="data-label">优惠券</text>
        </navigator>
      </view>
      <!-- 问诊医生 -->
      <custom-section :custom-style="{ paddingBottom: '20rpx' }" title="问诊中">
        <swiper
          class="uni-swiper"
          indicator-active-color="#2CB5A5"
          indicator-color="#EAF8F6"
          indicator-dots
        >
          <swiper-item>
            <view class="doctor-brief">
              <image
                class="doctor-avatar"
                src="/static/uploads/doctor-avatar.jpg"
              />
              <view class="doctor-info">
                <view class="meta">
                  <text class="name">王医生</text>
                  <text class="title">内分泌科 | 主任医师</text>
                </view>
                <view class="meta">
                  <text class="tag">三甲</text>
                  <text class="hospital">积水潭医院</text>
                </view>
              </view>
              <navigator class="doctor-contcat" hover-class="none" url=" ">
                进入咨询
              </navigator>
            </view>
          </swiper-item>
          <swiper-item>
            <view class="doctor-brief">
              <image
                class="doctor-avatar"
                src="/static/uploads/doctor-avatar.jpg"
              />
              <view class="doctor-info">
                <view class="meta">
                  <text class="name">王医生</text>
                  <text class="title">内分泌科 | 主任医师</text>
                </view>
                <view class="meta">
                  <text class="tag">三甲</text>
                  <text class="hospital">积水潭医院</text>
                </view>
              </view>
              <navigator class="doctor-contcat" hover-class="none" url=" ">
                进入咨询
              </navigator>
            </view>
          </swiper-item>
        </swiper>
      </custom-section>
      <!-- 药品订单 -->
      <custom-section show-arrow title="药品订单">
        <template #right>
          <navigator hover-class="none" url=" ">
            全部订单
          </navigator>
        </template>
        <view class="drug-order">
          <navigator hover-class="none" url=" ">
            <uni-badge :text="0" :offset="[3, 3]" absolute="rightTop">
              <image
                src="/static/images/order-status-1.png"
                class="status-icon"
              />
            </uni-badge>
            <text class="status-label">待付款</text>
          </navigator>
          <navigator hover-class="none" url=" ">
            <uni-badge text="2" :offset="[3, 3]" absolute="rightTop">
              <image
                src="/static/images/order-status-2.png"
                class="status-icon"
              />
            </uni-badge>
            <text class="status-label">待付款</text>
          </navigator>
          <navigator hover-class="none" url=" ">
            <uni-badge :text="0" :offset="[3, 3]" absolute="rightTop">
              <image
                src="/static/images/order-status-3.png"
                class="status-icon"
              />
            </uni-badge>
            <text class="status-label">待付款</text>
          </navigator>
          <navigator hover-class="none" url=" ">
            <uni-badge :text="0" :offset="[3, 3]" absolute="rightTop">
              <image
                src="/static/images/order-status-4.png"
                class="status-icon"
              />
            </uni-badge>
            <text class="status-label">待付款</text>
          </navigator>
        </view>
      </custom-section>
      <!-- 快捷工具 -->
      <custom-section title="快捷工具">
        <uni-list :border="false">
          <uni-list-item
            :border="false"
            title="我的问诊"
            show-arrow
            show-extra-icon
            :extra-icon="{
              customPrefix: 'icon-symbol',
              type: 'icon-symbol-tool-01',
            }"
          />
          <uni-list-item
            :border="false"
            title="我的处方"
            show-arrow
            show-extra-icon
            :extra-icon="{
              customPrefix: 'icon-symbol',
              type: 'icon-symbol-tool-02',
            }"
          />
          <uni-list-item
            :border="false"
            title="家庭档案"
            show-arrow
            show-extra-icon
            :extra-icon="{
              customPrefix: 'icon-symbol',
              type: 'icon-symbol-tool-03',
            }"
          />
          <uni-list-item
            :border="false"
            title="地址管理"
            show-arrow
            show-extra-icon
            :extra-icon="{
              customPrefix: 'icon-symbol',
              type: 'icon-symbol-tool-04',
            }"
          />
          <uni-list-item
            :border="false"
            title="我的评价"
            show-arrow
            show-extra-icon
            :extra-icon="{
              customPrefix: 'icon-symbol',
              type: 'icon-symbol-tool-05',
            }"
          />
          <uni-list-item
            :border="false"
            title="官方客服"
            show-arrow
            show-extra-icon
            :extra-icon="{
              customPrefix: 'icon-symbol',
              type: 'icon-symbol-tool-06',
            }"
          />
          <uni-list-item
            :border="false"
            title="设置"
            show-arrow
            show-extra-icon
            :extra-icon="{
              customPrefix: 'icon-symbol',
              type: 'icon-symbol-tool-07',
            }"
          />
        </uni-list>
      </custom-section>
      <!-- 退出登录 -->
      <view class="logout-button">退出登录</view>
    </view>
  </scroll-page>
</template>

<style lang="scss">
  @import './index.scss';
</style>
// pages/my/index.scss
.my-page {
  min-height: 500rpx;
  padding: 150rpx 30rpx 10rpx;
  background-image: linear-gradient(
    180deg,
    rgba(44, 181, 165, 0.46) 0,
    rgba(44, 181, 165, 0) 500rpx
  );
}

.user-profile {
  display: flex;
  height: 140rpx;
}

.user-avatar {
  width: 140rpx;
  height: 140rpx;
  border-radius: 50%;
}

.user-info {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  line-height: 1;
  padding: 30rpx 0;
  margin-left: 24rpx;

  .nickname {
    font-size: 36rpx;
    font-weight: 500;
    color: #333;
  }

  .icon-edit {
    color: #16c2a3;
    padding-top: 20rpx;
    font-size: 32rpx;
  }
}

.user-data {
  display: flex;
  justify-content: space-around;

  height: 100rpx;
  text-align: center;
  line-height: 1;
  margin: 50rpx 0 30rpx;

  .data-number {
    display: block;
    margin-bottom: 10rpx;
    font-size: 48rpx;
    color: #333;
  }

  .data-label {
    display: block;
    font-size: 24rpx;
    color: #979797;
  }
}

.doctor-brief {
  display: flex;
  align-items: center;
  height: 160rpx;

  .doctor-avatar {
    width: 100rpx;
    height: 100rpx;
    margin-left: 10rpx;
    border-radius: 50%;
  }

  .doctor-info {
    height: 100rpx;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    margin-left: 12rpx;
    flex: 1;
  }

  .name {
    font-size: 36rpx;
    color: #3c3e42;
    margin-right: 10rpx;
  }

  .title {
    font-size: 24rpx;
    color: #6f6f6f;
  }

  .tag {
    line-height: 1;
    padding: 2rpx 16rpx;
    font-size: 22rpx;
    color: #fff;
    border-radius: 6rpx;
    background-color: #677fff;
  }

  .hospital {
    font-size: 26rpx;
    color: #3c3e42;
    margin-left: 10rpx;
  }

  .doctor-contcat {
    line-height: 1;
    padding: 16rpx 24rpx;
    border-radius: 100rpx;
    font-size: 24rpx;
    color: #2cb5a5;
    background-color: rgba(44, 181, 165, 0.1);
  }
}

.uni-swiper {
  height: 200rpx;
}

.drug-order {
  display: flex;
  justify-content: space-between;
  text-align: center;
  padding: 30rpx 20rpx 10rpx;

  .status-icon {
    width: 54rpx;
    height: 54rpx;
  }

  .status-label {
    display: block;
    font-size: 24rpx;
    margin-top: 10rpx;
    color: #3c3e42;
  }
}

:deep(.uni-list-item__content-title) {
  font-size: 30rpx !important;
  color: #3c3e42 !important;
}

:deep(.uni-list-item__container) {
  padding: 20rpx 0 !important;
}

:deep(.uni-icon-wrapper) {
  padding-right: 0 !important;
  color: #c3c3c5 !important;
}

:deep(.uni-icons) {
  display: block !important;
}

.logout-button {
  height: 88rpx;
  text-align: center;
  line-height: 88rpx;
  margin: 40rpx 0 30rpx;
  border-radius: 20rpx;
  font-size: 32rpx;
  color: #3c3e42;
  background-color: #fff;
}
2.2 个人信息

在用户处于登录状态时调用接口获取户的头像、昵称等个人信息,我们分成两个步骤来实现:

  1. 封装调用接口的方法,接口文档地址在这里
// services/user.js
// 导入封装好的网络请求模块
import { http } from '@/utils/http'

// 省略前面小节的代码...

/**
 * 获取用户信息
 */
export const userInfoApi = () => {
  return http.get('/patient/myUser')
}
  1. 调用方法获取数据
<!-- pages/my/index.vue -->
<script setup>
  import { ref } from 'vue'
  import { userInfoApi } from '@/services/user'

  // 用户信息
  const userInfo = ref({})

  // 获取用户信息
  async function getUserInfo() {
    // 调用接口获取用户信息
    const { code, data, message } = await userInfoApi()
    // 检测接口是否调用成功
    if (code !== 10000) return uni.utils.toast(message)
    // 渲染用户数据
    userInfo.value = data
  }
  
  // 获取个人信息
  getUserInfo()
</script>
  1. 渲染个人信息
<!-- pages/my/index.vue -->
<template>
  <scroll-page background-color="#F6F7F9">
    <view class="my-page">
      <!-- 用户资料(头像&昵称) -->
      <view class="user-profile">
        <image class="user-avatar" :src="userInfo.avatar"></image>
        <view class="user-info">
          <text class="nickname">{{ userInfo.account }}</text>
          <text class="iconfont icon-edit"></text>
        </view>
      </view>
      <!-- 用户数据 -->
      <view class="user-data">
        <navigator hover-class="none" url=" ">
          <text class="data-number">{{ userInfo.collectionNumber }}</text>
          <text class="data-label">收藏</text>
        </navigator>
        <navigator hover-class="none" url=" ">
          <text class="data-number">{{ userInfo.likeNumber }}</text>
          <text class="data-label">关注</text>
        </navigator>
        <navigator hover-class="none" url=" ">
          <text class="data-number">{{ userInfo.score }}</text>
          <text class="data-label">积分</text>
        </navigator>
        <navigator hover-class="none" url=" ">
          <text class="data-number">{{ userInfo.couponNumber }}</text>
          <text class="data-label">优惠券</text>
        </navigator>
      </view>
      <!-- 此处省略前面小节代码... -->
      
      <!-- 退出登录 -->
      <view class="logout-button">退出登录</view>
    </view>
  </scroll-page>
</template>
2.3 退出登录

退出登录仅需将本地登录状态,即 token 清空即可,然后再跳转到登录页面。

<!-- pages/my/index.vue -->
<script setup>
  import { ref } from 'vue'
  import { onLoad } from '@dcloudio/uni-app'
  import { userInfoApi } from '@/services/user'
  import { useUserStore } from '@/stores/user'

  // 用户相关的数据
  const userStore = useUserStore()

	// 省略前面小节的代码...

  // 退出登录
  function onLogoutClick() {
    // 清除登录状态
    userStore.token = ''
    // 重置 Pinia 的数据
    userStore.openType = 'switchTab'
    userStore.redirectURL = '/pages/index/index'
    // 跳转到登录页
    uni.reLaunch({ url: '/pages/login/index' })
  }

	// 省略前面小节的代码...
</script>

<template>
  <scroll-page background-color="#F6F7F9">
    <view class="my-page">
     	<!-- 此处省略前面小节的代码... -->
      <!-- 退出登录 -->
      <view @click="onLogoutClick" class="logout-button">退出登录</view>
    </view>
  </scroll-page>
</template>

<style lang="scss">
  @import './index.scss';
</style>

在上述代码中要注意,不仅清除了用户的登录状态 token,同时还将 redirectURLopenType 重置为默认值,目的是重新登录后能够跳转到首页面。

还有就是在跳转页面时使用了 uni.reLaunch ,目的是清除所有的页面历史,不允许再有返回的操作。

三、家庭档案

家庭档案就是要填写并保存患者信息,有添加患者、删除患者、编辑患者和患者列表4部分功能构成。

3.1 创建分包

创建分包来包含家庭档案相关的页面,分包目录为 subpkg_archive

{
  "pages": [],
  "globalStyle": {},
  "tabBar": {},
  "subPackages": [
    {
      "root": "subpkg_archive",
      "pages": [
        {
          "path": "form/index",
          "style": {
            "navigationBarTitleText": "添加患者"
          }
        },
        {
          "path": "list/index",
          "style": {
            "navigationBarTitleText": "患者列表"
          }
        }
			]
  	}
  ],
  "uniIdRouter": {}
}

注意事项:先按上述的分包配置创建页面,再去 pages.json 中添加配置。

3.2 添加患者

填写患者信息包括姓名、身份证号、性别等,以表单的方式填写。

3.2.1 布局模板
<!-- subpkg_archive/form/index.vue -->
<script setup>
  import { ref } from 'vue'
  const isDefault = ref([0])
</script>

<template>
  <scroll-page>
    <view class="archive-page">
      <uni-forms border label-width="220rpx" ref="form">
        <uni-forms-item label="患者姓名" name="name">
          <uni-easyinput
            placeholder-style="color: #C3C3C5; font-size: 32rpx"
            :styles="{ color: '#121826' }"
            :input-border="false"
            :clearable="false"
            placeholder="请填写真实姓名"
          />
        </uni-forms-item>
        <uni-forms-item label="患者身份证号" name="name">
          <uni-easyinput
            placeholder-style="color: #C3C3C5; font-size: 32rpx"
            :styles="{ color: '#121826' }"
            :input-border="false"
            :clearable="false"
            placeholder="请填写身份证号"
          />
        </uni-forms-item>
        <uni-forms-item label="患者性别" name="name">
          <uni-data-checkbox
            selectedColor="#16C2A3"
            :localdata="[
              { text: '男', value: 1 },
              { text: '女', value: 0 },
            ]"
          />
        </uni-forms-item>
        <uni-forms-item label="默认就诊人" name="name">
          <view class="uni-switch">
            <switch checked color="#20c6b2" style="transform: scale(0.7)" />
          </view>
        </uni-forms-item>
        <button class="uni-button">保存</button>
      </uni-forms>
    </view>
  </scroll-page>
</template>

<style lang="scss">
  @import './index.scss';
</style>
// subpkg_archive/form/index.scss
.archive-page {
  padding: 30rpx;

  :deep(.uni-forms-item) {
    padding-top: 30rpx !important;
    padding-bottom: 20rpx !important;
  }

  :deep(.uni-forms-item__label) {
    font-size: 32rpx;
    color: #3c3e42;
  }

  :deep(.uni-forms-item--border) {
    border-top: none;
    border-bottom: 1rpx solid #ededed;
  }

  :deep(.uni-easyinput__content-input) {
    height: 36px;
  }

  :deep(.checklist-text) {
    font-size: 32rpx !important;
    margin-left: 20rpx !important;
  }

  // :deep(.uni-forms-item__error) {
  //   position: absolute !important;
  //   left: -220rpx !important;
  //   right: 0 !important;
  //   top: auto !important;
  //   padding-top: 10rpx !important;
  //   margin-top: 20rpx;
  //   border-top: 2rpx solid #eb5757;
  //   color: #eb5757;
  //   font-size: 24rpx;
  //   transition: none;
  // }

  :deep(.uni-data-checklist) {
    display: flex;
    height: 100%;
    padding-left: 10px;
  }

  :deep(.radio__inner) {
    transform: scale(1.25);
  }

  :deep(.checkbox__inner) {
    transform: scale(1.25);
  }
}

.uni-switch {
  display: flex;
  align-items: center;
  height: 100%;
}

.uni-button {
  margin-top: 60rpx;
}

HBuilder X 使用小技巧:当在编辑器中正在编辑某个页面时,点击【重新运行】,会自动在浏览器中打开这个页面,例如正在编辑的页面是 subpkg_archive/form/index.vue ,点击【重新运行】会自动在浏览器中打这个页面。

3.2.2 表单数据验证

在填写好表单数据后还需要验证表单数据的合法,数据合法后再提交给后端接口,分3个步骤来对数据进行验证:

  1. 获取表单数据,根据接口需要来定义数据名称并获取数据
<!-- subpkg_archive/form/index.vue -->
<script setup>
  import { ref } from 'vue'
  // 表单数据
  const formData = ref({
    name: '',
    idCard: '',
    gender: 1,
    defaultFlag: 1,
  })
  // 是否为默认就诊人
  function onSwitchChange(ev) {
    // 是否设置为默认就诊患人
    formData.value.defaultFlag = ev.detail.value ? 1 : 0
  }
</script>

<template>
  <scroll-page>
    <view class="archive-page">
      <uni-forms border label-width="220rpx" :model="formData" ref="form">
        <uni-forms-item label="患者姓名" name="name">
          <uni-easyinput
            v-model="formData.name"
            placeholder-style="color: #C3C3C5; font-size: 32rpx"
            :styles="{ color: '#121826' }"
            :input-border="false"
            :clearable="false"
            placeholder="请填写真实姓名"
          />
        </uni-forms-item>
        <uni-forms-item label="患者身份证号" name="idCard">
          <uni-easyinput
            v-model="formData.idCard"
            placeholder-style="color: #C3C3C5; font-size: 32rpx"
            :styles="{ color: '#121826' }"
            :input-border="false"
            :clearable="false"
            placeholder="请填写身份证号"
          />
        </uni-forms-item>
        <uni-forms-item label="患者性别" name="gender">
          <uni-data-checkbox
            v-model="formData.gender"
            selectedColor="#16C2A3"
            :localdata="[
              { text: '男', value: 1 },
              { text: '女', value: 0 },
            ]"
          />
        </uni-forms-item>
        <uni-forms-item label="默认就诊人">
          <view class="uni-switch">
            <switch
              @change="onSwitchChange"
              :checked="formData.defaultFlag === 1"
              color="#20c6b2"
              style="transform: scale(0.7)"
            />
          </view>
        </uni-forms-item>
        <button class="uni-button">保存</button>
      </uni-forms>
    </view>
  </scroll-page>
</template>

在获取表单数据时,用户名、身份证号、性别都是通过 v-model 来获取的,而默认就诊人则是通过监听 change 事件来获取的,并且接口接收的数据为 01 而不是 truefalse

注意事项:

  • uni-froms 组件要添加 :model 属性
  • uni-forms-item 组件要添加 name 属性
  1. 定义数据验证规则

为不同的表单数据定义不同的验证规:

  • 验证中文姓名正则 ^[\u4e00-\u9fa5]{2,5}$
  • 验证身份证 ^[1-9]\\d{5}(?:18|19|20)\\d{2}(?:0[1-9]|10|11|12)(?:0[1-9]|[1-2]\\d|30|31)\\d{3}[\\dXx]$
<!-- subpkg_archive/form/index.vue -->
<script setup>
  import { ref } from 'vue'
  // 表单数据
  const formData = ref({
    name: '',
    idCard: '',
    gender: 1,
    defaultFlag: 1,
  })

  // 表单验证规则
  const formRules = {
    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]|10|11|12)(?:0[1-9]|[1-2]\\d|30|31)\\d{3}[\\dXx]$',
          errorMessage: '身份证号格式不正确',
        },
      ],
    },
    gender: {
      rules: [
        { required: true, errorMessage: '请勾选患者姓名' },
      ],
    },
  }

  // 是否为默认就诊人
  function onSwitchChange(ev) {
    // 是否设置为默认就诊患人
    formData.value.defaultFlag = ev.detail.value ? 1 : 0
  }
</script>

<template>
  <scroll-page>
    <view class="archive-page">
      <uni-forms
        border
        label-width="220rpx"
        :model="formData"
        :rules="formRules"
        ref="form"
      >
        <!-- 省略前面小节代码... -->
      </uni-forms>
    </view>
  </scroll-page>
</template>

关于性别的验证还有补充,我们先把下一小节学习完再回来介绍。

我们都知道根据身份证号是可以区别性别的,当用户勾选的性别与身份证号性别不符时,要以身份证号中的性别为准,这就要求判断身份证号中性别与勾选的性别是否相同。

实现的关键步骤:

  • 身份证号中第17位用来标识性别,偶数为女,奇数为男。
  • validateFunction 自定义数据校验的逻辑,返回值为 true 表示验证通过,验证不通过时调用 callback 方法。
// 表单验证规则
const formRules = {
  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]|10|11|12)(?:0[1-9]|[1-2]\\d|30|31)\\d{3}[\\dXx]$',
        errorMessage: '身份证号格式不正确',
      },
    ],
  },
  gender: {
    rules: [
      { required: true, errorMessage: '请勾选患者性别' },
      {
        validateFunction(rule, value, data, callback) {
          // 检测身份证号第17位是否为偶数
          if (data.idCard.slice(16, 17) % 2 !== value) {
            callback('选择的性别与身份号中性别不一致')
          }
					// 验证通过时返回 true
          return true
        },
      },
    ],
  },
}
  1. 调用验证方法
<!-- subpkg_archive/form/index.vue -->
<script setup>
  import { ref } from 'vue'
  // 表单组件 ref
  const formRef = ref()
  
  // 表单数据
  const formData = ref({
    name: '',
    idCard: '',
    gender: 1,
    defaultFlag: 0,
  })

  // 省略前面小节的代码...

  // 提交表单数据
  async function onFormSubmit() {
    try {
      // 根据验证规则验证数据
      await formRef.value.validate()
    } catch(error) {
    	console.log(error)
    }
  }

  // 是否为默认就诊人
  function onSwitchChange(ev) {
    // 是否设置为默认就诊患人
    formData.value.defaultFlag = ev.detail.value ? 1 : 0
  }
</script>

<template>
  <scroll-page>
    <view class="archive-page">
      <uni-forms
        border
        label-width="220rpx"
        :model="formData"
        :rules="formRules"
        ref="formRef"
      >
        <!-- 省略前面小节代码... -->
        <button @click="onFormSubmit" class="uni-button">保存</button>
      </uni-forms>
    </view>
  </scroll-page>
</template>

测试用身份证号数据:

  • 110101198307212600
  • 110101196107145504
  • 11010119890512132X
  • 110101196501023433
  • 110101197806108758
  • 110101198702171378
  • 110101198203195893
  • 如有雷同纯属巧合,可删除。
3.2.3 提交数据
  1. 根据接口文档封装接口调用的方法,文档的地址在这里。
// services/patinet.js

// 导入封装好的网络请求模块
import { http } from '@/utils/http'

/**
 * 添加患者(家庭档案)
 */
export const addPatientApi = (data) => {
  return http.post('/patient/add', data)
}
  1. 调用接口提交数据
<script setup>
  import { ref } from 'vue'
  import { addPatientApi } from '@/services/patient'
	
  // 省略前面部分代码...

  // 提交表单数据
  async function onFormSubmit() {
    try {
      // 根据验证规则验证数据
      const formData = await formRef.value.validate()
      // 添加患者
      addPatient()
    } catch (error) {
      console.log(error)
    }
  }

  // 省略前面小节的代码...

  // 添加患者信息
  async function addPatient() {
    // 添加患者接口
    const { code, message } = await addPatientApi(formData.value)
    // 检测接口是否调用成功
    if (code !== 10000) return uni.utils.toast(message)

    // 跳转到患者列表页面
    uni.navigateBack()
  }
</script>

在添加患者成功后的逻辑是到患者列表中进行查看,而在正常的添加患者逻辑中,添加患者的页面是从患者列表跳转过来的,因此我们调用 uni.navigateBack 返加上一页就可以了。

3.3 患者列表

在【我的】页面中找到【家庭档案】,给它添加链接地址跳转到患者列表页面。

<!-- pages/my/index.vue -->
<script setup>
  // 省略前面小节代码...
</script>
<template>
  <scroll-page background-color="#F6F7F9">
    <view class="my-page">
      <!-- 省略前面小节的代码... -->
      <!-- 快捷工具 -->
      <custom-section title="快捷工具">
        <uni-list :border="false">
          ...
          <uni-list-item
            :border="false"
            title="家庭档案"
            show-arrow
            show-extra-icon
            to="/subpkg_archive/list/index"
            :extra-icon="{
              customPrefix: 'icon-symbol',
              type: 'icon-symbol-tool-03',
            }"
          />
          ...
        </uni-list>
      </custom-section>

      <!-- 退出登录 -->
      <view @click="onLogoutClick" class="logout-button">退出登录</view>
    </view>
  </scroll-page>
</template>
3.3.1 布局模板
<!-- subpkg_archive/list/index.vue -->
<script setup>
  import { ref } from 'vue'

  const swipeOptions = ref([
    {
      text: '删除',
      style: {
        backgroundColor: '#dd524d',
      },
    },
  ])
</script>

<template>
  <scroll-page>
    <view class="archive-page">
      <view class="archive-tips">最多可添加6人</view>

      <uni-swipe-action>
        <uni-swipe-action-item :right-options="swipeOptions">
          <view class="archive-card active">
            <view class="archive-info">
              <text class="name">李富贵</text>
              <text class="id-card">321***********6164</text>
              <text class="default">默认</text>
            </view>
            <view class="archive-info">
              <text class="gender">男</text>
              <text class="age">32岁</text>
            </view>
            <navigator
              hover-class="none"
              class="edit-link"
              url="/subpkg_archive/form/index"
            >
              <uni-icons
                type="icon-edit"
                size="20"
                color="#16C2A3"
                custom-prefix="iconfont"
              />
            </navigator>
          </view>
        </uni-swipe-action-item>

        <uni-swipe-action-item :right-options="swipeOptions">
          <view class="archive-card">
            <view class="archive-info">
              <text class="name">李富贵</text>
              <text class="id-card">321***********6164</text>
            </view>
            <view class="archive-info">
              <text class="gender">男</text>
              <text class="age">32岁</text>
            </view>
            <navigator
              hover-class="none"
              class="edit-link"
              url="/subpkg_archive/form/index"
            >
              <uni-icons
                type="icon-edit"
                size="20"
                color="#16C2A3"
                custom-prefix="iconfont"
              />
            </navigator>
          </view>
        </uni-swipe-action-item>
      </uni-swipe-action>

      <!-- 添加按钮 -->
      <view v-if="true" class="archive-card">
        <navigator
          class="add-link"
          hover-class="none"
          url="/subpkg_archive/form/index"
        >
          <uni-icons color="#16C2A3" size="24" type="plusempty" />
          <text class="label">添加患者</text>
        </navigator>
      </view>
    </view>
  </scroll-page>
</template>

<style lang="scss">
  @import './index.scss';
</style>
// subpkg_archive/list/index.scss
.archive-page {
  padding: 30rpx;
}

.archive-tips {
  line-height: 1;
  padding-left: 10rpx;
  margin: 30rpx 0;
  font-size: 26rpx;
  color: #6f6f6f;
}

.archive-card {
  display: flex;
  flex-direction: column;
  justify-content: center;

  position: relative;

  height: 180rpx;
  padding: 30rpx;
  margin-bottom: 30rpx;
  border-radius: 10rpx;
  box-sizing: border-box;
  border: 1rpx solid transparent;
  background-color: #f6f6f6;

  &.active {
    background-color: rgba(44, 181, 165, 0.1);
    // border: 1rpx solid #16c2a3;

    .default {
      display: block;
    }
  }

  .archive-info {
    display: flex;
    align-items: center;
    color: #6f6f6f;
    font-size: 28rpx;
    margin-bottom: 10rpx;
  }

  .name {
    margin-right: 30rpx;
    color: #121826;
    font-size: 32rpx;
    font-weight: 500;
  }

  .id-card {
    color: #121826;
  }

  .gender {
    margin-right: 30rpx;
  }

  .default {
    display: none;
    height: 36rpx;
    line-height: 36rpx;
    text-align: center;
    padding: 0 12rpx;
    margin-left: 30rpx;
    border-radius: 4rpx;
    color: #fff;
    font-size: 24rpx;
    background-color: #16c2a3;
  }
}

.edit-link {
  position: absolute;
  top: 50%;
  right: 30rpx;

  transform: translateY(-50%);
}

.add-link {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;

  .label {
    margin-top: 10rpx;
    font-size: 28rpx;
    color: #16c2a3;
  }
}

:deep(.uni-swipe_button-group) {
  bottom: 30rpx;
}
3.3.2 获取数据
  1. 根据接口文档的要求封装接口调用的方法,接口文档请看这里。
// services/patinent.js
// 导入封装好的网络请求模块
import { http } from '@/utils/http'

/**
 * 添加患者(家庭档案)
 */
export const addPatientApi = (data) => {
  return http.post('/patient/add', data)
}

/**
 * 获取患者(家庭档案)列表
 */
export const patientListApi = (data) => {
  return http.get('/patient/mylist')
}
  1. 在页面调用接口获取数据并渲染
<!-- subpkg_archive/list/index.uve -->
<script setup>
  import { ref } from 'vue'
  import { onShow } from '@dcloudio/uni-app'
  import { patientListApi } from '@/services/patient'

  // 是否显示页面内容
  const pageShow = ref(false)
  // 患者列表
  const patinetList = ref([])

  // 侧滑按钮配置
  const swipeOptions = ref([
    {
      text: '删除',
      style: {
        backgroundColor: '#dd524d',
      },
    },
  ])

  // 生命周期(页面显示)
  onShow(() => {
    getPatientList()
  })

  // 家庭档案(患者)列表
  async function getPatientList() {
    // 患者列表接口
    const { code, data } = await patientListApi()
    // 检测接口是否调用成功
    if (code !== 10000) return uni.utils.showToast('列表获取失败,稍后重试!')
    // 渲染接口数据
    patinetList.value = data
    // 展示页面内容
    pageShow.value = true
  }
</script>

<template>
  <scroll-page>
    <view class="archive-page" v-if="pageShow">
      <view class="archive-tips">最多可添加6人</view>
      <uni-swipe-action>
        <uni-swipe-action-item
          v-for="(patient, index) in patinetList"
          :key="patient.id"
          :right-options="swipeOptions"
        >
          <view
            :class="{ active: patient.defaultFlag === 1 }"
            class="archive-card"
          >
            <view class="archive-info">
              <text class="name">{{ patient.name }}</text>
              <text class="id-card">
                {{ patient.idCard.replace(/^(.{6}).+(.{4})$/, '$1********$2') }}
              </text>
              <text v-if="patient.defaultFlag === 1" class="default">默认</text>
            </view>
            <view class="archive-info">
              <text class="gender">{{ patient.genderValue }}</text>
              <text class="age">{{ patient.age }}岁</text>
            </view>
            <navigator
              hover-class="none"
              class="edit-link"
              :url="`/subpkg_archive/form/index?id=${patient.id}`"
            >
              <uni-icons
                type="icon-edit"
                size="20"
                color="#16C2A3"
                custom-prefix="iconfont"
              />
            </navigator>
          </view>
        </uni-swipe-action-item>
      </uni-swipe-action>

      <!-- 添加按钮 -->
      <view v-if="patinetList.length < 6" class="archive-card">
        <navigator
          class="add-link"
          hover-class="none"
          url="/subpkg_archive/form/index"
        >
          <uni-icons color="#16C2A3" size="24" type="plusempty" />
          <text class="label">添加患者</text>
        </navigator>
      </view>
    </view>
  </scroll-page>
</template>

在渲染数据时要注意:

  • pageShow 避免页面的抖动,数据未请求结束时显示空白内容
  • 身份证号脱敏正则 /^(.{6}).+(.{4})$/
  • 最多只能添加 6 名患者,超出6个后隐藏添加按钮
  • 跳转到编辑患者页面时地址中要拼接患者的 ID
  • 数据获取在是 onShow 生命周期获取,组件式函数 onShow@dcloudio/uni-app 提供
3.4 删除患者

用户在患者列表上向左滑动就能展示删除的按钮,点击这个按钮调用接口删除数据。

3.4.1 监听点击

此用用到了 uni-swipe-action-item 组件,该组件能够监听到用的点击事件

<!-- subpkg_archive/list/index.vue -->
<script setup>
  import { ref } from 'vue'
  import { onShow } from '@dcloudio/uni-app'
  import { patientListApi } from '@/services/patient'
	
  // 省略前面小节的代码...

  // 侧滑按钮配置
  const swipeOptions = ref([
    {
      text: '删除',
      style: {
        backgroundColor: '#dd524d',
      },
    },
  ])
	
  // 省略前面小节的代码...

  // 滑动操作点击
  async function onSwipeActionClick(id, index) {
		// 传递数据的 id 值和索引值
  }

	// 省略前面小节的代码...
</script>

<template>
  <scroll-page>
    <view class="archive-page" v-if="pageShow">
      <view class="archive-tips">最多可添加6人</view>
      <uni-swipe-action>
        <uni-swipe-action-item
          v-for="(patient, index) in patinetList"
          :key="patient.id"
          :right-options="swipeOptions"
          @click="onSwipeActionClick(patient.id, index)"
        >
          ...
        </uni-swipe-action-item>
      </uni-swipe-action>

      <!-- 省略前面小节代码... -->
    </view>
  </scroll-page>
</template>

在点击事件的回调函数里接收了待删除数据的 ID 和索引,这两个参数在后面小节当中会用到。

3.4.2 删除数据
  1. 根据接口文档封装调用接口的方法,接口文档地址在这里。
// services/patient.js

// 导入封装好的网络请求模块
import { http } from '@/utils/http'

// 省略前面小节的代码...

/**
 * 删除患者(家庭档案)
 */
export const removePatientApi = (id) => {
  return http.delete(`/patient/del/${id}`)
}
  1. 在点击事件回调中调用接口删除患者数据,在删除数据的时候要注意,调用接口是要删除服务器的患者数据,但是本地在 Vue 中也保存了一份患者数据,Vue 中保存的数据也可同步删除,根据索引值实来删除。
<!-- subpkg_archive/list/index.vue -->
<script setup>
  import { ref } from 'vue'
  import { onShow } from '@dcloudio/uni-app'
  import { patientListApi, removePatientApi } from '@/services/patient'
	
  // 省略前面小节的代码...

  // 侧滑按钮配置
  const swipeOptions = ref([
    {
      text: '删除',
      style: {
        backgroundColor: '#dd524d',
      },
    },
  ])
	
  // 省略前面小节的代码...

  // 滑动操作点击
  async function onSwipeActionClick(id, index) {
    // 调用删除患者接口
    const { code, message } = await removePatientApi(id)
    // 检测接口是否调用成功
    if (code !== 10000) return uni.utils.toast(message)
    // Vue 实例中的数据也要同步删除
    patinetList.value.splice(index, 1)
  }

	// 省略前面小节的代码...
</script>

<template>
  <scroll-page>
    <view class="archive-page" v-if="pageShow">
      <view class="archive-tips">最多可添加6人</view>
      <uni-swipe-action>
        <uni-swipe-action-item
          v-for="(patient, index) in patinetList"
          :key="patient.id"
          :right-options="swipeOptions"
          @click="onSwipeActionClick(patient.id, index)"
        >
          ...
        </uni-swipe-action-item>
      </uni-swipe-action>

      <!-- 省略前面小节代码... -->
    </view>
  </scroll-page>
</template>
3.5 编辑患者

编辑患者与添加患者共有了相同的页面,区别在于编患者时需要在地址中包含患者的 ID,并且获取这个 ID 将患者原信息查询出来,在此基础之上进行修改(编辑)。

3.5.1 查询患者信息
  1. 根据接口文档获封装接口调用的方法来获取患者信息,接口文档地址在这里。
// services/patinet.js
import { http } from '@/utils/http'

// 省略前面小节的代码...

/**
 * 患者详情(家庭档案)
 */
export const patientDetailApi = (id) => {
  return http.get(`/patient/info/${id}`)
}

3.5.2 获取地址上参数,根据 ID 参数查询患者信息

在 uni-app 中获取页面地址参数有两种方法,一种是在 onLoad 生命周期中,另一种是使用 defineProps,接下来分别演示两种用法:

<!-- subpkg_archive/form/index.vue -->
<script setup>
  import { onLoad } from '@dcloudio/uni-app'
  
  // 生命周期(页面加载完成)
  onLoad((query) => {
    console.log(query.id)
  })

  // 使用 defineProps 接收地址参数
  const props = defineProps({ id: String })
  console.log(props.id)
</script>

注意组件式函数 onLoad@dcloudio/uni-app 提供,本小节使用 defineProps 来获取地址参数

<!-- subpkg_archive/form/index.vue -->
<script setup>
  import { ref } from 'vue'
  import { addPatientApi, patientDetailApi } from '@/services/patient'

  // 省略前面小节的代码...

  // 使用 defineProps 接收地址参数
  const props = defineProps({ id: String })
	
  // 省略前面小节的代码...

  // 获取患者详情信息
  async function getPatientDetail() {
    // 是否存在患者 ID
    if (!props.id) return
    // 有ID说明当前处于编辑状态,修改页面标题
    uni.setNavigationBarTitle({ title: '编辑患者' })

    // 患者详情接口
    const {
      code,
      data: { genderValue, age, ...rest },
    } = await patientDetailApi(props.id)

    // 渲染患者信息
    formData.value = rest
  }

  // 查询患者信息
  getPatientDetail()
</script>

在上述的代码中要注意:

  1. 务必要判断地址中是否包含有 ID,有 ID 的情况下才会出查询数据
  2. uni.setNavigationBarTitle API 动态修改导航栏的标题
  3. 过滤掉多余的数据 agegenderValue,年龄是根据身份证号计算的,genderValue 不需要回显
3.5.2 更新患者信息

在原有患者信息基础之上进行修改,修改完毕合再次调用接口实现数据的更新,接口文档的在址在这里。

  1. 封装接口调用的方法
// services/patient.js
import { http } from '@/utils/http'

// 省略前面小节的代码...

/**
 * 编辑(更新)患者(家庭档案)
 */
export const updatePatientApi = (data) => {
  return http.put(`/patient/update`, data)
}
  1. 调用更新患者信息的接口
<!-- subpkg_archive/form/index.vue -->
<script setup>
  import { ref } from 'vue'
  import {
    addPatientApi,
    patientDetailApi,
    updatePatientApi,
  } from '@/services/patient'

  // 省略前面小节的代码...

  // 使用 defineProps 接收地址参数
  const props = defineProps({ id: String })

  // 提交表单数据
  async function onFormSubmit() {
    try {
      // 根据验证规则验证数据
      const formData = await formRef.value.validate()
      // 添加患者或更新患者
      /****************重要*****************/
      props.id ? updatePatient() : addPatient()
      /****************重要*****************/
    } catch (error) {
      console.log(error)
    }
  }
	
  // 省略前面小节的代码...

  // 添加患者信息
  async function addPatient() {
    // 添加患者接口
    const { code, message } = await addPatientApi(formData.value)
    // 检测接口是否调用成功
    if (code !== 10000) return uni.utils.toast(message)
    // 跳转到患者列表页面
    uni.navigateBack()
  }

  // 编辑(更新)患者信息
  async function updatePatient() {
    // 更新患者信息接口
    const { code, message } = await updatePatientApi(formData.value)
    // 检测接口是否调用成功
    if (code !== 10000) return uni.utils.toast(message)
    // 跳转到患者列表页面
    uni.navigateBack()
  }

	// 省略前面小节的代码...
</script>

在用户点击提交按钮时根据是否存在患者 ID 来区别到底是添加患者还是编辑患者。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员朱永胜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值