Skip to content
On this page

用户模块

布局容器-效果预览

image-20220808185458747

布局容器-嵌套路由

实现:首页,健康百科,消息通知,我的,布局容器的搭建

步骤:

  1. 准备四个二级页面组件:Home、Article、Notify、User
  2. 配置嵌套路由
  3. 设置二级路由出口

代码:

  1. 准备二级页面组件,直接从素材中获取

Home/index.vue

vue
<script setup lang="ts"></script>

<template>
  <div class="home-page">home</div>
</template>

<style lang="scss" scoped></style>

Article/index.vue

vue
<script setup lang="ts"></script>

<template>
  <div class="article-page">home</div>
</template>

<style lang="scss" scoped></style>

Notify/index.vue

vue
<script setup lang="ts"></script>

<template>
  <div class="notify-page">home</div>
</template>

<style lang="scss" scoped></style>

User/index.vue

vue
<script setup lang="ts"></script>

<template>
  <div class="user-page">home</div>
</template>

<style lang="scss" scoped></style>
  1. 配置嵌套路由:router/index.ts
ts
routes: [
  { path: "/login", component: () => import("@/views/Login/index.vue") },
  {
    path: "/",
    component: () => import("@/views/Layout/index.vue"),
    redirect: "/home",
    children: [
      { path: "/home", component: () => import("@/views/Home/index.vue") },
      {
        path: "/article",
        component: () => import("@/views/Article/index.vue"),
      },
      { path: "/notify", component: () => import("@/views/Notify/index.vue") },
      { path: "/user", component: () => import("@/views/User/index.vue") },
    ],
  },
];
  1. 配置二级路由出口:Layout/index.vue
diff
<script setup lang="ts"></script>

<template>
  <div class="layout-page">
+    <router-view />
    layout
  </div>
</template>

<style lang="scss" scoped></style>

布局容器-底部 tab 栏-自定义图标

实现:底部 tab 的切换

步骤:

  • 查文档,使用 tabbar组件, 实现路由切换功能
  • 查文档,给 tabbar 加上自定义图标

代码:

  • 路由切换功能
html
<van-tabbar route>
  <van-tabbar-item to="/home">首页</van-tabbar-item>
  <van-tabbar-item to="/article">健康百科</van-tabbar-item>
  <van-tabbar-item to="/notify">消息中心</van-tabbar-item>
  <van-tabbar-item to="/user">我的</van-tabbar-item>
</van-tabbar>
  • 自定义图标
vue
<script setup lang="ts"></script>

<template>
  <div class="layout-page">
    <router-view />
    <van-tabbar route>
      <van-tabbar-item to="/home">
        首页
        <template #icon="{ active }">
          <cp-icon
            :name="active ? 'home-index-active' : 'home-index-default'"
          />
        </template>
      </van-tabbar-item>
      <van-tabbar-item to="/article">
        健康百科
        <template #icon="{ active }">
          <cp-icon :name="`home-article-${active ? 'active' : 'default'}`" />
        </template>
      </van-tabbar-item>
      <van-tabbar-item to="/notify">
        消息中心
        <template #icon="{ active }">
          <cp-icon :name="`home-notice-${active ? 'active' : 'default'}`" />
        </template>
      </van-tabbar-item>
      <van-tabbar-item to="/user">
        我的
        <template #icon="{ active }">
          <cp-icon :name="`home-mine-${active ? 'active' : 'default'}`" />
        </template>
      </van-tabbar-item>
    </van-tabbar>
  </div>
</template>

<style lang="scss" scoped></style>

布局容器-显示加载进度

目标:显示加载进度

  • 懒加载的组件,在切换路由的时候,异步加载资源,网速不好会 “白屏”
  • 加上进度条,提高用户体验

步骤:

  1. 安装插件nprogress ,查文档使用
  2. 导航前置守卫,路由切换完成前,开启进度条
  3. 导航 后置守卫 ,路由切换完成后,关闭进度条
  4. 查文档,修改nprogress 的配置和样式

代码:

  1. 安装插件,router/index.ts 中导入模块、导入样式
