Skip to content
On this page

极速问诊模块

极速问诊-需求分析

理解:极速问诊阶段流程分析

image-20220813195713860

极速问诊阶段:

  1. 极速问诊(记录-问诊类型)

  2. 三甲图文问诊 或 普通图文问诊(记录-极速问诊类型)

  3. 选择科室(记录-疾病科室)

  4. 描述病情(记录-症状详情、时间、是否就诊过、图片)

  5. 选择患者(记录-患者ID)

  6. 支付问诊费

所有流程走完才能组合成完整的问诊记录,而且是不同的页面采集数据,这个实现需要 pinia

还有一个发现:接口数据

  • type 就诊类型: 1找医生 2极速问诊 3开药问诊 type:1|2|3
  • illnessType 极速问诊类型:0普通 1三甲 illnessType: 0|1

提问:

  • 刚刚看到 1 2 3 的时候你能记得他们代表什么意思吗?
    • 不清楚,对于数字字面量类型的联合类型语义差,建议使用 枚举

极速问诊-静态结构

image-20220815133500165

完成选择三甲还是普通问诊页面,点击后记录对应的类型,跳转到选择科室路由

步骤:

  • 路由和组件
  • 编写页面布局
  • 测试路由跳转

代码:

  1. 组件静态结构:直接复制

src/views/Consult/ConsultFast.vue

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

<template>
  <div class="consult-fast-page">
    <cp-nav-bar title="极速问诊" right-text="问诊记录" />
    <div class="fast-logo">
      <img class="img" src="@/assets/consult-fast.png" alt="" />
      <p class="text"><span>20s</span> 快速匹配专业医生</p>
    </div>
    <div class="fast-type">
      <router-link to="/consult/dep" class="item">
        <cp-icon class="pic" name="consult-doctor"></cp-icon>
        <div class="info">
          <p>三甲图文问诊</p>
          <p>三甲主治及以上级别医生</p>
        </div>
        <van-icon name="arrow"></van-icon>
      </router-link>
      <router-link to="/consult/dep" class="item">
        <cp-icon class="pic" name="consult-message"></cp-icon>
        <div class="info">
          <p>普通图文问诊</p>
          <p>二甲主治及以上级别医生</p>
        </div>
        <van-icon name="arrow"></van-icon>
      </router-link>
    </div>
  </div>
</template>

<style lang="scss" scoped>
.consult-fast-page {
  .fast-logo {
    padding: 30px 0;
    text-align: center;
    .img {
      width: 240px;
    }
    .text {
      font-size: 16px;
      margin-top: 10px;
      > span {
        color: var(--cp-primary);
      }
    }
  }
  .fast-type {
    padding: 15px;
    .item {
      display: flex;
      padding: 16px;
      border-radius: 4px;
      align-items: center;
      margin-bottom: 16px;
      border: 0.5px solid var(--cp-line);
    }
    .pic {
      width: 40px;
      height: 40px;
    }
    .info {
      margin-left: 12px;
      flex: 1;
      > p:first-child {
        font-size: 16px;
        color: var(--cp-text1);
        margin-bottom: 4px;
      }
      > p:last-child {
        font-size: 13px;
        color: var(--cp-tag);
      }
    }
    .van-icon {
      color: var(--cp-tip);
    }
  }
}
</style>
  1. 配置路由:router/index.ts
ts
{
      path: '/consult/fast',
      component: () => import('@/views/Consult/ConsultFast.vue'),
      meta: { title: '极速问诊' }
    }

极速问诊-选择科室-静态结构

image-20220824154050319

实现:路由与组件,和基础结构

步骤:

  • 组件与路由
  • 一级科室使用 sidebar 组件
  • 二级科室绘制

代码:

1)组件静态结构和样式

src/views/Consult/ConsultDep.vue

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

<template>
  <div class="consult-dep-page">
    <cp-nav-bar title="选择科室" />
    <div class="wrapper">
      <van-sidebar >
        <van-sidebar-item title="内科" />
        <van-sidebar-item title="外科" />
        <van-sidebar-item title="皮肤科" />
        <van-sidebar-item title="骨科" />
      </van-sidebar>
      <div class="sub-dep">
        <router-link to="/consult/illness">科室一</router-link>
        <router-link to="/consult/illness">科室二</router-link>
        <router-link to="/consult/illness">科室三</router-link>
      </div>
    </div>
  </div>
</template>

<style lang="scss" scoped>
.wrapper {
  height: calc(100vh - 46px);
  overflow: hidden;
  display: flex;
  .sub-dep {
    flex: 1;
    height: 100%;
    overflow-y: auto;
    > a {
      display: block;
      padding: 14px 30px;
      color: var(--cp-dark);
    }
  }
}
.van-sidebar {
  width: 114px;
  &-item {
    padding: 14px;
    color: var(--cp-tag);
    &--select {
      color: var(--cp-main);
      font-weight: normal;
      &::before {
        display: none;
      }
    }
  }
}
</style>
  1. 配置路由router/index.ts
