前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >uni-app(优医咨询)项目实战 - 第4天

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

作者头像
程序员朱永胜
发布2024-04-25 14:10:04
1150
发布2024-04-25 14:10:04
举报

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

学习目标:

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

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

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

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

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

1.1 配置拦截器
代码语言:javascript
复制
// 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

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

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

1.2 检测状态码

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

代码语言:javascript
复制
// 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 状态管理实现。

代码语言:javascript
复制
// 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 的页面路径。

代码语言:javascript
复制
// 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,然后跳转加这个页面(路由)

代码语言:javascript
复制
<!-- 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 组件来使用的,还要注意的是这个页面使用的是自定义导航栏。

代码语言:javascript
复制
{
  "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 的高度为视口的高度
代码语言:javascript
复制
<!-- /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. 适配安全区域
代码语言:javascript
复制
<!-- /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 边框线的效果。

代码语言:javascript
复制
<!-- /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 实现下拉刷新交互
代码语言:javascript
复制
<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. 右侧是否显示箭头
代码语言:javascript
复制
<!-- /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 布局模板
代码语言:javascript
复制
<!-- 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>
代码语言:javascript
复制
// 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. 封装调用接口的方法,接口文档地址在这里
代码语言:javascript
复制
// services/user.js
// 导入封装好的网络请求模块
import { http } from '@/utils/http'

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

/**
 * 获取用户信息
 */
export const userInfoApi = () => {
  return http.get('/patient/myUser')
}
  1. 调用方法获取数据
代码语言:javascript
复制
<!-- 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. 渲染个人信息
代码语言:javascript
复制
<!-- 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 清空即可,然后再跳转到登录页面。

代码语言:javascript
复制
<!-- 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

代码语言:javascript
复制
{
  "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 布局模板
代码语言:javascript
复制
<!-- 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>
代码语言:javascript
复制
// 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. 获取表单数据,根据接口需要来定义数据名称并获取数据
代码语言:javascript
复制
<!-- 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]$
代码语言:javascript
复制
<!-- 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 方法。
代码语言:javascript
复制
// 表单验证规则
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. 调用验证方法
代码语言:javascript
复制
<!-- 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. 根据接口文档封装接口调用的方法,文档的地址在这里。
代码语言:javascript
复制
// services/patinet.js

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

/**
 * 添加患者(家庭档案)
 */
export const addPatientApi = (data) => {
  return http.post('/patient/add', data)
}
  1. 调用接口提交数据
代码语言:javascript
复制
<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 患者列表

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

代码语言:javascript
复制
<!-- 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 布局模板
代码语言:javascript
复制
<!-- 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>
代码语言:javascript
复制
// 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. 根据接口文档的要求封装接口调用的方法,接口文档请看这里。
代码语言:javascript
复制
// 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. 在页面调用接口获取数据并渲染
代码语言:javascript
复制
<!-- 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 组件,该组件能够监听到用的点击事件

代码语言:javascript
复制
<!-- 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. 根据接口文档封装调用接口的方法,接口文档地址在这里。
代码语言:javascript
复制
// services/patient.js

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

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

/**
 * 删除患者(家庭档案)
 */
export const removePatientApi = (id) => {
  return http.delete(`/patient/del/${id}`)
}
  1. 在点击事件回调中调用接口删除患者数据,在删除数据的时候要注意,调用接口是要删除服务器的患者数据,但是本地在 Vue 中也保存了一份患者数据,Vue 中保存的数据也可同步删除,根据索引值实来删除。
代码语言:javascript
复制
<!-- 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. 根据接口文档获封装接口调用的方法来获取患者信息,接口文档地址在这里。
代码语言:javascript
复制
// 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,接下来分别演示两种用法:

代码语言:javascript
复制
<!-- 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 来获取地址参数

代码语言:javascript
复制
<!-- 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. 封装接口调用的方法
代码语言:javascript
复制
// services/patient.js
import { http } from '@/utils/http'

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

/**
 * 编辑(更新)患者(家庭档案)
 */
export const updatePatientApi = (data) => {
  return http.put(`/patient/update`, data)
}
  1. 调用更新患者信息的接口
代码语言:javascript
复制
<!-- 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 来区别到底是添加患者还是编辑患者。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2024-04-25,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • uni-app(优医咨询)项目实战 - 第4天
    • 一、权限验证
      • 1.1 配置拦截器
      • 1.2 检测状态码
      • 1.3 重定向页面
    • 二、我的
      • 2.1 页面布局
      • 2.2 个人信息
      • 2.3 退出登录
    • 三、家庭档案
      • 3.1 创建分包
      • 3.2 添加患者
      • 3.3 患者列表
      • 3.4 删除患者
      • 3.5 编辑患者
相关产品与服务
云开发 CloudBase
云开发(Tencent CloudBase,TCB)是腾讯云提供的云原生一体化开发环境和工具平台,为200万+企业和开发者提供高可用、自动弹性扩缩的后端云服务,可用于云端一体化开发多种端应用(小程序、公众号、Web 应用等),避免了应用开发过程中繁琐的服务器搭建及运维,开发者可以专注于业务逻辑的实现,开发门槛更低,效率更高。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档