bash
pnpm i nprogress
pnpm i @types/nprogress -D
ts
import NProgress from "nprogress";
import "nprogress/nprogress.css";
  1. 导航前置守卫,开启进度条
diff
+ router.beforeEach(() => {
+  // 💥 注意开启后,不会自动关闭
+  NProgress.start()
+ }
  1. 导航后置守卫,关闭进度条
ts
router.afterEach(() => {
  NProgress.done();
});
  1. 查文档,修改配置:关闭旋转按钮,颜色修改

router/index.ts

ts
NProgress.configure({
  showSpinner: false,
});

main.scss

scss
#nprogress .bar {
  background-color: var(--cp-primary) !important;
}

布局容器-页面标题

实现:切换页面切换标题,扩展 vue-router 的类型

步骤:

  1. 给每一个路由,添加 元信息 数据
  2. 后置守卫中,通过 to.meta 接收元信息,完成修改标题
  3. 优化-扩展元信息类型

代码:

  1. 给每一个路由添加 元信息 数据

router/index.ts

diff
const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '/',
      component: () => import('@/views/Layout/index.vue'),
      redirect: '/home',
      children: [
        {
          path: '/home',
          component: () => import('@/views/Home/index.vue'),
+          meta: { title: '首页' }
        },
        {
          path: '/article',
          component: () => import('@/views/Article/index.vue'),
+          meta: { title: '健康百科' }
        },
        {
          path: '/notify',
          component: () => import('@/views/Notify/index.vue'),
+          meta: { title: '消息通知' }
        },
        {
          path: '/user',
          component: () => import('@/views/User/index.vue'),
+          meta: { title: '个人中心' }
        }
      ]
    },
    {
      path: '/login',
      component: () => import('@/views/Login/index.vue'),
+      meta: { title: '登录' }
    }
  ]
})
  1. 后置守卫中,通过 to.meta 接收元信息,完成修改标题

router/index.ts

diff
router.afterEach((to) => {
  // 处理标题
+  document.title = `优医问诊-${to.meta.title || ''}`
  // ...省略其它代码
}
  1. 扩展元信息类型 types/vue-router.d.ts
ts
import "vue-router";

declare module "vue-router" {
  // 扩展 元信息类型
  interface RouteMeta {
    // 标题
    title?: string;
  }
}

请求函数自动带上 token

目标:大多接口需要传参 token,请求拦截器中,自动带上 token

utils/request.ts

diff
request.interceptors.request.use(

 function (config) {
+   const store = useUserStore()
+   const token = store.userInfo?.token
+   if (token) {
+     config.headers.Authorization = `Bearer ${token}`
+   }
    return config
  },
  function (err) {
    return Promise.reject(err)
  }
)

个人中心

image-20220808185103327

个人中心-头部展示

实现:头部个人信息展示与订单卡片布局

步骤:

  • 准备静态结构
  • 定义 API 函数
  • 获取数据
  • 定义类似,插值渲染

代码:

1)准备静态结构 view/User/index.vue

vue
<script setup lang="ts"></script>

<template>
  <div class="user-page">
    <div class="user-page-head">
      <div class="top">
        <van-image
          round
          fit="cover"
          src="https://yanxuan-item.nosdn.127.net/ef302fbf967ea8f439209bd747738aba.png"
        />
        <div class="name">
          <p>用户907456</p>
          <p><van-icon name="edit" /></p>
        </div>
      </div>
      <van-row>
        <van-col span="6">
          <p>150</p>
          <p>收藏</p>
        </van-col>
        <van-col span="6">
          <p>23</p>
          <p>关注</p>
        </van-col>
        <van-col span="6">
          <p>270</p>
          <p>积分</p>
        </van-col>
        <van-col span="6">
          <p>3</p>
          <p>优惠券</p>
        </van-col>
      </van-row>
    </div>
    <div class="user-page-order">
      <div class="head">
        <h3>药品订单</h3>
        <router-link to="/order"
          >全部订单 <van-icon name="arrow"
        /></router-link>
      </div>
      <van-row>
        <van-col span="6">
          <cp-icon name="user-paid" />
          <p>待付款</p>
        </van-col>
        <van-col span="6">
          <cp-icon name="user-shipped" />
          <p>待发货</p>
        </van-col>
        <van-col span="6">
          <cp-icon name="user-received" />
          <p>待收货</p>
        </van-col>
        <van-col span="6">
          <cp-icon name="user-finished" />
          <p>已完成</p>
        </van-col>
      </van-row>
    </div>
  </div>