ts
{
      path: '/consult/dep',
      component: () => import('@/views/Consult/ConsultDep.vue'),
      meta: { title: '选择科室' }
    }

极速问诊-创建问诊记录仓库

实现:病情描述仓库的定义,实现问诊记录分步修改

步骤:

  • 定义仓库,备用
  • 导出仓库
  • 首页创建store

代码:

1)定义仓库 src/stores/consult.ts

ts
import { defineStore } from 'pinia'

export const useConsultStore = defineStore(
  'cp-consult',
  () => {
    
  },    
  { persist: true }
)
  1. 导出仓库 stores/index.ts
ts
export * from './modules/consult'

3)首页创建store views/Home/index.vue

ts
// 省略其它代码
import { useConsultStore } from '@/stores'

const store = useConsultStore()

极速问诊-记录问诊类型

实现:点击问诊类型,pinia中保存问诊类型

步骤:

  1. 定义类型
  2. pinia中声明变量type
  3. 点击事件,修改
  4. 首页,点击极速问诊,记录问诊类型 views/Home/index.vue
  5. 极速问诊页面,点击问诊类型,记录问诊类型 views/ConsultDep.vue

代码:

  1. 定义类型,查看接口得知,问诊类型:1找医生、2极速问诊、3开药问诊

注意:枚举类型需要在 ts 文件中,因为枚举会编译成 js 代码

src/enums/index.ts

ts
/** 问诊类型 */
export enum ConsultType {
  /** 找医生 */
  Doctor = 1,
  /** 快速问诊 */
  Fast = 2,
  /** 开药问诊 */
  Medication = 3
}

/** 问诊时间,以1自增可以省略 */
export enum IllnessTime {
  /** 一周内 */
  Week = 1,
  /** 一月内 */
  Month,
  /** 半年内 */
  HalfYear,
  /** 半年以上 */
  More
}
  1. 定义采集数据的类型

types/consult.d.ts

ts
import { ConsultType, IllnessTime } from '@/enums'

/** 图片列表 */
export type Image = {
  /** 图片ID */
  id: string
  /** 图片地址 */
  url: string
}
/** 问诊记录 */
export type Consult = {
  /** 问诊记录ID */
  id: string
  /** 问诊类型 */
  type: ConsultType
  /** 快速问诊类型,0 普通 1 三甲 */
  illnessType: 0 | 1
  /** 科室ID */
  depId: string
  /** 疾病描述 */
  illnessDesc: string
  /** 疾病持续时间 */
  illnessTime: IllnessTime
  /** 是否就诊过,0 未就诊过  1 就诊过 */
  consultFlag: 0 | 1
  /** 图片数组 */
  pictures: Image[]
  /** 患者ID */
  patientId: string
  /** 优惠券ID */
  couponId: string
}

/** 问诊记录-全部可选 */
export type PartialConsult = Partial<Consult>
// 💥💥 全部可选是因为信息是一点一点累加上去的
  1. pinia中声明变量consultType

src/stores/modules/consult.ts

ts
import { defineStore } from 'pinia'
import { ConsultType } from '@/enums'
import type { PartialConsult } from '@/types/consult'
import { ref } from 'vue'

export const useConsultStore = defineStore(
  'cp-consult',
  () => {
    // 💥💥 全部可选是因为信息是一点一点累加上去的
    /** 用以收集用户选择的对象 */
    const consult = ref<PartialConsult>({})
    // 设置问诊类型
    const setType = (type: ConsultType) => (consult.value.type = type)
    // 设置极速问诊类型
    const setIllnessType = (type: 0 | 1) => (consult.value.illnessType = type)
    // 设置科室
    const setDep = (id: string) => (consult.value.depId = id)
   
    return { consult, setType, setIllnessType, setDep }
  },
  {
    persist: true
  }
)
  1. 首页,点击极速问诊,记录问诊类型 views/Home/index.vue
diff
<router-link
    to="/consult/fast"
+   @click="store.setType(ConsultType.Fast)"
    class="nav"
  >
    <cp-icon name="home-graphic"></cp-icon>
    <p class="title">极速问诊</p>
    <p class="desc">20s医生极速回复</p>
  </router-link>
  1. 极速问诊页面,点击问诊类型,记录问诊类型 views/ConsultDep.vue
diff
<script setup lang="ts">
 // 省略其它代码
 
+ import { useConsultStore } from '@/stores'
+ const store = useConsultStore()

</script>

<template>
  <div class="fast-type">
    <router-link
      to="/consult/dep"
      class="item"
+     @click="store.setIllnessType(1)"
    >
    
       ... 省略 ... 
       
    </router-link>
    <router-link
      to="/consult/dep"
      class="item"
+     @click="store.setIllnessType(0)"
    >
    
      ... 省略 ... 
      
    </router-link>
  </div>
</template>

极速问诊-选择一级科室

实现:科室切换以及跳转到病情描述

步骤:

  • 编写科室需要的类型

  • 准备API函数

  • 挂载后,调用API函数

  • 声明数据,保存数据,列表渲染

