uni-app(优医咨询)项目实战 - 第 6 天
学习目标:
- 掌握第三方支付的流程
- 能够使用支付宝完成支付
- 了解 uni-pay 聚合支付的使用步骤
- 知道如何通过 websocket 进行通信
- 能够完成问诊订单的创建
一、极速问诊
继续完善极速问诊的相关功能。
1.1 选择患者
在患者列表中选择需要问诊的患者,访部分的逻辑在家庭档案管理模块已经实现过了,偷懒将之前的代码拷贝过来(当然也可进行更完善的封装)。
1.1.1 布局模板
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
| <!-- subpkg_consult/patient/index.vue --> <script setup> import { ref } from 'vue' // 侧滑按钮配置 const swipeOptions = ref([ { text: ' 删除 ', style: { backgroundColor: '#dd524d', }, }, ]) </script> <template> <scroll-page> <view class="patient-page"> <view class="page-header"> <view class="patient-title"> 请选择患者信息 </view> <view class="patient-tips"> 以便医生给出更准确的治疗,信息仅医生可见 </view> </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>
<!-- 添加按钮 --> <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> <!-- 下一步操作 --> <view class="next-step"> <button class="uni-button"> 下一步 </button> </view> </scroll-page> </template>
<style lang="scss"> @import './index.scss'; </style>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110
| .patient-page { padding: 30rpx 30rpx calc(env(safe-area-inset-bottom) + 200rpx); }
.page-header { margin-top: 20rpx; margin-bottom: 40rpx; }
.patient-title { font-size: 36rpx; color: #121826; }
.patient-tips { margin-top: 10rpx; 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); }
.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 { 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; } }
.next-step { position: fixed; right: 0; left: 0; bottom: 0; padding: 30rpx 40rpx calc(env(safe-area-inset-bottom) + 30rpx); background-color: #fff; }
:deep(.uni-swipe_button-group) { bottom: 30rpx; }
|
1.1.2 患者列表
患者列表的接口在前面家庭档案模块中已经封装过了,在此直接调用获取数据即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
| <!-- subpkg_consult/patient/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', }, }, ]) // 患者列表 const patientList = ref([]) // 是否显示页面内容 const pageShow = ref(false)
// 页面加载生命周期 onShow(() => { // 获取患者列表 getPatientList() })
// 家庭档案(患者)列表 async function getPatientList() { // 患者列表接口 const { code, data } = await patientListApi() // 检测接口是否调用成功 if (code !== 10000) return uni.utils.showToast(' 列表获取失败,稍后重试!') // 渲染接口数据 patientList.value = data // 展示页面内容 pageShow.value = true } </script>
<template> <scroll-page> <view class="patient-page"> <view class="page-header"> <view class="patient-title"> 请选择患者信息 </view> <view class="patient-tips"> 以便医生给出更准确的治疗,信息仅医生可见 </view> </view> <uni-swipe-action> <uni-swipe-action-item v-for="(patient, index) in patientList" :key="patient.id" :right-options="swipeOptions" > <view 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 class="edit-link" :url="`/subpkg_archive/add/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="patientList.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> <!-- 下一步操作 --> <view class="next-step"> <navigator class="uni-button" url="/subpkg_consult/payment/index"> 下一步 </navigator> </view> </scroll-page> </template>
|
1.2.3 选择患者
用户通过点击的方式选择就诊患者,被选中的患者需要以高亮的方式显示(添加 .active
类名),实现步骤:
- 监听点击事件
- 根据索引值设置高亮样式
- 根据索引值获取患者 ID
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
| <!-- subpkg_consult/patient/index.vue --> <script setup> import { ref, computed } from 'vue' import { onShow } from '@dcloudio/uni-app' import { patientListApi } from '@/services/patient' // 省略前面小节的代码... // 患者卡片索引值 const patientCardIndex = ref(0) // 所选患者的 ID const patientId = computed(() => { return patientList.value[patientCardIndex.value].id }) // 省略前面小节的代码
function onPatientCardClick(index) { // 患者的索引值 patientCardIndex.value = index }
// 省略前面小节的代码... </script>
<template> <scroll-page> <view class="patient-page" v-if="pageShow"> <view class="page-header"> <view class="patient-title"> 请选择患者信息 </view> <view class="patient-tips"> 以便医生给出更准确的治疗,信息仅医生可见 </view> </view>
<uni-swipe-action> <uni-swipe-action-item v-for="(patient, index) in patientList" :key="patient.id" :right-options="swipeOptions" > <view @click="onPatientCardClick(index)" :class="{ active: patientCardIndex === index }" class="archive-card" > <!-- 省略前面小节的代码... --> </view> </uni-swipe-action-item> </uni-swipe-action>
<!-- 添加按钮 --> <view v-if="patientList.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> <!-- 下一步操作 --> <view class="next-step"> <navigator class="uni-button" url="/subpkg_consult/payment/index"> 下一步 </navigator> </view> </scroll-page> </template>
|
1.2 预付订单
选择患者后的下一个步骤是创建问诊订单。按下面的分包配置创建分包页面,先创建好页面再来补充配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| { "subPackages": [ { "root": "subpkg_consult", "pages": [ { "path": "payment/index", "style": { "navigationBarTitleText": " 等待付款 " } } ] } ] }
|
1.2.1 布局模板
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
| <!-- subpkg_consult/payment/index.vue --> <script setup></script>
<template> <scroll-page> <view class="payment-page"> <uni-section title-font-size="32rpx" title-color="#121826" padding="30rpx" title=" 图文问诊 49 元 " > <uni-list :border="false"> <uni-list-item title=" 极速问诊 " note=" 自动分配医生 " thumb="/static/uploads/doctor-avatar.jpg" thumb-size="lg" /> <uni-list-item title=" 优惠券 " show-arrow right-text="-¥10.00" /> <uni-list-item title=" 积分抵扣 "> <template #footer> <view class="uni-list-text-red">-¥1.00</view> </template> </uni-list-item> <uni-list-item title=" 实付款 "> <template #footer> <view class="uni-list-text-red">¥39.00</view> </template> </uni-list-item> </uni-list> </uni-section>
<view class="dividing-line"></view>
<uni-section title-font-size="32rpx" title-color="#121826" padding="30rpx" title=" 患者资料 " > <uni-list :border="false"> <uni-list-item title=" 患者信息 "> <template #footer> <view class="uni-list-text-gray"> 李富贵 | 男 | 30 岁 </view> </template> </uni-list-item> <uni-list-item border title=" 病情描述 " note=" 头痛, 头晕, 恶心 " /> </uni-list> </uni-section>
<!-- <view class="payment-agreement"> <radio color="#20c6b2" value="1" /> 我已同意 <text style="color: #20c6b2"> 支付协议 </text> </view> --> </view> <!-- 下一步操作 --> <view class="next-step"> <view class="total-amount"> 合计: <text class="number">¥39.00</text> </view> <button class="uni-button"> 立即支付 </button> </view> </scroll-page> </template>
<style lang="scss"> @import './index.scss'; </style>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112
| .payment-page { }
:deep(.uni-section-header) { font-weight: 500 !important; padding-left: 30rpx !important; padding-bottom: 0 !important; }
:deep(.uni-section-content) { padding-top: 0 !important; padding-bottom: 0 !important; }
:deep(.uni-list-item__container) { padding-left: 0 !important; padding-right: 0 !important; }
:deep(.uni-list-item__content-title) { font-size: 32rpx !important; color: #3c3e42 !important; }
:deep(.uni-list-item__extra-text) { font-size: 32rpx !important; color: #3c3e42 !important; }
:deep(.uni-list-item__content-note) { font-size: 28rpx !important; }
:deep(.uni-list-item__icon) { margin-right: 0 !important; }
:deep(.uni-icon-wrapper) { padding: 0 !important; margin-right: -10rpx !important; font-size: 36rpx !important; }
.dividing-line { height: 30rpx; background-color: #f6f6f6; }
.uni-list-text-red { color: #eb5757; }
.uni-list-text-gray { color: #848484; font-size: 30rpx; }
.payment-agreement { position: fixed; left: 0; right: 0; bottom: 180rpx;
display: flex; justify-content: center; align-items: center; font-size: 28rpx;
:deep(.uni-radio-input) { transform: scale(0.9); margin-right: 0 !important; }
radio { transform: scale(0.7); margin-right: -5rpx !important; } }
.next-step { position: fixed; left: 0; right: 0; bottom: 0;
display: flex; align-items: center; height: 88rpx; padding: 30rpx 40rpx calc(env(safe-area-inset-bottom) + 30rpx); background-color: #fff;
.uni-button { width: 400rpx; }
.total-amount { flex: 1; display: flex; align-items: center; font-size: 30rpx; color: #3c3e42; }
.number { font-size: 40rpx; color: #eb5757; margin-left: 10rpx; } }
|
在选择患者页面点【击一下】按钮时记录所选患者的 ID 并跳转到预付订单页面。
1.2.2 跳转页面
将所选择患者的 ID 记录到 Pinia 之中,然后跳转到预付订单详情页面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import { ref } from 'vue' import { defineStore } from 'pinia'
export const useConsultStore = defineStore( 'consult', () => { const patientId = ref('')
return { illnessInfo, initalValue, type, illnessType, depId, patientId } }, { persist: { paths: ['illnessInfo', 'type', 'illnessType', 'depId', 'patientId'], }, } )
|
点击【下一步】时存储患者 ID 并跳转页面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| <!-- subpkg_consult/patient/index.vue --> <script setup> import { ref, computed } from 'vue' import { onShow } from '@dcloudio/uni-app' import { patientListApi } from '@/services/patient' import { useConsultStore } from '@/stores/consult'
// 患者相关的数据 const consultStore = useConsultStore()
// 省略前面小节的代码...
// 下一步操作 function onNextStepClick() { // 将选中的患者 ID 记录到 Pinia 中 consultStore.patientId = patientId.value // 下一步操作 uni.navigateTo({ url: '/subpkg_consult/payment/index' }) }
// 省略前面小节的代码... </script> <template> <scroll-page> <!-- 省略前面小节的代码... --> <!-- 下一步操作 --> <view class="next-step"> <button @click="onNextStepClick" class="uni-button"> 下一步 </button> </view> </scroll-page> </template>
|
1.2.3 订单信息
根据问诊类型 type
生成预支付订单并展示订单信息,在核对订单信息无误后方可进行支付。
- 先根据接口文档来封装调用接口的方法,接口文档 地址在这里。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import { http } from '@/utils/http'
export const preOrderApi = (type, options = {}) => { return http.get('/patient/consult/order/pre', { params: { type, ...options, }, }) }
|
- 调用接口,生成预支付订单
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
| <!-- subpkg_consult/payment/index.vue --> <script setup> import { ref } from 'vue' import { useConsultStore } from '@/stores/consult' import { preOrderApi } from '@/services/consult'
// 患者相关的数据(不具有响应性) const { type, illnessType } = useConsultStore() // 预付订单信息 const preOrderInfo = ref({})
// 生成预付订单 async function createPreOrder() { // 预付订单信息 const { code, data, message } = await preOrderApi(type, { illnessType, }) // 检测接口是否调用成功 if (code !== 10000) return uni.utils.toast(message) // 渲染订单数据 preOrderInfo.value = data }
// 生成预支付订单 createPreOrder() </script>
<template> <scroll-page> <view class="payment-page"> <uni-section title-font-size="32rpx" title-color="#121826" padding="30rpx" :title="` 图文问诊 ${preOrderInfo.payment} 元 `" > <uni-list :border="false"> <uni-list-item title=" 极速问诊 " note=" 自动分配医生 " thumb="/static/uploads/doctor-avatar.jpg" thumb-size="lg" /> <uni-list-item title=" 优惠券 " show-arrow :right-text="`-¥${preOrderInfo.couponDeduction}`" /> <uni-list-item title=" 积分抵扣 "> <template #footer> <view class="uni-list-text-red"> -¥{{ preOrderInfo.pointDeduction }} </view> </template> </uni-list-item> <uni-list-item title=" 实付款 "> <template #footer> <view class="uni-list-text-red"> ¥{{ preOrderInfo.actualPayment }} </view> </template> </uni-list-item> </uni-list> </uni-section> <!-- 省略前面小节的代码... --> </view> <!-- 下一步操作 --> <view class="next-step"> <view class="total-amount"> 合计: <text class="number">¥{{ preOrderInfo.actualPayment }}</text> </view> <button class="uni-button"> 立即支付 </button> </view> </scroll-page> </template>
|
1.2.4 患者信息
预付订单中除了展示订单信息外,还需要用户对患者信息进行核对。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
| <!-- subpkg_consult/payment/index.vue --> <script setup> import { ref } from 'vue' import { useConsultStore } from '@/stores/consult' import { preOrderApi } from '@/services/consult' import { patientDetailApi } from '@/services/patient'
// 患者相关的数据(不具有响应性) const { type, illnessType, patientId, illnessInfo } = useConsultStore()
// 预付订单信息 const preOrderInfo = ref({}) // 就诊患者信息 const patientDetail = ref({})
// 省略前面小节的代码...
// 获取患者信息 async function getPatientDetail() { // 患者详情接口 const { code, data, message } = await patientDetailApi(patientId) // 检测接口是否调用成功 if (code !== 10000) return uni.utils.toast(message) // 渲染患者数据 patientDetail.value = data }
// 生成预支付订单 createPreOrder() // 获取就诊患者信息 getPatientDetail() </script>
<template> <scroll-page> <view class="payment-page"> <!-- 省略前面小节的代码 --> <!-- 患者资料 --> <uni-section title-font-size="32rpx" title-color="#121826" padding="30rpx" title=" 患者资料 " > <uni-list :border="false"> <uni-list-item title=" 患者信息 "> <template #footer> <view class="uni-list-text-gray"> {{ patientDetail.name }} | {{ patientDetail.genderValue }} | {{ patientDetail.age }} 岁 </view> </template> </uni-list-item> <uni-list-item border title=" 病情描述 " :note="illnessInfo.illnessDesc" /> </uni-list> </uni-section> </view> <!-- 省略前面小节的代码 --> </scroll-page> </template>
|
在上述代码中从 Pinia 获取数据时,采用的直接解构的方式获取的,这种方式解构出的数据是不具有响应式的,如果想要保持其响应式请使用 storeToRefs
后再进行解构。
1.3 待付订单
在核对预支付订单信息无误后,点击立即购买来生成待支付订单。
- 首先根据接口文档来封装接口调用的方法,接口文档 的地址在这里。
1 2 3 4 5 6 7 8 9 10 11
| import { http } from '@/utils/http'
export const createOrderApi = (data) => { return http.post('/patient/consult/order', data) }
|
- 调用接口生成待支付订单
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
| <!-- subpkg_consult/payment/index.vue --> <script setup> import { ref } from 'vue' import { useConsultStore } from '@/stores/consult' import { preOrderApi, createOrderApi } from '@/services/consult' import { patientDetailApi } from '@/services/patient'
// 患者相关的数据(不具有响应性) const { type, illnessType, patientId, illnessInfo, depId } = useConsultStore() // 省略前面小节的代码... // 订单 ID const orderId = ref('')
// 立即支付 async function onPaymentButtonClick() { if (orderId.value !== '') return uni.utils.toast(' 订单不能重复创建!') // 处理上传的图片,要求包含 ID 和 url (接口规订的) // 订单只能提交一次!!! illnessInfo.pictures = illnessInfo.pictures.map(({ url, uuid }) => { return { url, id: uuid } }) // 生成订单接口 const { code, data, message } = await createOrderApi({ type, illnessType, depId, patientId, ...illnessInfo, }) // 检测接口是否计用成功 if (code !== 10000) return uni.utils.toast(message) // 接收订单 ID orderId.value = data.id
// 将 Pinia 中缓存的数据清空掉(订单已创建完成) const consultStore = useConsultStore() // 病情描述 consultStore.illnessInfo = consultStore.initalValue consultStore.type = '' consultStore.illnessType = '' consultStore.depId = '' consultStore.patientId = ''
// 选择支付渠道... }
// 省略前面小节的代码... </script>
<template> <scroll-page> <!-- 省略前面小节的代码... --> <!-- 下一步操作 --> <view class="next-step"> <view class="total-amount"> 合计: <text class="number">¥{{ preOrderInfo.actualPayment }}</text> </view> <button @click="onPaymentButtonClick" class="uni-button"> 立即支付 </button> </view> </scroll-page> </template>
|
1.4 支付渠道
支付渠道指的完成支付的方式,最常见的有支付宝支付和微信支付,除此之外还有银联、百度钱包等。
1.4.1 Custom-payment
根据需求的要求,在生成预付订单之后页面中需要弹出一个弹层,弹层中展示的内容为支付方式(渠道),由用户选择一种支付方式进行支付。
该弹层组件是以扩展组件 uni-popup
为核心的,关于 uni-popup
组件的 使用文档 请查看这里,这里只介绍我们用到的部分:
type
属性,指定弹层出现的位置
is-mask-click
是否允许点击蒙层关闭弹层
maskClick
点击弹层时触发事件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81
| <!-- pages/test/test.vue --> <script setup> import { ref } from 'vue' // 省略前面小节的代码...
// 弹层的引用 const popupRef = ref()
// 点击蒙层 function onMaskClick() { console.log(' 蒙层点击了...') } // 打开弹层 function openPopup() { popupRef.value.open() }
// 关闭弹层 function closePopup() { popupRef.value.close() } </script>
<template> <scroll-page background-color="#f6f6f6" refresher-enabled @scrolltolower="test" @refresherrefresh="test" > <view class="content"> <!-- 省略前面小节的代码... --> <view class="popup-demo"> <button @click="openPopup" class="button" type="primary"> 打开弹层 </button> <button @click="closePopup" class="button" type="primary"> 关闭弹层 </button> </view> <uni-popup ref="popupRef" @maskClick="onMaskClick" :is-mask-click="false" type="bottom" > <view class="popup-container"></view> </uni-popup>
</view> </scroll-page> </template>
<style lang="scss"> .content { padding: 30rpx 30rpx 0; overflow: hidden; } // 省略前面小节的代码... .popup-demo { display: flex; justify-content: space-between; margin: 30rpx 0;
.button { width: 300rpx; margin: 0; } }
.popup-container { height: 400rpx; background-color: #fff; } </style>
|
在掌握了 uni-popup
的基本用法后,我们来封装支付渠道组件,组件要满足以下要求:
- 开放打开(open)和关闭(close)弹层的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108
| <!-- components/custom-payment/custom-payment.vue --> <script setup> import { ref } from 'vue' // 在线支付弹层 const paymentPopup = ref() // 打开弹层 function open() { paymentPopup.value.open() } // 关闭弹层 function close() { paymentPopup.value.close() } // 开放关闭和显示弹层的方法 defineExpose({ open, close }) </script> <template> <uni-popup :is-mask-click="false" ref="paymentPopup" type="bottom"> <view class="payment-container"> <view class="payment-header"> <text class="title"> 选择支付方式 </text> <uni-icons class="uni-icons-close" size="18" color="#333" type="closeempty" /> </view> <view class="order-amount">¥ 99.9 元 </view> <uni-list :border="false"> <uni-list-item title=" 支付宝支付 " thumb="/static/images/alipay-icon.png" > <template #footer> <radio color="#16C2A3" /> </template> </uni-list-item> <uni-list-item title=" 微信支付 " thumb="/static/images/wechatpay-icon.png" > <template #footer> <uni-icons v-if="false" size="26" color="#16C2A3" type="checkbox-filled" /> <uni-icons v-else size="26" color="#d1d1d1" type="circle" /> </template> </uni-list-item> </uni-list> <button class="uni-button"> 立即支付 </button> </view> </uni-popup> </template>
<script> export default { options: { styleIsolation: 'shared', }, } </script>
<style lang="scss"> .payment-container { min-height: 400rpx; border-radius: 30rpx 30rpx 0 0; background-color: #fff; padding: 10rpx 30rpx 40rpx;
.payment-header { height: 88rpx; line-height: 88rpx; text-align: center; margin-bottom: 20rpx; font-size: 32rpx; color: #333; position: relative; }
.uni-icons-close { position: absolute; top: 2rpx; right: 0; }
.order-amount { padding: 10rpx 0 10rpx; text-align: center; font-size: 40rpx; color: #333; }
:deep(.uni-list-item__container) { padding: 40rpx 0 !important; } :deep(.uni-list-item--hover) { background-color: #fff !important; }
:deep(.uni-list-item__icon) { margin-right: 0; }
.uni-button { margin-top: 40rpx; } } </style>
|
- 支持两个自定义属性,
orderId
、amount
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| <!-- components/custom-payment/custom-payment.vue --> <script setup> import { ref } from 'vue' // 在线支付弹层 const paymentPopup = ref() // 接收组件外部传入的数据 const paymentProps = defineProps({ // 待支付订单 ID orderId: String, // 待支付金额 amount: { type: [String, Number], default: 0, }, }) // 省略前面小节的代码... </script> <template> <uni-popup :is-mask-click="false" ref="paymentPopup" type="bottom"> <view class="payment-container"> <!-- 省略前面小节的代码... --> <view class="order-amount">¥ {{ paymentProps.amount }}</view> <!-- 省略前面小节的代码... --> </view> </uni-popup> </template>
|
- 支持 3 个自定义事件,
confirm
、change
和 close
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
| <!-- components/custom-payment/custom-payment.vue --> <script setup> import { ref } from 'vue' // 在线支付弹层 const paymentPopup = ref() // 支付渠道的索引 const channelIndex = ref(0) // 支付渠道(方式) const paymentChannel = [ { title: ' 微信支付 ', thumb: '/static/images/wechatpay-icon.png', }, { title: ' 支付宝支付 ', thumb: '/static/images/alipay-icon.png', }, ] // 接收组件外部传入的数据 const paymentProps = defineProps({ // 待支付订单 ID orderId: String, // 待支付金额 amount: { type: [String, Number], default: 0, }, })
// 自定义事件 const paymentEmits = defineEmits(['confirm', 'change', 'close']) // 切换支付渠道 function onChannelChange(index) { // 当前选中渠道索引 channelIndex.value = index // 触发 change 事件 paymentEmits('change', { index }) }
// 省略前面小节的代码... </script> <template> <uni-popup :is-mask-click="false" ref="paymentPopup" type="bottom"> <view class="payment-container"> <!-- 省略前面小节的代码... --> <uni-list :border="false"> <uni-list-item v-for="(channel, index) in paymentChannel" :key="channel.title" :title="channel.title" :thumb="channel.thumb" clickable @click="onChannelChange(index)" > <template #footer> <uni-icons v-if="channelIndex === index" size="26" color="#16C2A3" type="checkbox-filled" /> <uni-icons v-else size="26" color="#d1d1d1" type="circle" /> </template> </uni-list-item> </uni-list> <button class="uni-button"> 立即支付 </button> </view> </uni-popup> </template>
|
- 在用户点击了弹层中的立即购买按钮后触发
confirm
事件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <!-- components/custom-payment/custom-payment.vue --> <script setup> // 省略前面小节的代码... </script> <template> <uni-popup :is-mask-click="false" ref="paymentPopup" type="bottom"> <view class="payment-container"> <!-- 省略前面小节的代码... --> <button @click="$emit('confirm', { index: channelIndex })" class="uni-button" > 立即支付 </button> </view> </uni-popup> </template>
|
- 在用户点击蒙层或者右上角关闭按钮时触发
close
事件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| <!-- components/custom-payment/custom-payment.vue --> <script setup> // 省略前面小节的代码... </script> <template> <uni-popup @maskClick="$emit('close')" :is-mask-click="false" ref="paymentPopup" type="bottom" > <view class="payment-container"> <view class="payment-header"> <text class="title"> 选择支付方式 </text> <uni-icons class="uni-icons-close" size="18" color="#333" type="closeempty" @click="$emit('close')" /> </view> <!-- 省略前面小节的代码... --> </view> </uni-popup> </template>
|
在待支付页面中当用户点击了蒙层或右上角关闭按钮时,调用 uni.showModal
弹出确认框:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| <!-- subpkg_consult/payment/index.vue --> <script setup> import { ref } from 'vue' import { useConsultStore } from '@/stores/consult' import { preOrderApi, createOrderApi } from '@/services/consult' import { patientDetailApi } from '@/services/patient' // 省略前面小节的代码...
// 待支付订单 ID const orderId = ref('') // 支付组件引用 const paymentRef = ref()
// 立即支付 async function onPaymentButtonClick() { // 生成订单接口 const { code, data, message } = await createOrderApi({ type, illnessType, depId, patientId, ...illnessInfo, }) // 检测接口是否计用成功 if (code !== 10000) return uni.utils.toast(message) // 获取待支付订单 ID orderId.value = data.id // 选择支付渠道 paymentRef.value.open() }
// 当支付弹层关闭时 async function onPaymentClose() { const { confirm } = await uni.showModal({ title: ' 关闭支付 ', content: ' 取消支付将无法获得医生回复,医生接诊名额有限,是否确认关闭?', cancelText: ' 仍要关闭 ', cancelColor: '#848484', confirmText: ' 继续支付 ', confirmColor: '#16C2A3', })
if (!confirm) paymentRef.value.close() } // 省略前面小节的代码... </script>
<template> <!-- 省略前面小节的代码... --> <!-- 支付渠道 --> <custom-payment @close="onPaymentClose" :amount="preOrderInfo.actualPayment" :order-id="orderId" ref="paymentRef" /> </template>
|
1.4.2 支付流程
一般的支付流程如下:
- 第三方支付提供的开发者平台注册账号、创建应用、申请认证用的证书或者
key
- 前端获取待支付订单 ID、支付金额、支付渠道等数据,传递给后端接口
- 后端接口在获取前端传递的数据后,根据支付平台提供文档与支付平台接口进行对接
- 后端与支付平台对接成功后,后端将支付信息再回传给前端
- 前端根据回传的信息 引导用户 进行支付
在整个支付的过程中前端的任务仍然是调用接口(与调用普通的接口几乎没有差别),真正完成支付任务的其实是后端接口。
1.4.3 支付宝支付
- 自行 注册支付宝支付账号
- 在企业中开发时需要创建应用,然而创建应用后还需要一些资质才可以进行支付,在课堂学习时无法满足这些资质,好在支付定平台提供了沙箱环境,沙箱环境是协助开发者进行接口开发及主要功能联调的模拟环境,目前仅支持网页 / 移动应用和小程序两种应用类型。
- 在正式应用或沙箱应用中获取到商家账号、认证证书、APPID、回调地址等。
- 上述的操作其实都是由后端来操作的,这里只是让大家了解一下支付管理后台的相关信息。
咱们的后端已经完成了与支付宝平台的支付对接,咱们前端只需要调用后端提供的接口即可。
- 根据接口文档封装调用接口的方法,接口文档 的地址在这里。
1 2 3 4 5 6 7 8 9 10
|
import { http } from '@/utils/http'
export const orderPayApi = (data) => { return http.post('/patient/consult/pay', data) }
|
- 调用支付接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
| <!-- subpkg_consult/payment/index.vue --> <script setup> import { ref } from 'vue' import { useConsultStore } from '@/stores/consult' import { preOrderApi, createOrderApi } from '@/services/consult' import { patientDetailApi } from '@/services/patient' import { paymentApi } from '@/services/payment'
// 患者相关的数据(不具有响应性) const { type, illnessType, patientId, illnessInfo, depId } = useConsultStore()
// 省略前面小节的代码...
// 支付 async function onPaymentConfirm({ index }) { if (index === 0) return uni.utils.toast(' 暂不支持微信支付!')
// 调用后端提供的支付接口 const { code, data, message } = await paymentApi({ orderId: orderId.value, paymentMethod: index, payCallback: 'http://localhost:5173/#/subpkg_consult/room/index', })
// 接口是否调用成功 if (code !== 10000) return uni.utils.toast(message) // #ifdef H5 // 引导用户支付(地址跳转方式) window.location.href = data.payUrl // #endif
// #ifdef MP-WEIXIN // 引导用户支付(wx.requestPayment 小程序) wx.requestPayment({ // 4 个参数 }) // #endif } // 省略前面小节的代码... </script>
<template> <scroll-page> <!-- 省略前面小节的代码... --> <!-- 下一步操作 --> <view class="next-step"> <view class="total-amount"> 合计: <text class="number">¥{{ preOrderInfo.actualPayment }}</text> </view> <button @click="onPaymentButtonClick" class="uni-button"> 立即支付 </button> </view> </scroll-page> <!-- 支付渠道 --> <custom-payment @close="onPaymentClose" @confirm="onPaymentConfirm" :amount="preOrderInfo.actualPayment" :order-id="orderId" ref="paymentRef" /> </template>
|
接口参数说明:
orderId
待付订单的 ID,通过这个 ID 能查询到支付的金额
paymentMethod
支付方式 0
微信支付、1
支付宝、2
云闪付
payCallback
在支付完成后自动跳转的页面地址
沙箱应用账号,登录和支付密码都是 111111
askgxl8276@sandbox.com
scobys4865@sandbox.com
重点要注意的事项:
目前只有支付宝提供了沙箱开发环境且只能支持 H5 的环境
在调用支付接口成功后,根据回传会引导用户进行支付,但是引导的方式有多种
- 在 H5 环境中是通过跳转即
window.location.href
或 a
链接
- 在小程序环境中要调用
wx.requestPayment
- 在 PC 端可能提供一个二维码(本质还是一个链接)
- 在 App 端调用 SDK 提供的方法
但无论哪种方法都是要唤起手机上的支付宝 App 和微信 App 来进行支付。
1.4.4 微信支付
支付账号的申请受到限制且后端接口也暂未支持微信支付,暂不支持微信支付。
二、WebSocket
WebSockets 是一种先进的技术。它可以在用户的浏览器和服务器之间打开交互式通信会话。你可以向服务器发送消息并接收事件驱动的响应,而无需通过轮询服务器的方式以获得响应,比较典型的应用场景就是即时通讯(聊天)系统。
2.1 原生用法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>WebSocket 示例</title> </head> <body> <script> const ws = new WebSocket('wss://socketsbay.com/wss/v2/1/demo/') ws.onopen = function (ev) { console.log(' 建立连接...') ws.send('Hello WebSockets!') } ws.onclose = function (ev) { console.log(' 断开连接...') } ws.onmessage = function (ev) { console.log(' 收到消息: ' + ev.data) } </script> </body> </html>
|
通过以上的示例大家只需要对 WebSocket 建立起这样在印象:
- 采用的是
wss://
协议
- 分为客户端和服务端
- 实现连续的、长时间的与服务器通信
然而 WebSocket 提供的只是通信底层的机制,结合业务通常需进行二次封装,其中比较流行就是 socket.io
2.2 Socket.IO 库
Socket.IO 是基于 WebSocket 进行的二次封装,封装了更多的业务层面的逻辑,如身份认证等、事件驱动等。
注:其实 Socket.IO 不仅仅是封装了 WebSocket,还有基于 Ajax 的长轮询机制,在不支持 WebSocket 的环境中会自动降级为基于 Ajax 的长轮询机制。
长轮询机制可以粗暴的理解为在一个定时器中不断的重复发 Ajax 请求,以实现与服务器实时通信的功能。
- 下载 socket.io.js
在项目中导入
1 2 3 4 5
| import { io } from "socket.io-client";
const { io } = require("socket.io-client");
|
也可以使用 CDN 方式引入到项目当中:
1
| <script src="https://cdn.socket.io/4.4.1/socket.io.min.js"></script>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>socket.io 示例 </title> </head> <body> <script src="https://cdn.socket.io/4.4.1/socket.io.min.js"></script> <script> // 建立连接 const socket = io('http://localhost:3000', { transports: ['websocket', 'polling'], }) // 监听连接建立状态 socket.on('connect', () => { console.log(' 建立连接...') }) // 监听连接断开状态 socket.on('disconnect', () => { console.log(' 断开连接...') }) </script> </body> </html>
|
事件驱动,所谓的事件驱动是指服务端与客户端的通信过程是基于 自定义事件 来实现的:
1 2 3 4 5 6 7 8 9
| const socket = io('http://localhost:3000', { transports: ['websocket', 'polling'], })
socket.on('wait-some-data', (msg) => { console.log(msg) })
|
emit
方法用来触发一个事件,这个事件名必须是服务端已经定义好的
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| const socket = io('http://localhost:3000', { transports: ['websocket', 'polling'], })
socket.on('wait-some-data', (msg) => { console.log(msg) })
socket.emit('send-some-data', ' 这里是数据 ', () => { })
|
提供了 socket.io 服务端的示例大家,供大家学习 socket.io 的用法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| import { createServer } from 'http' import { Server } from 'socket.io' import path from 'path'
const server = createServer() const io = new Server(server)
io.on('connection', (client) => { console.log(' 建立连接...') client.on('sendToServer', (msg) => { console.log(' 收到了客户端的数据: ' + msg)
const messages = [' 你好!', ' 我在写代码 ', ' 快下课了吧?'] const index = Math.floor(Math.random() * 3)
io.emit('sendToClient', messages[index]) })
client.on('disconnect', () => { console.log(' 断开连接...') }) })
server.listen(3000, () => { console.log('server start') })
|