</template>

<style lang="scss" scoped>
.user-page {
  background-color: var(--cp-bg);
  min-height: calc(100vh - 50px);
  padding: 0 15px 65px;
  // 头部
  &-head {
    height: 200px;
    background: linear-gradient(
      180deg,
      rgba(44, 181, 165, 0.46),
      rgba(44, 181, 165, 0)
    );
    margin: 0 -15px;
    padding: 0 15px;
    .top {
      display: flex;
      padding-top: 50px;
      align-items: center;
      .van-image {
        width: 70px;
        height: 70px;
      }
      .name {
        padding-left: 10px;
        p {
          &:first-child {
            font-size: 18px;
            font-weight: 500;
          }
          &:last-child {
            margin-top: 10px;
            color: var(--cp-primary);
            font-size: 16px;
          }
        }
      }
    }
    .van-row {
      margin: 0 -15px;
      padding-top: 15px;
      p {
        text-align: center;
        &:first-child {
          font-size: 18px;
          font-weight: 500;
        }
        &:last-child {
          color: var(--cp-dark);
          font-size: 12px;
          padding-top: 4px;
        }
      }
    }
  }
  // 订单
  &-order {
    background-color: #fff;
    border-radius: 8px;
    margin-bottom: 15px;
    padding-bottom: 15px;
    .head {
      display: flex;
      justify-content: space-between;
      line-height: 50px;
      padding: 0 15px;
      a {
        color: var(--cp-tip);
      }
    }
    .van-col {
      text-align: center;
      .cp-icon {
        font-size: 28px;
      }
      p {
        font-size: 12px;
        padding-top: 4px;
      }
    }
  }
}
</style>

2)定义 API 函数

services/user.ts

ts
// 获取个人信息
export const getUserInfoAPI = () => {
  return request({
    url: "/patient/myUser",
  });
};

3)获取数据进行渲染

ts
import { onMounted, ref } from "vue";
import { getUserInfoAPI } from "@/services/user";
import type { IUserInfo } from "@/types/user";

const userInfo = ref<IUserInfo>({} as IUserInfo);

const loadData = async () => {
  const res = await getUserInfoAPI();
  userInfo.value = res.data;
};
onMounted(loadData);
diff
+<div class="user-page" v-if="user">
    <div class="user-page-head">
      <div class="top">
+        <van-image round fit="cover" :src="user.avatar" />
        <div class="name">
+          <p>{{ userInfo.account }}</p>
          <p><van-icon name="edit" /></p>
        </div>
      </div>
      <van-row>
        <van-col span="6">
+          <p>{{ userInfo.collectionNumber }}</p>
          <p>收藏</p>
        </van-col>
        <van-col span="6">
+          <p>{{ userInfo.likeNumber }}</p>
          <p>关注</p>
        </van-col>
        <van-col span="6">
+          <p>{{ userInfo.score }}</p>
          <p>积分</p>
        </van-col>
        <van-col span="6">
+          <p>{{ userInfo.couponNumber }}</p>
          <p>优惠券</p>
        </van-col>
      </van-row>
    </div>
    <div class="user-page-order">
      <div class="head">
        <h3>药品订单</h3>
        <router-link to="/order">全部订单 <van-icon name="arrow" /></router-link>
      </div>
      <van-row>
        <van-col span="6">
+          <van-badge :content="userInfo.orderInfo?.paidNumber || ''">
            <cp-icon name="user-paid" />
+          </van-badge>
          <p>待付款</p>
        </van-col>
        <van-col span="6">
+          <van-badge :content="userInfo.orderInfo?.shippedNumber || ''">
            <cp-icon name="user-shipped" />