代码:

1)编写科室需要的类型 types/consult.d.ts

ts
export interface TopDep {
  /**
   * 子级集合
   */
  child?: Child[]
  /**
   * 科室id--一级科室
   */
  id?: string
  /**
   * 科室名称
   */
  name?: string
}

export interface SubDep {
  /**
   * 科室的图标
   */
  avatar: Avatar
  /**
   * 子级id
   */
  id: string
  /**
   * 子级name
   */
  name: string
}

2)准备API函数 services/consult.ts

diff
import type {
  DoctorPage,
  FollowType,
  KnowledgePage,
  KnowledgeParams,
  PageParams,
+  TopDep
} from '@/types/consult'

+ export const getAllDepAPI = () => request({ url: '/dep/all' })

3)挂载后,调用API函数Consult/ConsultDep.vue

ts
import { getAllDepAPI } from '@/services/consult'
import type { TopDep } from '@/types/consult'
import { onMounted, ref } from 'vue

const allDep = ref<TopDep[]>([])

const loadData = async () => {
  const res = await getAllDepAPI()
  allDep.value = res.data
}

onMounted(loadData)
  1. 列表渲染
html
<van-sidebar >
        <van-sidebar-item :title="top.name" v-for="top in allDep" :key="top.id" />
      </van-sidebar>
  1. v-model收集选中科室
ts
/** 选中的一级科室下标 */
const topSelectIndex = ref(0)
diff
-     <van-sidebar >
+			<van-sidebar v-model="topSelectIndex">
        <van-sidebar-item :title="top.name" v-for="top in allDep" :key="top.id" />
      </van-sidebar>

极速问诊-选择二级科室

步骤:

  1. 计算属性,计算二级科室列表subDep
  2. 列表渲染
  3. 点击事件,保存选中二级科室到pinia中

代码:

  1. 计算属性,计算二级科室列表subDep
ts
import { computed, onMounted, ref } from 'vue'

// 二级科室,注意:组件初始化没有数据 child 可能拿不到
const subDep = computed(() => allDep.value[topSelectIndex.value]?.child)
  1. 列表渲染
html
<div class="sub-dep">
        <router-link to="/consult/illness" v-for="sub in subDep" :key="sub.id">
          {{ sub.name }}
        </router-link>
      </div>
  1. 点击事件,保存选中二级科室到pinia中
ts
import { useConsultStore } from '@/stores'

const store = useConsultStore()
diff
<router-link
          to="/consult/illness"
          v-for="sub in subDep"
          :key="sub.id"
+          @click="store.setDep(sub.id)"
        >
          {{ sub.name }}
        </router-link>

病情描述-静态结构

image-20220824154141532

实现:路由和组件以及页面的基础布局(医生提示,描述,症状时间,是否已就诊)

代码:

1)路由与组件

src/views/Consult/ConsultIllness.vue

vue
<script setup lang="ts">
import { IllnessTime } from '@/enums'

const timeOptions = [
  { label: '一周内', value: IllnessTime.Week },
  { label: '一月内', value: IllnessTime.Month },
  { label: '半年内', value: IllnessTime.HalfYear },
  { label: '大于半年', value: IllnessTime.More }
]
const flagOptions = [
  { label: '就诊过', value: 0 },
  { label: '没就诊过', value: 1 }
]
</script>

<template>
  <div class="consult-illness-page">
    <cp-nav-bar title="图文问诊" />
  </div>

  <!-- 医生提示 -->
  <div class="illness-tip van-hairline--bottom">
    <img class="img" src="@/assets/avatar-doctor.svg" />
    <div class="info">
      <p class="tit">在线医生</p>
      <p class="tip">
        请描述你的疾病或症状、是否用药、就诊经历,需要我听过什么样的帮助
      </p>
      <p class="safe">
        <cp-icon name="consult-safe" /><span>内容仅医生可见</span>
      </p>
    </div>
  </div>

  <!-- 表单 -->
  <div class="illness-form">
    <van-field
      type="textarea"
      rows="3"
      placeholder="请详细描述您的病情,病情描述不能为空"
    />
    <div class="item">
      <p>本次患病多久了?</p>
      <cp-radio-btn :options="timeOptions" />
    </div>
    <div class="item">
      <p>此次病情是否去医院就诊过?</p>
      <cp-radio-btn :options="flagOptions" />
    </div>
  </div>
</template>

<style lang="scss" scoped>

.illness-tip {
  display: flex;
  padding: 15px;
  .img {
    width: 52px;
    height: 52px;
    border-radius: 4px;
    overflow: hidden;
    margin-top: 10px;
  }
  .info {
    flex: 1;
    padding-left: 12px;
    .tit {
      font-size: 16px;
      margin-bottom: 5px;
    }
    .tip {
      padding: 12px;
      background: var(--cp-bg);
      color: var(--cp-text3);
      font-size: 13px;
      margin-bottom: 10px;
    }
    .safe {
      font-size: 10px;
      color: var(--cp-text3);
      display: flex;
      align-items: center;
      .cp-icon {
        font-size: 12px;
        margin-right: 2px;
      }
    }
  }
}

