极速问诊模块
极速问诊-需求分析
理解:极速问诊阶段流程分析
极速问诊阶段:
极速问诊(记录-问诊类型)
三甲图文问诊 或 普通图文问诊(记录-极速问诊类型)
选择科室(记录-疾病科室)
描述病情(记录-症状详情、时间、是否就诊过、图片)
选择患者(记录-患者ID)
支付问诊费
所有流程走完才能组合成完整的问诊记录,而且是不同的页面采集数据,这个实现需要 pinia
还有一个发现:接口数据
- type 就诊类型: 1找医生 2极速问诊 3开药问诊
type:1|2|3
- illnessType 极速问诊类型:0普通 1三甲
illnessType: 0|1
提问:
- 刚刚看到 1 2 3 的时候你能记得他们代表什么意思吗?
- 不清楚,对于数字字面量类型的联合类型语义差,建议使用
枚举
- 不清楚,对于数字字面量类型的联合类型语义差,建议使用
极速问诊-静态结构
完成选择三甲还是普通问诊页面,点击后记录对应的类型,跳转到选择科室路由
步骤:
- 路由和组件
- 编写页面布局
- 测试路由跳转
代码:
- 组件静态结构:直接复制
src/views/Consult/ConsultFast.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>
- 配置路由:
router/index.ts
{
path: '/consult/fast',
component: () => import('@/views/Consult/ConsultFast.vue'),
meta: { title: '极速问诊' }
}
极速问诊-选择科室-静态结构
实现:路由与组件,和基础结构
步骤:
- 组件与路由
- 一级科室使用 sidebar 组件
- 二级科室绘制
代码:
1)组件静态结构和样式
src/views/Consult/ConsultDep.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>
- 配置路由
router/index.ts
{
path: '/consult/dep',
component: () => import('@/views/Consult/ConsultDep.vue'),
meta: { title: '选择科室' }
}
极速问诊-创建问诊记录仓库
实现:病情描述仓库的定义,实现问诊记录分步修改
步骤:
- 定义仓库,备用
- 导出仓库
- 首页创建store
代码:
1)定义仓库 src/stores/consult.ts
import { defineStore } from 'pinia'
export const useConsultStore = defineStore(
'cp-consult',
() => {
},
{ persist: true }
)
- 导出仓库
stores/index.ts
export * from './modules/consult'
3)首页创建store views/Home/index.vue
// 省略其它代码
import { useConsultStore } from '@/stores'
const store = useConsultStore()
极速问诊-记录问诊类型
实现:点击问诊类型,pinia中保存问诊类型
步骤:
- 定义类型
- pinia中声明变量
type
- 点击事件,修改
- 首页,点击极速问诊,记录问诊类型
views/Home/index.vue
- 极速问诊页面,点击问诊类型,记录问诊类型
views/ConsultDep.vue
代码:
- 定义类型,查看接口得知,问诊类型:1找医生、2极速问诊、3开药问诊
注意:枚举类型需要在 ts 文件中,因为枚举会编译成 js 代码
src/enums/index.ts
/** 问诊类型 */
export enum ConsultType {
/** 找医生 */
Doctor = 1,
/** 快速问诊 */
Fast = 2,
/** 开药问诊 */
Medication = 3
}
/** 问诊时间,以1自增可以省略 */
export enum IllnessTime {
/** 一周内 */
Week = 1,
/** 一月内 */
Month,
/** 半年内 */
HalfYear,
/** 半年以上 */
More
}
- 定义采集数据的类型
types/consult.d.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>
// 💥💥 全部可选是因为信息是一点一点累加上去的
- pinia中声明变量
consultType
src/stores/modules/consult.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
}
)
- 首页,点击极速问诊,记录问诊类型
views/Home/index.vue
<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>
- 极速问诊页面,点击问诊类型,记录问诊类型
views/ConsultDep.vue
<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
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
import type {
DoctorPage,
FollowType,
KnowledgePage,
KnowledgeParams,
PageParams,
+ TopDep
} from '@/types/consult'
+ export const getAllDepAPI = () => request({ url: '/dep/all' })
3)挂载后,调用API函数Consult/ConsultDep.vue
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)
- 列表渲染
<van-sidebar >
<van-sidebar-item :title="top.name" v-for="top in allDep" :key="top.id" />
</van-sidebar>
- v-model收集选中科室
/** 选中的一级科室下标 */
const topSelectIndex = ref(0)
- <van-sidebar >
+ <van-sidebar v-model="topSelectIndex">
<van-sidebar-item :title="top.name" v-for="top in allDep" :key="top.id" />
</van-sidebar>
极速问诊-选择二级科室
步骤:
- 计算属性,计算二级科室列表subDep
- 列表渲染
- 点击事件,保存选中二级科室到pinia中
代码:
- 计算属性,计算二级科室列表subDep
import { computed, onMounted, ref } from 'vue'
// 二级科室,注意:组件初始化没有数据 child 可能拿不到
const subDep = computed(() => allDep.value[topSelectIndex.value]?.child)
- 列表渲染
<div class="sub-dep">
<router-link to="/consult/illness" v-for="sub in subDep" :key="sub.id">
{{ sub.name }}
</router-link>
</div>
- 点击事件,保存选中二级科室到pinia中
import { useConsultStore } from '@/stores'
const store = useConsultStore()
<router-link
to="/consult/illness"
v-for="sub in subDep"
:key="sub.id"
+ @click="store.setDep(sub.id)"
>
{{ sub.name }}
</router-link>
病情描述-静态结构
实现:路由和组件以及页面的基础布局(医生提示,描述,症状时间,是否已就诊)
代码:
1)路由与组件
src/views/Consult/ConsultIllness.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>
- 配置路由
{
path: '/consult/illness',
component: () => import('@/views/Consult/ConsultIllness.vue'),
meta: { title: '病情描述' }
},
病情描述-采集表单数据
目标:v-model采集表单数据
步骤:
- 定义表单数据类型
- 声明变量
- v-model绑定表单元素
代码:
- 准备定义表单数据类型
types/consult.d.ts
export type ConsultIllness = Pick<
PartialConsult,
'illnessDesc' | 'illnessTime' | 'consultFlag' | 'pictures'
>
2.声明变量formData:stores/modules/consult.ts
import type { PartialConsult, ConsultIllness } from '@/types/consult'
import { ref } from 'vue'
const formData = ref<ConsultIllness>({
illnessDesc: '',
illnessTime: undefined,
consultFlag: undefined,
pictures: []
})
- 双向绑定
<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 组件,进行样式和功能配置
步骤:
- 组件基础结构
- 配置文字和图标
- 配置最多数量和最大体积
- 支持双向数据绑定,支持选择图片后触发函数,支持点击删除事件函数
代码:
- 组件基础结构
Consult/ConsultIllness.vue
<!-- 图片上传区域 -->
<div class="illness-img">
<van-uploader />
<p class="tip" >上传内容仅医生可见,最多9张图,最大5MB</p>
</div>
- 支持双向数据绑定,支持选择图片后触发函数,支持点击删除事件函数
<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>
- 配置文字和图标
<van-uploader
v-model="fileList"
+ upload-icon="photo-o"
+ upload-text="上传图片"
/>
- 配置最多数量和最大体积
<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
/** 上传图片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
import { uploadImageAPI } from '@/services/consult'
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 = '上传失败'
})
}
<!-- 图片上传区域 -->
<div class="illness-img">
<van-uploader
...省略其它代码
+ @onAfterRead="onAfterRead"
/>
...省略其它代码
</div>
3)实现删除
const onDelete = (item: UploaderFileListItem) => {
form.value.pictures = form.value.pictures?.filter((pic) => pic.url !== item.url)
}
<!-- 图片上传区域 -->
<div class="illness-img">
<van-uploader
...省略其它代码
+ @delete="onDelete"
/>
...省略其它代码
</div>
小结:
- 给 item 加上 url 是为了删除可以根据 url 进行删除
病情描述-保存数据
实现:按钮点亮,提交校验,保存数据,跳转选择患者
1)按钮解除禁用
<van-button :class="{disabled: true}" type="primary" block round>下一步</van-button>
import { computed, ref } from 'vue'
// ... 省略 ...
const disabled = computed(
// illnessDesc consultFlag illnessTime均为必选
() =>
!formData.value.illnessDesc ||
formData.value.illnessTime === undefined ||
formData.value.consultFlag === undefined
)
2)提交校验 保存数据,跳转选择患者
const onNext = () => {
if (!formData.value.illnessDesc) return showToast('请输入病情描述')
if (formData.value.illnessTime === undefined) return showToast('请选择症状持续时间')
if (formData.value.consultFlag === undefined) return showToast('请选择是否已经就诊')
}
<van-button
...省略其它代码
+ @click="onNext"
>
下一步
</van-button>
- 保存数据,跳转选择问诊信息:
stores/modules/consult.ts
- 扩展pinia仓库,保存病情描述
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
}
},
// ... 省略其它代码 ...
)
- 保存数据,跳转路由
const store = useConsultStore()
const router = useRouter()
const onNext = () => {
// ... 省略其它代码 ...
store.setIllness(formData.value)
// 跳转档案管理(复用),需要根据 isSelect 实现选择功能
router.push('/user/patient?isSelect=1')
}
病情描述-回显数据
实现:进入页面时候提示用户是否回显之前填写的病情描述信息
1)进入页面,如果有记录数据,弹出确认框
// 回显数据
onMounted(() => {
if (store.consult.illnessDesc) {
showConfirmDialog({
title: '温馨提示',
message: '是否恢复您之前填写的病情信息呢?',
confirmButtonColor: 'var(--cp-primary)'
}).then(() => {
// 确认
})
}
})
2)回显数据
从 store 拿出记录的数据
.then(() => {
// 确认
const { illnessDesc, illnessTime, consultFlag, pictures } = store.consult
form.value = { illnessDesc, illnessTime, consultFlag, pictures }
// 图片回显
fileList.value = pictures || []
})
选择患者-家庭档案(复用)
实现:在家庭档案基础上实现选择患者功能
步骤:
- 复用
家庭档案
组件,根据地址栏是否有标识 - 点击选中效果
- 默认选中效果
- 记录患者ID跳转到待支付页面
代码:User/PatientPage.vue
1)界面兼容选择患者
// 是否是选择患者
const route = useRoute()
const isSelect = route.query.isSelect
<cp-nav-bar :title="isSelect ? '选择患者' : '家庭档案'" />
<!-- 头部提示 -->
<div class="patient-change" v-if="isSelect">
<h3>请选择患者信息</h3>
<p>以便医生给出更准确的治疗,信息仅医生可见</p>
</div>
<!-- 底部按钮 -->
<div class="patient-next" v-if="isSelect">
<van-button type="primary" round block>下一步</van-button>
</div>
.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)点击选中效果
const selectId = ref<string>()
const setId = (id: string) => {
if (isSelect.value) {
patientId.value = item.id
}
}
<div
class="patient-item"
v-for="item in list"
:key="item.id"
+ @click="setId(item)"
+ :class="{ selected: selectId === item.id }"
>
3) 默认选中效果
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跳转到待支付页面
const router = useRouter()
const store = useConsultStore()
const onNext = () => {
if (!selectId.value) return showToast('请选就诊择患者')
store.setId(selectId.value)
router.push('/consult/pay')
}
export const useConsultStore = defineStore(
'cp-consult',
() => {
// ... 省略其它代码 ...
/** 设置患者id */
const setPatientId = (id: string) => (consult.value.patientId = id)
return {
// ... 省略其它代码 ...
setPatientId
}
},
// ... 省略其它代码 ...
)
问诊支付-
静态结构准备
实现:问诊页面的基础布局,和业务需求情况。
1)组件与路由
组件 Consult/ConsultPay.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
{
path: '/consult/pay',
component: () => import('@/views/Consult/ConsultPay.vue'),
meta: { title: '问诊支付' }
}
请求数据
- 定义数据类型、定义参数类型
- 定义 API 函数 (两个)
- 定义数据
- 挂载后,发送请求,保存数据
- 渲染界面
types/consult.d.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
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
<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
问诊支付-生成订单-打开弹窗
步骤:
- 声明变量
agree
- 声明变量
isShow
- 声明变量
paymentMethod: 0 | 1
v-model
双向绑定- 插值显示
代码:
- 声明变量
agree
- 声明变量
isShow
- 声明变量
paymentMethod: 0 | 1
const agree = ref(false)
const isShow = ref(false)
const paymentMethod = ref<0 | 1>(1)
const onSubmit = () => {
if (!agree.value) return showToast('请勾选我已同意支付协议')
isShow.value = true
}
v-model
双向绑定插值显示
<van-submit-bar
button-type="primary"
:price="payInfo.actualPayment * 100"
button-text="立即支付"
text-align="left"
+ @click="onSubmit"
/>
<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>
问诊支付-生成订单
步骤:
- 封装
API
函数 - 声明变量
loading
,点击防抖 - 声明变量
orderId
,保存订单id - 点击事件调用
API
,获取订单id
services/consut.ts
/** 生成订单接口 */
export const createOrderAPI = (data: PartialConsult) => {
return request({ url: '/patient/consult/order', method: 'POST', data })
}
Consult/ConsultPay.ts
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
}
<van-submit-bar
button-type="primary"
:price="payInfo.actualPayment * 100"
button-text="立即支付"
text-align="left"
+ :loading="loading"
@click="submit"
/>
问诊支付-进行支付
1)生成订单后不可回退
import { onBeforeRouteLeave } from 'vue-router'
onBeforeRouteLeave(() => {
if (orderId.value) return false
})
2)配置组件,不监听路由变化
<van-action-sheet
v-model:show="show"
title="选择支付方式"
+ :close-on-popstate="false"
:closeable="false"
>
3)生成支付地址的 API 函数
services/consut.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)跳转到支付宝页面
// 跳转支付
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
}
防止在当前页面刷新,问诊记录已经清空,组件初始化需要校验
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()
})