+          </van-badge>
          <p>待发货</p>
        </van-col>
        <van-col span="6">
+          <van-badge :content="userInfo.orderInfo?.receivedNumber || ''">
            <cp-icon name="user-received" />
+          </van-badge>
          <p>待收货</p>
        </van-col>
        <van-col span="6">
+          <van-badge :content="userInfo.orderInfo?.finishedNumber || ''">
            <cp-icon name="user-finished" />
+          </van-badge>
          <p>已完成</p>
        </van-col>
      </van-row>
    </div>
  </div>

个人中心-快捷工具

实现:快捷工具栏目渲染

步骤:

  • 准备初始化结构
  • 准备初始化数据
  • 遍历生成多个 cell 组件

代码: 1)准备初始化结构

html
<div class="user-page-group">
  <h3>快捷工具</h3>
  <van-cell title="标题" is-link :border="false">
    <template #icon><cp-icon name="user-tool-01" /></template>
  </van-cell>
  <van-cell title="标题" is-link :border="false">
    <template #icon><cp-icon name="user-tool-01" /></template>
  </van-cell>
</div>
scss
// 分组
&-group {
  background-color: #fff;
  border-radius: 8px;
  overflow: hidden;
  h3 {
    padding-left: 16px;
    line-height: 44px;
  }
  .van-cell {
    align-items: center;
  }
  .cp-icon {
    font-size: 17px;
    margin-right: 10px;
  }
}

2)准备初始化数据

ts
const router = useRouter();
const tools = [
  { label: "我的问诊", path: "/user/consult" },
  { label: "我的处方", path: "/" },
  { label: "家庭档案", path: "/user/patient" },
  { label: "地址管理", path: "/user/address" },
  { label: "我的评价", path: "/" },
  { label: "官方客服", path: "/" },
  { label: "设置", path: "/" },
];

3)遍历

html
<div class="user-page-group">
  <h3>快捷工具</h3>
  <van-cell
    :title="item.label"
    is-link
    :to="item.path"
    :border="false"
    v-for="(item, i) in tools"
    :key="i"
    @click="router.push(item.path)"
  >
    <template #icon><cp-icon :name="`user-tool-0${i + 1}`" /></template>
  </van-cell>
</div>

个人中心-退出登录

实现:退出功能

步骤:

  • 准备按钮
  • 实现退出
    • 确认框
    • 清除 token
    • 跳转登录

代码:

1)准备按钮

html
<a class="logout" href="javascript:;">退出登录</a>
scss
.logout {
  display: block;
  margin: 20px auto;
  width: 100px;
  text-align: center;
  color: var(--cp-price);
}

2)实现退出

ts
import { useUserStore } from "@/stores";

// ... 省略 ...

const router = useRouter();
const store = useUserStore();

const loginOut = async () => {
  await showConfirmDialog({
    title: "温馨提示",
    message: "您确认要退出优医问诊吗?",
    cancelButtonText: "取消",
    confirmButtonText: "确认",
  });
  store.delUser();
  router.push("/login");
};

权限控制-页面访问权限

实现:需要登录的页面,需要判断是否有token

步骤:

  1. 定义白名单
  2. 判断,如果没有登录且不在白名单内,跳转到登录

代码

ts
// 访问权限控制
router.beforeEach((to) => {
  // 1. 定义白名单
  const whiteList = ["/login"];
  // 用户仓库
  const store = useUserStore();

  // 2. 如果没有登录且不在白名单内,去登录
  if (!store.userInfo?.token && !whiteList.includes(to.path)) return "/login";
  // 否则不做任何处理
});

提问:

  • 如果 /register 也不需要登录,写哪里?
    • const wihteList = ['/login', 'register']

权限控制-401 和统一异常提示

utils/request.ts

diff
//  ... 省略其它代码
+ import { useUserStore } from '@/stores'
+ import { showFailToast } from 'vant'
+	import router from '@/router'