.illness-form {
  padding: 15px;
  .van-field {
    padding: 0;
    &::after {
      border-bottom: none;
    }
  }
  .item {
    > p {
      color: var(--cp-text3);
      padding: 15px 0;
    }
  }
}

.illness-img {
  padding-top: 16px;
  margin-bottom: 40px;
  display: flex;
  align-items: center;
  .tip {
    font-size: 12px;
    color: var(--cp-tip);
  }
  ::v-deep() {
    .van-uploader {
      &__preview {
        &-delete {
          left: -6px;
          top: -6px;
          border-radius: 50%;
          background-color: var(--cp-primary);
          width: 20px;
          height: 20px;
          &-icon {
            transform: scale(0.9) translate(-22%, 22%);
          }
        }
        &-image {
          border-radius: 8px;
          overflow: hidden;
        }
      }
      &__upload {
        border-radius: 8px;
      }
      &__upload-icon {
        color: var(--cp-text3);
      }
    }
  }
}

.van-button {
  font-size: 16px;
  margin-bottom: 30px;
  &.disabled {
    opacity: 1;
    background: #fafafa;
    color: #d9dbde;
    border: #fafafa;
  }
}
</style>
  1. 配置路由
typescript
{
      path: '/consult/illness',
      component: () => import('@/views/Consult/ConsultIllness.vue'),
      meta: { title: '病情描述' }
    },

病情描述-采集表单数据

目标:v-model采集表单数据

步骤:

  1. 定义表单数据类型
  2. 声明变量
  3. v-model绑定表单元素

代码:

  1. 准备定义表单数据类型

types/consult.d.ts

ts
export type ConsultIllness = Pick<
  PartialConsult,
  'illnessDesc' | 'illnessTime' | 'consultFlag' | 'pictures'
>

2.声明变量formData:stores/modules/consult.ts

ts
import type { PartialConsult, ConsultIllness } from '@/types/consult'
import { ref } from 'vue'

const formData = ref<ConsultIllness>({
  illnessDesc: '',
  illnessTime: undefined,
  consultFlag: undefined,
  pictures: []
})
  1. 双向绑定
diff
<template>
+ // ... 省略其它代码

  <!-- 表单 -->
  <div class="illness-form">
    <van-field
      type="textarea"
      rows="3"
      placeholder="请详细描述您的病情,病情描述不能为空"
+      v-model="formData.illnessDesc"
    />
    <div class="item">
      <p>本次患病多久了?</p>
      <cp-radio-btn 
      	:options="timeOptions"
+        v-model="formData.illnessTime"
      />
    </div>
    <div class="item">
      <p>此次病情是否去医院就诊过?</p>
      <cp-radio-btn 
      	:options="flagOptions" 
+      	v-model="formData.consultFlag" 
      />
    </div>
  </div>
</template>

病情描述-图片上传-组件

实现:使用 van-upload 组件,进行样式和功能配置

步骤:

  • 组件基础结构
  • 配置文字和图标
  • 配置最多数量和最大体积
  • 支持双向数据绑定,支持选择图片后触发函数,支持点击删除事件函数

代码:

  1. 组件基础结构 Consult/ConsultIllness.vue
html
<!-- 图片上传区域 -->
<div class="illness-img">
  <van-uploader />
  <p class="tip" >上传内容仅医生可见,最多9张图,最大5MB</p>
</div>
  1. 支持双向数据绑定,支持选择图片后触发函数,支持点击删除事件函数
diff
<script setup lang="ts">
+ import type { UploaderFileListItem } from 'vant'

+ const fileList = ref<UploaderFileListItem[]>([])
</>

<template>
	<van-uploader
+     v-model="fileList"
    />
    <p 
    	class="tip" 
+    	v-show="!fileList.length"
    >
     上传内容仅医生可见,最多9张图,最大5MB
    </p>
</template>
  1. 配置文字和图标
diff
<van-uploader
        	v-model="fileList"
+         upload-icon="photo-o"
+         upload-text="上传图片"
        />
  1. 配置最多数量和最大体积
diff
<van-uploader
        	v-model="fileList"
+         max-count="9"
+         :max-size="5 * 1024 * 1024"
          upload-icon="photo-o"
          upload-text="上传图片"
        />

小结:

  • fileList 是配置组件使用的,同步 form 中的 pictures

病情描述-实现图片上传和删除

实现:上传图片与删除图片功能

步骤:

  • 定义 api 函数
  • 实现上传
  • 实现删除

代码:

1)定义 api 函数 services/consult.ts

ts
/** 上传图片API */
export const uploadImageAPI = (file: File) => {
  const fd = new FormData()
  fd.append('file', file)
  return request({ url: '/upload', method: 'post', data: fd })
}

2)实现上传 Consult/ConsultIllness.vue