//  响应拦截器
request.interceptors.response.use(
  function (res) {
+    // 后台约定,响应成功,但是code不是10000,是业务逻辑失败
+    if (res.data?.code !== 10000) {
+      showFailToast(res.data?.message)
+      return Promise.reject(res.data)
+    }
    // 业务逻辑成功,返回响应数据,作为axios成功的结果
    return res.data
  },
  function (err) {
+    if (err.response.status === 401) {
+			 // 提示用户
+      showToast('登录超时, 请重新登录')
+      // 删除用户信息
+      const store = useUserStore()
+      store.delUserInfo()
+      // 跳转登录,带上接口失效所在页面的地址,登录完成后回跳使用
+      router.push({
+        path: '/login',
+        query: { returnPath: router.currentRoute.value.fullPath }
+      })
+    }
    return Promise.reject(err)
  }
)

export default request

Login/index.vue

diff
const onLogin = async () => {
  if (!agree.value) return showToast('请勾选我已同意')
  const res = isPass.value
    ? await loginAPI(mobile.value, password.value)
    : await loginByCodeAPI(mobile.value, code.value)
  store.saveUser(res.data)
  showSuccessToast('登陆成功')

-	 router.push('/')
+  const returnPath = route.query.returnPath
+  console.log('returnPath  ----->  ', returnPath)
+  if (returnPath) {
+    router.push(returnPath as string)
+  } else {
+    router.push('/')
+  }
}

家庭档案

image-20220808184827626

家庭档案-静态结构

实现:路由的配置与组件基础布局

  • src/views/User/PatientPage.vue
vue
<script setup lang="ts"></script>

<template>
  <div class="patient-page">
    <cp-nav-bar title="家庭档案" />
    <div class="patient-list">
      <div class="patient-item">
        <div class="info">
          <span class="name">李富贵</span>
          <span class="id">321***********6164</span>
          <span></span>
          <span>32岁</span>
        </div>
        <div class="icon"><cp-icon name="user-edit" /></div>
        <div class="tag">默认</div>
      </div>
      <div class="patient-item">
        <div class="info">
          <span class="name">李富贵</span>
          <span class="id">321***********6164</span>
          <span></span>
          <span>32岁</span>
        </div>
        <div class="icon"><cp-icon name="user-edit" /></div>
      </div>
      <div class="patient-add">
        <cp-icon name="user-add" />
        <p>添加患者</p>
      </div>
      <div class="patient-tip">最多可添加 6 人</div>
    </div>
  </div>
</template>

<style lang="scss" scoped>
.patient-page {
  padding: 0 0 80px;
}
.patient-list {
  padding: 15px;
}
.patient-item {
  display: flex;
  align-items: center;
  padding: 15px;
  background-color: var(--cp-bg);
  border-radius: 8px;
  margin-bottom: 15px;
  position: relative;
  border: 1px solid var(--cp-bg);
  transition: all 0.3s;
  overflow: hidden;
  .info {
    display: flex;
    flex-wrap: wrap;
    flex: 1;
    span {
      color: var(--cp-tip);
      margin-right: 20px;
      line-height: 30px;
      &.name {
        font-size: 16px;
        color: var(--cp-text1);
        width: 80px;
        margin-right: 0;
      }
      &.id {
        color: var(--cp-text2);
        width: 180px;
      }
    }
  }
  .icon {
    color: var(--cp-tag);
    width: 20px;
    text-align: center;
  }
  .tag {
    position: absolute;
    right: 60px;
    top: 21px;
    width: 30px;
    height: 16px;
    font-size: 10px;
    color: #fff;
    background-color: var(--cp-primary);
    border-radius: 2px;
    display: flex;
    justify-content: center;
    align-items: center;
  }
  &.selected {
    border-color: var(--cp-primary);
    background-color: var(--cp-plain);
    .icon {
      color: var(--cp-primary);
    }
  }
}
.patient-add {
  background-color: var(--cp-bg);
  color: var(--cp-primary);
  text-align: center;
  padding: 15px 0;
  border-radius: 8px;
  .cp-icon {
    font-size: 24px;
  }
}
.patient-tip {
  color: var(--cp-tag);
  padding: 12px 0;
}
</style>
  • 路由 router/index.ts