ts
import { uploadImageAPI } from '@/services/consult'
ts
const onAfterRead: UploaderAfterRead = (item) => {
  if (Array.isArray(item)) return
  if (!item.file) return
  // 开始上传
  item.status = 'uploading'
  item.message = '上传中...'
  uploadImageAPI(item.file)
    .then((res) => {
      item.status = 'done'
      item.message = undefined
      item.url = res.data.url
      formData.value.pictures?.push(res.data)
    })
    .catch(() => {
      item.status = 'failed'
      item.message = '上传失败'
    })
}
diff
<!-- 图片上传区域 -->
    <div class="illness-img">
      <van-uploader
    		...省略其它代码
+        @onAfterRead="onAfterRead"
      />
      ...省略其它代码
    </div>

3)实现删除

ts
const onDelete = (item: UploaderFileListItem) => {
  form.value.pictures = form.value.pictures?.filter((pic) => pic.url !== item.url)
}
diff
<!-- 图片上传区域 -->
    <div class="illness-img">
      <van-uploader
    		...省略其它代码
+       @delete="onDelete"
      />
      ...省略其它代码
    </div>

小结:

  • 给 item 加上 url 是为了删除可以根据 url 进行删除

病情描述-保存数据

实现:按钮点亮,提交校验,保存数据,跳转选择患者

1)按钮解除禁用

html
<van-button :class="{disabled: true}" type="primary" block round>下一步</van-button>
ts
import { computed, ref } from 'vue'
// ... 省略 ...
const disabled = computed(
  // illnessDesc consultFlag illnessTime均为必选
  () =>
    !formData.value.illnessDesc ||
    formData.value.illnessTime === undefined ||
    formData.value.consultFlag === undefined
)

2)提交校验 保存数据,跳转选择患者

ts
const onNext = () => {
  if (!formData.value.illnessDesc) return showToast('请输入病情描述')
  if (formData.value.illnessTime === undefined) return showToast('请选择症状持续时间')
  if (formData.value.consultFlag === undefined) return showToast('请选择是否已经就诊')
}
diff
<van-button 
	...省略其它代码	
+ @click="onNext"
>
	下一步
</van-button>
  1. 保存数据,跳转选择问诊信息:stores/modules/consult.ts
  • 扩展pinia仓库,保存病情描述
ts
export const useConsultStore = defineStore(
  'cp-consult',
  () => {
		// ... 省略其它代码 ...

    /** 设置病情描述 */
    const setIllness = (illness: ConsultIllness) => {
      consult.value.illnessDesc = illness.illnessDesc
      consult.value.illnessTime = illness.illnessTime
      consult.value.consultFlag = illness.consultFlag
      consult.value.pictures = illness.pictures
    }

    return { 
     // ... 省略其它代码 ...
      setIllness 
    }
  },
  // ... 省略其它代码 ...
)
  • 保存数据,跳转路由
ts
const store = useConsultStore()
const router = useRouter()

const onNext = () => {
   // ... 省略其它代码 ...
  
  store.setIllness(formData.value)
  // 跳转档案管理(复用),需要根据 isSelect 实现选择功能
  router.push('/user/patient?isSelect=1')
}

病情描述-回显数据

实现:进入页面时候提示用户是否回显之前填写的病情描述信息

1)进入页面,如果有记录数据,弹出确认框

ts
// 回显数据
onMounted(() => {
  if (store.consult.illnessDesc) {
    showConfirmDialog({
      title: '温馨提示',
      message: '是否恢复您之前填写的病情信息呢?',
      confirmButtonColor: 'var(--cp-primary)'
    }).then(() => {
      // 确认
    })
  }
})

2)回显数据

从 store 拿出记录的数据

ts
.then(() => {
      // 确认
      const { illnessDesc, illnessTime, consultFlag, pictures } = store.consult
      form.value = { illnessDesc, illnessTime, consultFlag, pictures }
      // 图片回显
      fileList.value = pictures || []
    })

选择患者-家庭档案(复用)

image-20220824154246436

实现:在家庭档案基础上实现选择患者功能

步骤:

  • 复用家庭档案 组件,根据地址栏是否有标识
  • 点击选中效果
  • 默认选中效果
  • 记录患者ID跳转到待支付页面

代码:User/PatientPage.vue

1)界面兼容选择患者

ts
// 是否是选择患者
const route = useRoute()
const isSelect = route.query.isSelect
html
<cp-nav-bar :title="isSelect ? '选择患者' : '家庭档案'" />
html
<!-- 头部提示 -->
    <div class="patient-change" v-if="isSelect">
      <h3>请选择患者信息</h3>
      <p>以便医生给出更准确的治疗,信息仅医生可见</p>
    </div>
html
<!-- 底部按钮 -->
    <div class="patient-next" v-if="isSelect">
      <van-button type="primary"  round block>下一步</van-button>
    </div>
scss
.patient-change {
  padding: 15px;
  > h3 {
    font-weight: normal;
    margin-bottom: 5px;
  }
  > p {
    color: var(--cp-text3);
  }
}
.patient-next {
  padding: 15px;
  background-color: #fff;
  position: fixed;
  left: 0;
  bottom: 0;
  width: 100%;
  height: 80px;
  box-sizing: border-box;
}

2)点击选中效果

ts
const selectId = ref<string>()
const setId = (id: string) => {
  if (isSelect.value) {
    patientId.value = item.id
  }
}
diff
<div
        class="patient-item"
        v-for="item in list"
        :key="item.id"
+       @click="setId(item)"
+       :class="{ selected: selectId === item.id }"
      >

3) 默认选中效果

diff
const loadData = async () => {
  const res = await getPatientListAPI()
  list.value = res.data
  
+  // 设置默认选中的ID,当你是选择患者的时候,且有患者信息的时候
+  if (!isSelect || !list.value.length) return
+  const defPatient = list.value.find((item) => item.defaultFlag === 1)
+  if (defPatient) {
+    selectId.value = defPatient.id
+  } else {
+    selectId.value = list.value[0].id
+  }
}

4)记录患者ID跳转到待支付页面

ts
const router = useRouter()
const store = useConsultStore()
ts
const onNext = () => {
  if (!selectId.value) return showToast('请选就诊择患者')
  store.setId(selectId.value)
  router.push('/consult/pay')
}
ts
export const useConsultStore = defineStore(
  'cp-consult',
  () => {
		// ... 省略其它代码 ...

    /** 设置患者id */
     const setPatientId = (id: string) => (consult.value.patientId = id)

    return { 
     // ... 省略其它代码 ...
      setPatientId 
    }
  },
  // ... 省略其它代码 ...
)

问诊支付-

静态结构准备

image-20220824154323546

实现:问诊页面的基础布局,和业务需求情况。

1)组件与路由

组件 Consult/ConsultPay.vue

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

<template>
  <div class="consult-pay-page">
    <!-- 导航栏 -->
    <cp-nav-bar title="支付" />

    <!-- 费用信息 -->
    <div class="pay-info">
      <p class="tit">图文问诊 49 元</p>
      <img class="img" src="@/assets/avatar-doctor.svg" />
      <p class="desc">
        <span>极速问诊</span>
        <span>自动分配医生</span>
      </p>
    </div>

    <!-- 折扣 -->
    <van-cell-group>
      <van-cell title="优惠券" value="-¥10.00" />
      <van-cell title="积分抵扣" value="-¥10.00" />
      <van-cell title="实付款" value="¥29.00" class="pay-price" />
    </van-cell-group>
    <div class="pay-space"></div>

    <!-- 患者信息 -->
    <van-cell-group>
      <van-cell title="患者信息" value="李富贵 | 男 | 30岁"></van-cell>
      <van-cell title="病情描述" label="头痛,头晕,恶心"></van-cell>
    </van-cell-group>
    <div class="pay-schema">
      <van-checkbox>我已同意 <span class="text">支付协议</span></van-checkbox>
    </div>

    <!-- 底部支付按钮 -->
    <van-submit-bar
      button-type="primary"
      :price="2900"
      button-text="立即支付"
      text-align="left"
    />
    
    <!-- 支付选项卡片 -->
    <van-action-sheet title="选择支付方式">
      <div class="pay-type">
        <p class="amount">¥20元</p>
        <van-cell-group>
          <van-cell title="微信支付">
            <template #icon><cp-icon name="consult-wechat" /></template>
            <template #extra><van-checkbox /></template>
          </van-cell>
          <van-cell title="支付宝支付">
            <template #icon><cp-icon name="consult-alipay" /></template>
            <template #extra><van-checkbox /></template>
          </van-cell>
        </van-cell-group>
        <div class="btn">
          <van-button type="primary" round block>立即支付</van-button>
        </div>
      </div>
    </van-action-sheet>
  </div>
</template>

<style lang="scss" scoped>
.consult-pay-page {
  padding: 0 0 50px;
}

.pay-info {
  display: flex;
  padding: 15px;
  flex-wrap: wrap;
  align-items: center;
  .tit {
    width: 100%;
    font-size: 16px;
    margin-bottom: 10px;
  }
  .img {
    margin-right: 10px;
    width: 38px;
    height: 38px;
    border-radius: 4px;
    overflow: hidden;
  }
  .desc {
    flex: 1;
    > span {
      display: block;
      color: var(--cp-tag);
      &:first-child {
        font-size: 16px;
        color: var(--cp-text2);
      }
    }
  }
}

.pay-price {
  ::v-deep() {
    .vam-cell__title {
      font-size: 16px;
    }
    .van-cell__value {
      font-size: 16px;
      color: var(--cp-price);
    }
  }
}
.pay-space {
  height: 12px;
  background-color: var(--cp-bg);
}

.pay-schema {
  height: 56px;
  display: flex;
  align-items: center;
  justify-content: center;
  .text {
    color: var(--cp-primary);
  }
}

::v-deep() {
  .van-submit-bar__button {
    font-weight: normal;
    width: 160px;
  }
}
  