ts
{
      path: '/user/patient',
      component: () => import('@/views/User/PatientPage.vue'),
      meta: { title: '家庭档案' }
    }

注意是一级路由

家庭档案-渲染页面

实现:患者的查询操作业务逻辑

步骤:

  • 定义 api 函数,以及对应类型
  • 挂载后,发送请求
  • 插值,渲染界面

代码:

1)定义 api 函数,以及对应类型

  • 定义类型 types/user.d.ts,API 文档复制类型
ts
export interface Patient {
  // 代码省略
}
  • 定义 API: services/user.ts
ts
/** 获取患者列表 */
export const getPatientListAPI = () => {
  return request({ url: "/patient/mylist" });
};

2)实现查询患者业务 User/PatientPage.vue

  • 调用 API,保存数据
ts
import { getPatientListAPI } from "@/services/user";
import type { Patient } from "@/types/user";
import { ref, onMounted } from "vue";

// 2. 调用API,保存数据
const list = ref<Patient[]>([]);
const loadData = async () => {
  const res = await getPatientListAPI();
  list.value = res.data;
};
onMounted(() => {
  loadData();
});
  • 插值,渲染界面
html
<div class="patient-item" v-for="item in list" :key="item.id">
  <div class="info">
    <span class="name">{{ item.name }}</span>
    <span class="id"
      >{{ item.idCard.replace(/^(\d{6})\d{8}(\d{4})$/, '\$1******\$2') }}</span
    >
    <span>{{ item.genderValue }}</span>
    <span>{{ item.age }}岁</span>
  </div>
  <div class="icon"><cp-icon name="user-edit" /></div>
  <div class="tag" v-if="item.defaultFlag === 1">默认</div>
</div>
<div class="patient-add" v-if="list.length < 6"></div>

身份证脱敏处理:(\d{6})\d{8}(\d{4})

  • 匹配第一个$1 ^(.{6})
  • 匹配第二个$2 (.{4})$

添加患者-弹出层显示隐藏

实现:使用 van-popup 完成侧边栏效果

需求:

  • 使用 van-popup 组件,实现显示隐藏

代码:

ts
// 1. 定义布尔值控制显示与隐藏
const isShow = ref(false);
  1. 使用 van-popup 组件,实现显示隐藏 , 并添加样式代码

User/PatientPage.vue

html
<!-- 2. 使用van-popup组件,并添加样式代码 -->
<div class="patient-page">

    //  省略上方代码 
  
    <!-- 🔔🔔 注意:写在div.patient-page内 -->
    <van-popup v-model:show="isShow" position="right">
      <cp-nav-bar title="添加患者" right-text="保存" />
    </van-popup>
  </div>
diff
.patient-page {
  padding: 0 0 80px;
+  ::v-deep() {
+    .van-popup {
+      width: 100%;
+      height: 100%;
+    }
+  }
}
  1. 点击事件,修改 isShow
diff
<div
	class="patient-add"
+	@click="isShow = true"
>
  <cp-icon name="user-add" />
  <p>添加患者</p>
</div>
  1. navbar-自定义返回处理函数
diff
<van-popup v-model:show="isShow" position="right">
	<cp-nav-bar
		title="添加患者"
		right-text="保存"
+		:on-back="() => (isShow = false)"
	/>
</van-popup>

小结:

  • 属性可以传函数吗?
    • 可以
  • popup 组件怎么双向绑定?
    • v-model:show

添加患者-静态结构-收集数据

实现:患者信息录入的表单和绑定数据

需求:

  • 表单静态结构
  • 数据双向绑定
  • 关闭侧边栏、重置表单

代码:User/PatientPage.vue

1)表单静态结构

vue
<script setup lang="ts">
//  省略其它代码

const options = [
  { label: "", value: 1 },
  { label: "", value: 0 },
];
</script>

<template>
  // 省略其它代码
  <van-form autocomplete="off">
    <van-field label="真实姓名" placeholder="请输入真实姓名" />
    <van-field label="身份证号" placeholder="请输入身份证号" />
    <van-field label="性别">
      <!-- 单选按钮组件 -->
      <template #input>
        <cp-radio-btn :options="options"></cp-radio-btn>
      </template>
    </van-field>
    <van-field label="默认就诊人">
      <template #input>
        <van-checkbox round />
      </template>
    </van-field>
  </van-form>
</template>

2)声明数据、绑定数据

ts
type AddPatient = Pick<Patient, "name" | "idCard" | "gender" | "defaultFlag"> & { 		     gender: number 
}

PatientPage.vue

ts
const initFormData: AddPatient = {
  name: "",
  idCard: "",
  gender: 1,
  defaultFlag: 0,
};

// 2. 声明表单初始值,数据数据双向绑定
const formData = ref<AddPatient>({ ...initFormData });
// 默认值需要转换
const defaultFlag = computed({
  get() {
    return formData.value.defaultFlag === 1 ? true : false;
  },
  set(value) {
    formData.value.defaultFlag = value ? 1 : 0;
  },
});

3)与表单元素,双向绑定,代码省略

4)侧边栏关闭时,重置表单初始值

ts
// 4. 关闭弹出层时,还原表单初始值
watch(isShow, (newValue) => {
  if (!newValue) {
    // 还原表单的初始值
    formData.value = { ...initFormData };
  }
});

小结:

  • 为什么使用计算属性 get set 写法?
    • 组件需要的是 布尔 类型,需要通过计算属性转换一下
  • 为什么要解构初始数据?
    • 克隆一份新数据,要不然是同一个对象。

添加患者-表单校验

实现:提交的时候校验表单,身份证需要校验格式

需求:

  • 点击保存按钮进行校验
    • 名字非空,身份证非空
    • 身份证格式,性别需要和填写的一致

代码:

1)名字非空,身份证非空

diff
<cp-nav-bar
        :back="() => (show = false)"
        title="添加患者"
        right-text="保存"
+        @click-right="submit"
      />
ts
const submit = () => {
  if (!patient.value.name) return showToast("请输入真实姓名");
  if (!patient.value.idCard) return showToast("请输入身份证号");
};

2)身份证格式,性别需要和填写的一致

提示:身份证号,7-14位为出生年月日。

提示:身份证号,倒数第二位,奇数为男,偶数为女。

  • 测试号
  • 110101198307212600 - 错误
  • 110101196107145504 - 错误
  • 11010119890512132X - 错误
  • 110101196501023433
  • 110101197806108758
  • 110101198702171378
  • 110101198203195893
  • 如有雷同纯属巧合,可删除。

身份证验证

bash
pnpm i id-validator

由于是比较老的库,没有提供类型,自己定义类型 types/id-validator.d.ts

ts
declare module "id-validator" {
  // 默认导出的,class是es6的类语法,对应 es5 的构造函数
  export default class {
    // es6 类中的方法,对应 es5 的原型方法
    isValid(id: string): boolean;
    getInfo(id: string): {
      sex: number;
    };
    makeID(): string;
  }
}

User/PatientPage.vue 使用库进行校验

ts
import Validator from "id-validator";
diff
const submit = () => {
  if (!patient.value.name) return showToast('请输入真实姓名')
  if (!patient.value.idCard) return showToast('请输入身份证号')

+  const validate = new Validator()
+  if (!validate.isValid(patient.value.idCard)) return showToast('身份证格式错误')
+  const { sex } = validate.getInfo(patient.value.idCard)
+  if (patient.value.gender !== sex) return showToast('性别和身份证不符')
}

小结:

  • 模块默认返回是构造函数怎么写类型声明文件?
    • declare module 'id-validate' { export default class {} }

添加患者-实现新增

实现:患者的添加操作业务逻辑

步骤:

  • 定义 api 函数
  • 调用 API,提示用户、更新界面

代码:

1)定义 api 函数

ts
import type { AddPatient } from "@/types/user";

/** 添加患者 */
export const addPatientAPI = (patient: AddPatient) => {
  return request({ url: "/patient/add", method: "POST", data: patient });
};