.pay-type {
  .amount {
    padding: 20px;
    text-align: center;
    font-size: 16px;
    font-weight: bold;
  }
  .btn {
    padding: 15px;
  }
  .van-cell {
    align-items: center;
    .cp-icon {
      margin-right: 10px;
      font-size: 18px;
    }
    .van-checkbox :deep(.van-checkbox__icon) {
      font-size: 16px;
    }
  }
}
</style>

路由 router/index.ts

ts
{
      path: '/consult/pay',
      component: () => import('@/views/Consult/ConsultPay.vue'),
      meta: { title: '问诊支付' }
    }

请求数据

  1. 定义数据类型、定义参数类型
  2. 定义 API 函数 (两个)
  3. 定义数据
  4. 挂载后,发送请求,保存数据
  5. 渲染界面

types/consult.d.ts

ts
/** 问诊订单预支付信息 */
export interface OrderPreData {
  /** 实付金额 */
  actualPayment: number
  /** 优惠券抵扣 */
  couponDeduction: number
  /** 使用的优惠券id-使用优惠券时,返回 */
  couponId?: string
  /** 极速问诊类型:0普通1三甲,极速问题必须有值 */
  illnessType?: number
  /** 应付款/价格-图文或者极速的费用,极速普通10元,三甲39元 */
  payment: number
  /** 积分可抵扣 */
  pointDeduction: number
  /** 1问医生2极速问诊2开药问诊--默认是1 */
  type?: number
}

/** 请求订单支付信息的参数 */
export type OrderPreParams = Pick<OrderPreData, 'type' | 'illnessType'>

services/consult.ts

ts
import type { OrderPreParams } from '@/types/consult'

// 2.1 请求预支付接口
/** 请求预支付数据 */
export const getPreOrderAPI = (params: OrderPreParams) => {
  return request({
    url: '/patient/consult/order/pre',
    params
  })
}

// 2.2 定义患者详情查询API
/** 查询患者详情 */
export const getPatientDetail = (id: string) => {
  return request({ url: `/patient/info/${id}` })
}

ConsultPay.vue

vue
<script setup lang="ts">
import { getPreOrderAPI } from '@/services/consult'
import { getPatientDetail } from '@/services/user'
import { useConsultStore } from '@/stores'
import type { OrderPreData } from '@/types/consult'
import type { Patient } from '@/types/user'
import { onMounted, ref } from 'vue'

const store = useConsultStore()

const payInfo = ref<OrderPreData>()
const loadData = async () => {
  const res = await getPreOrderAPI({
    type: store.consult.type,
    illnessType: store.consult.illnessType
  })
  payInfo.value = res.data

}

const patient = ref<Patient>()
const loadPatient = async () => {
  if (!store.consult.patientId) return
  const res = await getPatientDetail(store.consult.patientId)
  patient.value = res.data
}

onMounted(() => {
  loadData()
  loadPatient()
})

</script>

<template>
  <div class="consult-pay-page" v-if="payInfo">
    <cp-nav-bar title="支付" />
    <div class="pay-info">
      <p class="tit">图文问诊 {{ payInfo?.payment }} 元</p>
      <img class="img" src="@/assets/avatar-doctor.svg" />
      <p class="desc">
        <span>极速问诊</span>
        <span>自动分配医生</span>
      </p>
    </div>
    <van-cell-group>
      <van-cell title="优惠券" :value="`-¥${payInfo.couponDeduction}`" />
      <van-cell title="积分抵扣" :value="`-¥${payInfo.pointDeduction}`" />
      <van-cell title="实付款" :value="`¥${payInfo.actualPayment}`" class="pay-price" />
    </van-cell-group>
    <div class="pay-space"></div>
    <van-cell-group>
      <van-cell
        title="患者信息"
        :value="`${patient?.name} | ${patient?.genderValue} | ${patient?.age}岁`"
      ></van-cell>
      <van-cell title="病情描述" :label="store.consult.illnessDesc"></van-cell>
    </van-cell-group>
    <div class="pay-schema">
      <van-checkbox >我已同意 <span class="text">支付协议</span></van-checkbox>
    </div>
    <van-submit-bar
      button-type="primary"
      :price="payInfo.actualPayment * 100"
      button-text="立即支付"
      text-align="left"
    />
    <van-action-sheet title="选择支付方式" :closeable="false">
      <div class="pay-type">
        <p class="amount">¥20元</p>
        <van-cell-group>
          <van-cell title="微信支付">
            <template #icon><cp-icon name="consult-wechat" /></template>
            <template #extra><van-checkbox /></template>
          </van-cell>
          <van-cell title="支付宝支付">
            <template #icon><cp-icon name="consult-alipay" /></template>
            <template #extra><van-checkbox /></template>
          </van-cell>
        </van-cell-group>
        <div class="btn">
          <van-button type="primary" round block>立即支付</van-button>
        </div>
      </div>
    </van-action-sheet>
  </div>
</template>

问诊支付-流程讲解

支付流程:

  • 点击支付按钮,调用生成订单接口,得到 订单ID,打开选择支付方式对话框
  • 选择支付方式,(测试环境需要配置 回跳地址)调用获取支付地址接口,得到支付地址,跳转到支付宝页面
    • 使用支付宝APP支付(在手机上且安装沙箱支付宝)
    • 使用浏览器账号密码支付 (测试推荐)
  • 支付成功回跳到问诊室页面

回跳地址:

http://localhost:3000/room

支付宝沙箱账号:

买家账号:jfjbwb4477@sandbox.com
登录密码:111111
支付密码:111111

问诊支付-生成订单-打开弹窗

步骤:

  1. 声明变量agree
  2. 声明变量isShow
  3. 声明变量paymentMethod: 0 | 1
  4. v-model双向绑定
  5. 插值显示

代码:

  1. 声明变量agree
  2. 声明变量isShow
  3. 声明变量paymentMethod: 0 | 1
ts
const agree = ref(false)
const isShow = ref(false)
const paymentMethod = ref<0 | 1>(1)
const onSubmit = () => {
  if (!agree.value) return showToast('请勾选我已同意支付协议')
  isShow.value = true
}
  1. v-model双向绑定

  2. 插值显示

diff
<van-submit-bar
      button-type="primary"
      :price="payInfo.actualPayment * 100"
      button-text="立即支付"
      text-align="left"
+      @click="onSubmit"
    />
html
<van-action-sheet v-model:show="isShow" title="选择支付方式" :closeable="false">
      <div class="pay-type">
        <p class="amount">¥{{ payInfo.actualPayment.toFixed(2) }}</p>
        <van-cell-group>
          <van-cell title="微信支付" @click="paymentMethod = 0">
            <template #icon><cp-icon name="consult-wechat" /></template>
            <template #extra><van-checkbox :checked="paymentMethod === 0" /></template>
          </van-cell>
          <van-cell title="支付宝支付" @click="paymentMethod = 1">
            <template #icon><cp-icon name="consult-alipay" /></template>
            <template #extra><van-checkbox :checked="paymentMethod === 1" /></template>
          </van-cell>
        </van-cell-group>
        <div class="btn">
          <van-button type="primary" round block>立即支付</van-button>
        </div>
      </div>
    </van-action-sheet>

问诊支付-生成订单

步骤:

  1. 封装API函数
  2. 声明变量loading,点击防抖
  3. 声明变量orderId,保存订单id
  4. 点击事件调用API,获取订单id

services/consut.ts

ts
/** 生成订单接口 */
export const createOrderAPI = (data: PartialConsult) => {
  return request({ url: '/patient/consult/order', method: 'POST', data })
}

Consult/ConsultPay.ts

diff
const agree = ref(false)
const show = ref(false)
const paymentMethod = ref<0 | 1>()

+ const loading = ref(false)
+ const orderId = ref('')

const onSubmit = async () => {
  if (!agree.value) return showToast('请勾选我已同意支付协议')
+  loading.value = true
+  const res = await createOrderAPI(store.consult)
+  orderId.value = res.data.id
+  loading.value = false

  // 打开
  show.value = true
}
diff
<van-submit-bar
      button-type="primary"
      :price="payInfo.actualPayment * 100"
      button-text="立即支付"
      text-align="left"
+      :loading="loading"
      @click="submit"
      
    />

问诊支付-进行支付

1)生成订单后不可回退

ts
import { onBeforeRouteLeave } from 'vue-router'
ts
onBeforeRouteLeave(() => {
  if (orderId.value) return false
})

2)配置组件,不监听路由变化

diff
<van-action-sheet
    v-model:show="show"
    title="选择支付方式"
+   :close-on-popstate="false"
    :closeable="false"
  >

3)生成支付地址的 API 函数

services/consut.ts

ts
/** 请求支付地址接口的参数类型 */
export type PayUrlParams = {
  paymentMethod: 0 | 1
  orderId: string
  payCallback: string
}

/** 获取支付地址  0 是微信  1 支付宝 */
export const getOrderPayUrl = (data: PayUrlParams) => {
  return request({ url: '/patient/consult/pay', method: 'post', data })
}

4)跳转到支付宝页面

ts
// 跳转支付
const onPay = async () => {
  if (paymentMethod.value === undefined) return showToast('请选择支付方式')
  showLoadingToast('跳转支付')
  const res = await getOrderPayUrl({
    orderId: orderId.value,
    paymentMethod: paymentMethod.value,
    payCallback: 'http://localhost:3000/room'
  })
  window.location.href = res.data.payUrl
}

防止在当前页面刷新,问诊记录已经清空,组件初始化需要校验

ts
onMounted(() => {
  if (
    !store.consult.type ||
    !store.consult.illnessType ||
    !store.consult.depId ||
    !store.consult.patientId
  ) {
    return showDialog({
      title: '温馨提示',
      message: '问诊信息不完整请重新填写,如有未支付的问诊订单可在问诊记录中继续支付',
      closeOnPopstate: false
    }).then(() => {
      router.push('/')
    })
  }
  loadData()
  loadPatient()
})

Released under the MIT License.