2)调用 API,提示用户、更新界面

diff
+import { addPatientAPI } from '@/services/user'

// ... 省略 ...

const submit = async () => {
  if (!patient.value.name) return showToast('请输入真实姓名')
  if (!patient.value.idCard) return showToast('请输入身份证号')
  const validate = new Validator()
  if (!validate.isValid(patient.value.idCard)) return showToast('身份证格式错误')
  const { sex } = validate.getInfo(patient.value.idCard)
  if (patient.value.gender !== sex) return showToast('性别和身份证不符')

 // 添加
+  await addPatientAPI(formData.value)
+  isShow.value = false
+  loadData()
+  showSuccessToast('添加成功')
}

家庭档案-编辑患者

实现:患者的编辑操作业务逻辑

步骤:

  • 扩展类型 IEditPatient
  • 打开弹出层
  • 回填表单信息
  • 编写 api 函数
  • 复用提交函数,根据 id 区分:新增、编辑

代码:

  • 扩展类型

user.d.ts

ts
export type IAddPatient = Pick<
  Patient,
  "name" | "idCard" | "gender" | "defaultFlag"
> & { id?: string };
  • 打开弹出层
  • 回填表单信息
html
<div @click="showEdit(item)" class="icon"><cp-icon name="user-edit" /></div>
ts
const showEdit = (item: Patient) => {
  isShow.value = true;
  const { id, gender, name, idCard, defaultFlag } = item;
  formData.value = { id, gender, name, idCard, defaultFlag };
};
diff
<cp-nav-bar
        :back="() => (show = false)"
+        :title="formData.id ? '编辑患者' : '添加患者'"
        right-text="保存"
        @click-right="submit"
      />
  • api 函数
ts
/** 编辑患者 */
export const editPatientAPI = (patient: IAddPatient) => {
  return request({ url: "/patient/update", method: "PUT", data: patient });
};
  • 复用提交函数,根据 id 区分:新增、编辑
diff
const submit = async () => {
 // ... 省略其它代码

-  // 添加
-  await addPatientAPI(formData.value)
-  isShow.value = false
-  loadData()
-  showSuccessToast('添加成功')

  // 添加
+  formData.value.id
+    ? await editPatientAPI(formData.value)
+    : await addPatientAPI(formData.value)

+  isShow.value = false
+  loadData()
+  showSuccessToast(formData.value.id ? '编辑成功' : '添加成功')
}

家庭档案-删除患者

实现:患者的删除操作业务逻辑

步骤:

  • 准备按钮和样式
  • 定义 API 函数
  • 点击事件,二次确认
  • 调用 API
  • 提示用户

代码:

  • 使用 ActionBar,准备按钮 和样式
diff
</van-form>
+      <van-action-bar  v-if="formData.id">
+        <van-action-bar-button>删除</van-action-bar-button>
+      </van-action-bar>
    </van-popup>
scss
// 底部操作栏
.van-action-bar {
  padding: 0 10px;
  margin-bottom: 10px;
  .van-button {
    color: var(--cp-price);
    background-color: var(--cp-bg);
  }
}
  • 定义 API
ts
/** 根据id,删除患者 */
export const delPatientAPI = (id: string) => {
  return request({ url: `/patient/del/${id}`, method: "DELETE" });
};
  • 点击事件
diff
<van-action-bar v-if="formData.id">
-        <van-action-bar-button>删除</van-action-bar-button>
+        <van-action-bar-button @click="delById">删除</van-action-bar-button>
      </van-action-bar>
  • 二次确认提示
  • 调用 API,提示用户
ts
import { delPatientAPI } from "@/services/user";
// ... 省略 ...
const delById = async () => {
  await showConfirmDialog({
    title: "温馨提示",
    message: `您确认要删除 ${formData.value.name} 患者信息吗 ?`,
    cancelButtonText: "取消",
    confirmButtonText: "确认",
  });
  await delPatientAPI(formData.value.id as string);
  isShow.value = false;
  loadData();
  showSuccessToast("删除成功");
};

Released under the MIT License.