uni-app(优医咨询)项目实战 - 第 9 天
学习目标:
- 能够完成问诊订单的支付及详情
- 能够完成问诊订单删除 / 取消操作功能
- 能够完成药品订单的支付及查看
一、uniCloud 云开发
1.1 云函数
在使用云函数之前需要开通 uniCloud 服务空间。
云函数即在云空间(服务器)上的函数,云函数中封装的逻辑其实就是后端程序的代码,比如可以执行操作数据库的操作等。
在前面的步骤中只是获得了用户的授权,并未真正拿到用户的手机号,需要服务端直接或间接的调用中国移动、中国联通、中国电信的接口才可拿到用户的手机号码,我们将这部分逻辑封装进云函数当中。
- 创建云函数
默认生成的代码为:
1 2 3 4 5 6 7 8
| 'use strict'; exports.main = async (event, context) => { console.log('event : ', event) return event }
|
- 调用云函数
在 uni-app 中通过 uni.callFunction 专门调用云端的函数:
1 2 3 4 5 6 7 8 9 10 11 12
| <!-- subpkg_medicine/timeline/index.vue --> <script setup> // 调用云函数 uniCloud.callFunction({ name: ' 云函数名(即文件名)', data: {/* 给云函数传的实参 */}, success: (result) => { // result 是云函数 return 的返回值 } }) </script>
|
在对云函数进行调试时需要安装 HBuilderX 的插件,点击命令行窗口右上角的【开启断点调试】就会自动对插件进行下载安装了。
- 部暑云函数,云函数在本地开发调试完毕后,需要部暑上线到云空间,在 App 端调试本地云函数时要求手机必须与电脑处于相同的网络下。
1.2 地图服务
1.2.1 腾讯地图
- 创建应用并申请 Key,为了在 uni-app 中同时兼容 H5 端、App 端和小程序端选择 WebServiceAPI
- 找到 WebServiceAPI 文档
- 路线规划
腾讯地址提供了 API 来获取路线规划的数据,该 API 在浏览器端请求会有跨域的限制,也可以在服务端(云函数)调用 API。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 'use strict'
exports.main = async (event, context) => { const { data } = await uniCloud.request({ url: 'https://apis.map.qq.com/ws/direction/v1/walking/', data: { key: 'FFABZ-43ZHH-72BDF-WAD6P-FDZYK-UHBOQ', from: '40.060539,116.343847', to: '40.086757,116.328634', }, }) return data }
|
- 运行本地云函数进行调试,在云函数目录上右键,也可以选择上传并运行。
1.2.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
| <!-- subpkg_consult/medicine/timeline/index.vue --> <script setup> import { computed } from 'vue'
// 胶囊距离顶部的距离 let top = 0
// #ifdef MP-WEIXIN const menuButton = uni.getMenuButtonBoundingClientRect() top = menuButton.top - (50 - menuButton.height) / 2 + 'px' // #endif
function onBackClick() { uni.navigateBack({ delta: 1 }) } </script>
<template> <view class="timeline-page"> <map latitude="40.0586531" longitude="116.3385867" class="uni-map" /> <cover-view :style="{ paddingTop: top }" class="navigator-bar"> <cover-view @click="onBackClick" class="icon-back"> <cover-image src="/static/images/icon-back.png" /> </cover-view>
<cover-view class="title"> 派送中 </cover-view> </cover-view> <cover-view class="timeline-meta"> <cover-view class="status"> 订单派送中 </cover-view> <cover-view class="extra"> 预计明天送达 申通快递 7511266366963366 </cover-view> </cover-view> <view class="timeline-detail"> <view class="title"> 物流详情 </view> <view class="timeline"> <view class="line"> <view class="badge text"> 收 </view> <view class="content"> 收货地址:广东省广州市大华区明离路科技园 880 号 </view> </view> <view class="line"> <view class="badge icon"> <uni-icons custom-prefix="iconfont" color="#2cb5a5" size="15" type="icon-checked" /> </view> <view class="label"> 已签收 </view> <view class="content"> 您的订单已由本人签收。如有疑问请联系配送员【赵赵,18332566962】确认。感谢您在优医购用,欢迎再次光临。 </view> <view class="time"> 今天 10:25</view> </view> <view class="line"> <view class="badge icon"> <uni-icons color="#2cb5a5" custom-prefix="iconfont" type="icon-truck" /> </view> <view class="label"> 派送中 </view> <view class="content"> 您的订单正在派送中【深圳市】科技园派送员宋平正在为您派件 </view> <view class="time"> 今天 10:25</view> </view> <view class="line"> <view class="badge dot"></view> <view class="label"> 运输中 </view> <view class="content"> 在广东深圳公司进行发出扫描 </view> <view class="time"> 今天 10:25</view> </view> <view class="line"> <view class="badge dot"></view> <view class="content"> 在分拨中心广东深圳公司进行卸车扫描 </view> <view class="time"> 今天 10:25</view> </view> </view> </view> </view> </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 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145
| .timeline-page { padding-bottom: calc(env(safe-area-inset-bottom) + 60rpx); }
.uni-map { width: 100%; height: 470px; }
.navigator-bar { position: fixed; top: 10px; left: 10px; right: 10px; height: 44px; display: flex; align-items: center; justify-content: center; font-size: 16px; border-radius: 4px; background-color: #fff;
top: 25px;
height: 50px; top: 0; left: 0px; right: 0px; border-radius: 0;
.icon-back { width: 16px; height: 16px; position: absolute; left: 0; padding: 14px; } }
.timeline-meta { position: absolute; top: 380px; left: 10px; right: 10px;
line-height: 1; padding: 15px 20px; border-radius: 4px; background-color: #fff;
.status { font-size: 15px; color: #121826; font-weight: 500; }
.extra { font-size: 14px; color: #6f6f6f; margin-top: 10px;
} }
.timeline-detail { padding: 0 30rpx;
.title { line-height: 1; padding: 40rpx 0; font-size: 30rpx; color: #121826; font-weight: 500; }
.timeline { min-height: 300rpx; margin-left: 30rpx; margin-top: 20rpx;
padding: 10rpx 60rpx 1rpx; border-left: 4rpx solid #16c2a3; }
.line { margin-bottom: 30rpx; position: relative; }
.badge { position: absolute; left: -92rpx; top: -10rpx; display: flex; align-items: center; justify-content: center; width: 60rpx; height: 60rpx; border-radius: 50%;
&.text { color: #2cb5a5; font-size: 24rpx; background-color: #eaf8f6; }
&.icon { background-color: #f6f7f9; }
&.dot::before { content: ''; display: block; width: 24rpx; height: 24rpx; background-color: #16c2a3; border-radius: 50%; } }
.label { font-size: 32rpx; font-weight: 500; color: #6f6f6f; margin-bottom: 10rpx; }
.content { color: #848484; font-size: 28rpx; margin-bottom: 10rpx; }
.time { font-size: 28rpx; font-weight: 500; color: #6f6f6f; } }
|
- 调用接口查看物流起止点,接口文档 的地址在这里
1 2 3 4 5 6 7 8 9 10
| import { http } from '@/utils/http.js'
export const logisticsApi = (id) => { return http.get(`/patient/order/${id}/logistics`) }
|
- 在页面中调用接口获取数据并渲染
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
| <!-- subpkg_consult/medicine/timeline/index.vue --> <script setup> import { ref } from 'vue' import { logisticsApi } from '@/services/medicine'
// 获取地址参数 const props = defineProps({ id: String, }) // 当前位置 const logisticsInfo = ref({})
// 省略前面小节的代码...
// 调用云函数获取路线坐标点 uniCloud.callFunction({ name: 'qq-maps', data: { from: '40.060539,116.343847', to: '40.086757,116.328634', }, success({ result }) { console.log(result) }, fail(error) { console.log(error) }, })
// 查看物流信息 async function getLogistics() { // 调用接口 const { code, data, message } = await logisticsApi(props.id) // 判断接口是否调用成功 if (code !== 10000) return uni.utils.toast(message) // 渲染数据 logisticsInfo.value = data }
// 省略前面小节的代码... // 获取物流信息 getLogistics() </script>
<template> <view class="timeline-page"> <map latitude="40.0586531" longitude="116.3385867" class="uni-map" /> <cover-view :style="{ paddingTop: top }" class="navigator-bar"> <cover-view @click="onBackClick" class="icon-back"> <cover-image src="/static/images/icon-back.png" /> </cover-view>
<cover-view class="title">{{ logisticsInfo.statusValue }}</cover-view> </cover-view> <cover-view class="timeline-meta"> <cover-view class="status">{{ logisticsInfo.statusValue }}</cover-view> <cover-view class="extra"> 预计 {{ logisticsInfo.estimatedTime }} 送达 {{ logisticsInfo.name }} {{ logisticsInfo.awbNo }} </cover-view> </cover-view> <view class="timeline-detail"> <view class="title"> 物流详情 </view> <view class="timeline"> <view class="line"> <view class="badge text"> 收 </view> <view class="content"> 收货地址:广东省广州市大华区明离路科技园 880 号 </view> </view> <view v-for="item in logisticsInfo.list" :key="item.id" class="line"> <view class="badge dot" v-if="item.status <= 3"></view> <view class="badge icon" v-if="item.status === 4"> <uni-icons color="#2cb5a5" custom-prefix="iconfont" type="icon-truck" /> </view> <view class="badge icon" v-if="item.status === 5"> <uni-icons color="#2cb5a5" custom-prefix="iconfont" type="icon-checked" /> </view> <view class="label">{{ item.statusValue }}</view> <view class="content">{{ item.content }}</view> <view class="time">{{ item.createTime }}</view> </view> </view> </view> </view> </template>
|
物流状态如下表:
物流状态 |
说明 |
备注 |
1 |
已发货 |
类名 .dot,无图标 |
2 |
已揽件 |
类名 .dot,无图标 |
3 |
运输中 |
类名 .dot,无图标 |
4 |
派送中 |
类名 .icon,图标 icon-truck |
5 |
已签收 |
类名 .icon,图标 icon-checked |
1.2.3 路线规划
调用腾讯地图获取到的路线是以压缩后的经纬度坐标方式呈现的,在使用之前需要将其按着官方提供的方法对经纬度坐标解压缩,然后再结合 map
地图组件来显示物流配送的线路。
- 解压缩经纬度坐标
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_medicine/timeline/index.vue --> <script setup> // 省略前面小节的代码... // 调用云函数获取路线坐标点 uniCloud.callFunction({ name: 'qq-maps', data: { from: '40.060539,116.343847', to: '40.086757,116.328634', }, success({ result }) { // 腾讯地图返回的数据 const coords = result.result.routes[0].polyline const points = []
// 坐标解压(返回的点串坐标,通过前向差分进行压缩) for (let i = 2; i < coords.length; i++) { coords[i] = Number(coords[i - 2]) + Number(coords[i]) / 1000000 }
// 将解压后的坐标放入点串数组 points 中 for (let i = 0; i < coords.length; i += 2) { points.push({ latitude: coords[i], longitude: coords[i + 1] }) } // 处理后的经纬度坐标 console.log(points) }, }) // 省略前面小节的代码... </script>
|
地图组件 map
latitude
地图中心点的纬度
longitude
地图中心点的经度
scale
地图的缩放比例,取值范围 3-20
markers
地图上的标记
polyline
运输路线轨迹
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
| <!-- subpkg_medicine/timeline/index.vue --> <script setup> import { ref } from 'vue' import { logisticsApi } from '@/services/medicine'
// 省略前面小节的代码...
// 地图路线 const polyline = ref([]) // 起点终点标记 const markers = ref([ { id: 1, latitude: 40.060539, longitude: 116.343847, iconPath: '/static/images/start.png', width: 25, height: 30, }, { id: 2, latitude: 40.086757, longitude: 116.328634, iconPath: '/static/images/end.png', width: 25, height: 30, },
{ id: 3, latitude: 40.083465, longitude: 116.332938, iconPath: '/static/images/car.png', width: 50, height: 30, }, ]) // 省略前面小节的代码...
// 调用云函数获取路线坐标点 uniCloud.callFunction({ name: 'qq-maps', data: { from: '40.060539,116.343847', to: '40.086757,116.328634', }, success({ result }) { // 省略前面小节的代码... // 运输路线轨迹 polyline.value.push({ points, color: '#16c2a3', width: 5, }) }, fail(error) { console.log(error) }, }) // 省略前面小节的代码... </script>
<template> <view class="timeline-page"> <map :markers="markers" :polyline="polyline" latitude="40.0586531" longitude="116.3385867" scale="14" class="uni-map" /> <!-- 省略前面小节的代码... --> </view> </template>
|
- 替换地图组件数据
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 113 114
| <!-- subpkg_medicine/timeline/index.vue --> <script setup> // 省略前面小节的代码...
// 地图路线 const polyline = ref([]) // 起点终点标记 const markers = ref([]) // 地图中心点 const center = ref({})
// 省略前面小节的代码...
// 查看物流信息 async function getLogistics() { // 调用接口 const { code, data, message } = await logisticsApi(props.id) // 判断接口是否调用成功 if (code !== 10000) return uni.utils.toast(message) // 渲染数据 logisticsInfo.value = data
// 起始点坐标 const from = data.logisticsInfo[0] // 终止点坐标 const to = data.logisticsInfo[data.logisticsInfo.length - 1] // 线路标记点,起点、终点 markers.value = [ { id: 1, latitude: from.latitude, longitude: from.longitude, iconPath: '/static/images/start.png', width: 25, height: 30, }, { id: 2, latitude: to.latitude, longitude: to.longitude, iconPath: '/static/images/end.png', width: 25, height: 30, }, ]
// 调用云函数获取路线坐标点 uniCloud.callFunction({ name: 'qq-maps', data: { from: Object.values(from).join(','), to: Object.values(to).join(','), }, success({ result }) { const coords = result.result.routes[0].polyline const points = []
// 坐标解压(返回的点串坐标,通过前向差分进行压缩) for (let i = 2; i < coords.length; i++) { coords[i] = Number(coords[i - 2]) + Number(coords[i]) / 1000000 }
// 将解压后的坐标放入点串数组 pl 中 for (let i = 0; i < coords.length; i += 2) { points.push({ latitude: coords[i], longitude: coords[i + 1] }) }
// 假设位置 const current = points[100] // 地图中心点为运输车辆所在位置 center.value = current // 标记运输车辆的位置 markers.value.push({ id: 3, latitude: current.latitude, longitude: current.longitude, iconPath: '/static/images/car.png', width: 50, height: 30, }) // 路线轨迹 polyline.value.push({ points, color: '#16c2a3', width: 5, }) }, fail(error) { console.log(error) }, }) }
// 省略前面小节的代码... </script>
<template> <view class="timeline-page"> <map :polyline="polyline" :markers="markers" :latitude="center.latitude" :longitude="center.longitude" scale="12" class="uni-map" /> <!-- 省略前面小节的代码... --> </view> </template>
<style lang="scss"> @import './index.scss'; </style>
|
1.3 实人认证(扩展)
实人认证是通过视频方式来验证用户身份的技术,uniCloud 提供了简单方便的实现方式。
1.3.2 开通服务
参见 官方文档
2.3.2 配置优医咨询
2.3.3 客户端(App)调用
- 获取设备信息
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
| <!-- subpkg_archive/form/index.vue --> <script setup> // 省略前面小节的代码... // 提交表单数据 async function onFormSubmit() { try { // 根据验证规则验证数据 const formData = await formRef.value.validate() /******** 实人认证 ********/ // #ifdef APP // 1. 获取设备信息 const metaInfo = uni.getFacialRecognitionMetaInfo() console.log(metaInfo) // #endif /******** 实人认证 ********/
// #ifndef APP // 添加患者或更新患者 props.id ? updatePatient() : addPatient() // #endif } catch (error) { console.log(error) } } // 省略前面小节的代码... </script>
|
- 创建云函数获取
certifyId
创建好云函数之后,添加如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 'use strict' exports.main = async (event, context) => { const frvManager = uniCloud.getFacialRecognitionVerifyManager({ requestId: context.requestId, })
const result = await frvManager.getCertifyId({ realName: event.realName, idCard: event.idCard, metaInfo: event.metaInfo, })
return result }
|
在调用云函数时需要 App 客户端传入 3 个参数:
realName
验证用户的真实姓名
idCard
验证用户的身份证号
metaInfo
设备信息
- 客户端 App 调用云函数
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
| <!-- subpkg_archive/form/index.vue --> <script setup> // 省略前面小节的代码... // 提交表单数据 async function onFormSubmit() { try { // 根据验证规则验证数据 const formData = await formRef.value.validate() /******** 实人认证 ********/ // #ifdef APP // 1. 获取设备信息 const metaInfo = uni.getFacialRecognitionMetaInfo() // 2. 调用云函数 uniCloud.callFunction({ name: 'uni-verify', data: { metaInfo, realName: formData.value.name, idCard: formData.value.idCard }, success() {} }) // #endif /******** 实人认证 ********/
// #ifndef APP // 添加患者或更新患者 props.id ? updatePatient() : addPatient() // #endif } catch (error) { console.log(error) } } // 省略前面小节的代码... </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 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
| <!-- subpkg_archive/form/index.vue --> <script setup> // 省略前面小节的代码... // 提交表单数据 async function onFormSubmit() { try { // 根据验证规则验证数据 const formData = await formRef.value.validate() /******** 实人认证 ********/ // #ifdef APP // 1. 获取设备信息 const metaInfo = uni.getFacialRecognitionMetaInfo() // 2. 调用云函数 uniCloud.callFunction({ name: 'uni-verify', data: { metaInfo, realName: formData.value.name, idCard: formData.value.idCard }, success({ result }) { // 3. 客户端调起 sdk 刷脸认证 uni.startFacialRecognitionVerify({ certifyId: result.certifyId, success() { // 添加患者或更新患者 props.id ? updatePatient() : addPatient() }, fail() { uni.utils.toast(' 实人认证失败!') }, }) }, }) // #endif /******** 实人认证 ********/
// #ifndef APP // 添加患者或更新患者 props.id ? updatePatient() : addPatient() // #endif } catch (error) { console.log(error) } } // 省略前面小节的代码... </script>
|
1.4 统一支付(扩展)
通过上述支付流程的分析实现后发现,要支持多个支付平台和多个项目运行平台,开发的工作量相当的大,针对这种情况市场上出现了整合了多种支付方式的第三方机构,可以高效率的将支付引入到项目当中,如 ping+
uni-app 本身也提供了聚合支付(统一支付)的功能模块 uni-pay
,只要配置自已的支付平台的账号、认证证书等信息即可完成支付。
1.4.1 安装插件
首先通过插件市场 安装 uni-pay
, 同时将示例项目也下载到本地,然后再了解插件的相关使用方法。
安装完成后还手动执行两个操作:
- 在配置文件是新增一个分包,分包的页面已经下载好了,直接添加如下配置即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| { "subPackages": [ { "root": "uni_modules/uni-pay/pages", "pages": [ { "path": "success/success", "style": { "navigationBarTitleText": " 支付成功 ", "backgroundColor": "#F8F8F8" } }, { "path": "ad-interactive-webview/ad-interactive-webview", "style": { "navigationBarTitleText": "ad", "backgroundColor": "#F8F8F8" } } ] } ], }
|
- 把下载的示例文件中的
uni-pay
和 uni-id
拷贝到项目云空间目录 uni-config-center
中。
1.4.2 申请支付账号
申请微信支付账号
申请支付宝支付账号
- 以支付宝为例(只有支付宝有沙箱账号),找到沙箱应用启用证书模式
- 获取私钥并下载证书
补充说明:以上的证书和私钥是沙箱环境自动给创建好的,如果在企业中开发时正式应用所对应的证书和私钥需要自行生成,生成证书 的文档在这里。
微信支付账号与支付宝支付的流程相似,也需要根据文档去生成证书等。
1.4.3 完善配置
在配置文件中填写支付账号的相关信息,如商户号、密钥、证书等,这些信息需要自去微信或支付宝平台申请。
以支付宝为例,使用支付宝有沙箱账号进行演示,然而 uni-pay 却不支持支付宝沙箱环境。
把下载好的支付宝相关的 3 个证书,拷贝到项目云空间目录中 uni-config-center/uni-pay/alipay
,证书的文件名称要相应的调整:
appPublicCert.crt
==> appCertPublicKey.crt
alipayPublicCert.crt
==> alipayCertPublicKey_RSA2.crt
alipayRootCert.crt
==> alipayRootCert.crt
(这个没变)
支付回调地址,格式为 “ 服务空间 ID”: “URL 化地址 “,登录到 uniCloud 控制台,进入到云空间,在云函数 / 对象列表中找到云对象 uni-pay-co
,再进入详情就可以看到 URL 化的地址了。
1 2 3 4 5 6 7 8 9 10 11
| const fs = require('fs'); const path = require('path') module.exports = { "notifyUrl": { "fc-mp-664d2554-cfa5-4427-9adf-d14025991d5f": "https://fc-mp-664d2554-cfa5-4427-9adf-d14025991d5f.next.bspapp.com/uni-pay-co", "fc-mp-664d2554-cfa5-4427-9adf-d14025991d5f": "https://fc-mp-664d2554-cfa5-4427-9adf-d14025991d5f.next.bspapp.com/uni-pay-co", }, }
|
- AppId 在支付宝平台找到沙箱应用的 APPID
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
| const fs = require('fs'); const path = require('path') module.exports = { "notifyUrl": { "fc-mp-664d2554-cfa5-4427-9adf-d14025991d5f": "https://fc-mp-664d2554-cfa5-4427-9adf-d14025991d5f.next.bspapp.com/uni-pay-co", "fc-mp-664d2554-cfa5-4427-9adf-d14025991d5f": "https://fc-mp-664d2554-cfa5-4427-9adf-d14025991d5f.next.bspapp.com/uni-pay-co", }, mp: { appId: '9021000124653462', privateKey: '', }, app: { appId: '9021000124653462', privateKey: '', }, native: { appId: '9021000124653462', privateKey: '', }, }
|
- privateKey 私钥
私钥的字符内容太长了,请自行拷贝到配置文件中。
- 上传公共模块、云对象,包括
uni-config-center
、uni-pay
、uni-pay-co
配置完成后重新启动项目!!
1.4.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
| <!-- 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 paymentRef = ref() const uniPayRef = ref()
// 立即支付(生成待支付订单) async function onPaymentButtonClick() { // 省略前面小节的代码...
// 检测接口是否计用成功 if (code !== 10000) return uni.utils.toast(message) // 获取待支付订单 ID orderId.value = data.id
// 选择支付渠道 // paymentRef.value.open() uniPayRef.value.open({ total_fee: 1, // 支付金额,单位分 100 = 1 元 order_no: data.id, // 业务系统订单号(即你自己业务系统的订单表的订单号) description: ' 问诊订单 ', // 支付描述 type: 'consult', // 支付回调类型,可自定义, }) } // 省略前面小节的代码... </script>
<template> <!-- 省略前面小节的代码... -->
<!-- 统一支付 uni-pay --> <uni-pay ref="uniPayRef"></uni-pay>
<!-- 支付渠道 --> <custom-payment @close="onPaymentClose" @confirm="onPaymentConfirm" :amount="preOrderInfo.actualPayment" :order-id="orderId" ref="paymentRef" /> </template>
|
没有支付成功的主要原因是因为课堂中无法获取完全的开发资质,但是其使用的基本步骤和流程确定的,只需要将支付平台提供的信息填写到配置文件中就可以了。
1.5 一键登录(扩展)
一键登录是通过运营商(中国移动、中国电信、中国联通)来获取用户手机号,进而实现用户登录的功能。
1.5.1 开通服务
参见 官方文档
1.5.2 配置优医咨询
1.5.3 客户端(App)调用
在 uni-app 中通过调用 API uni.login
即可唤起一键登录请求授权
1 2 3 4 5 6
| uni.login({ provider: 'univerify', univerifyStyle: { fullScreen: true, }, })
|
provider
: 指定登录服务类型,univerify
表示是一键登录
univerifystyle
自定义授权界面的样式
- 调用
uni.login
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <!-- pages/login/index.vue --> <script setup> // 省略前面小节的代码... /******** 一键登录(仅支持 App) ********/ // #ifdef APP function onWeiboButtonClick() { uni.login({ provider: 'univerify', univerifyStyle: { fullScreen: true, }, }) } // #endif /******** 一键登录(仅支持 App) ********/ </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 26 27 28 29 30
| <!-- pages/login/index.vue --> <script setup> // 省略前面小节的代码... /******** 一键登录(仅支持 App) ********/ // #ifdef APP function onWeiboButtonClick() { uni.login({ provider: 'univerify', univerifyStyle: { fullScreen: true, icon: { path: 'static/logo.png', // 自定义显示在授权框中的 logo,仅支持本地图片 默认显示 App logo }, authButton: { normalColor: '#16c2a3', // 授权按钮正常状态背景颜色 默认值:#3479f5 highlightColor: '#16c2a3', // 授权按钮按下状态背景颜色 默认值:#2861c5(仅 ios 支持) }, privacyTerms: { defaultCheckBoxState: false, uncheckedImage: 'static/images/uncheckedImage.png', checkedImage: 'static/images/checkedImage.png', termsColor: '#16c2a3', }, }, }) } // #endif /******** 一键登录(仅支持 App) ********/ </script>
|
- 创建云函数
默认生成的代码为:
1 2 3 4 5 6 7 8
| 'use strict'; exports.main = async (event, context) => { console.log('event : ', event) return event }
|
- 调用
uniCloud.getPhoneNumber
获取用户手机号
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 'use strict' exports.main = async (event, context) => { const res = await uniCloud.getPhoneNumber({ appid: context.APPID, provider: 'univerify', apiKey: 'fc7aa3fb2672f947a501f2392a22501a', apiSecret: 'b4d9fafeb3c3ed12ff6cc97b9f9a1817', access_token: event.access_token, openid: event.openid, })
console.log(res) return { code: 200, message: ' 获取手机号成功 ', } }
|
- 调用云函数
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
| <!-- pages/login/index.vue --> <script setup> // 省略前面小节的代码... /******** 一键登录(仅支持 App) ********/ // #ifdef APP function onWeiboButtonClick() { uni.login({ provider: 'univerify', univerifyStyle: { // 省略前面小节的代码... }, success() { // 获取运营商提供的手机号 uniCloud.callFunction({ name: 'uni-login', success({ result }) { console.log(result) // 调用后端接口,发送手机号码完成登录 }, }) } }) } // #endif /******** 一键登录(仅支持 App) ********/ </script>
|
如上图所示,在获取用户手机号时需要为云函数指定依赖 uni-cloud-verify
,添加至云函数的 package.json
中,另外就是这获取用户手机号码是需要付费的,充值付费后才可以使用。
二、QQ 登录
2.1 网站接入流程
2.1.1 申请 Appid 和 Appkey
申请流程见 官方文档,申请 appid
和 appkey
需要认证等严格条件,大家了解一下流程,课程中使用学校提供好的 appid
和 appkey
- appid: 102015968
- appkey: 没有提供(暂时用不上)
2.1.2 获取 Access_Token
按官方文档说明 使用 Implicit_Grant 方式 获取 Access_Token
参数 |
含义 |
response_type |
授权类型,此值固定为 token |
client_id |
申请 QQ 登录成功后,分配给应用的 appid |
redirect_uri |
成功授权后的回调地址,必须是注册 appid 时填写的主域名下的地址,建议设置为网站首页或网站的用户中心。注意需要将 url 进行 URLEncode |
QQ 互联平台要求 redirect_uri
必须是备案的域名,本地开发使用的确是 localhost 或 127.0.0.1,为了解决调试的问题,需要本地开发服务器做一些配置调整:
- 将本地服务器的端口设置为 80
- 将本地服务器的路由模式设置为 history
- 为本地服务器配置一个域名,这个域名在 QQ 互联平台配置过了
- 配置 uniCloud 云函数跨域访问
按接口文档的说明拼接链接地址,请求 QQ 互联平台的接口:
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
| <!-- pages/login/index.vue --> <script setup> // 省略前面小节的代码...
// QQ 登录(仅支持 H5 端) // #ifdef H5 function onQQButtonClick() { window.location.href = `https://graph.qq.com/oauth2.0/authorize?response_type=token&client_id=102015968&redirect_uri=http://consult-patients.itheima.net/pages/login/qq` } // #endif
// 省略前面小节的代码... </script>
<template> <!-- 省略前面小节的代码... --> <!-- 社交账号登录 --> <view class="social-login"> <view class="legend"> <text class="text"> 其它方式登录 </text> </view> <view class="social-account"> <!-- #ifdef H5 --> <view @click="onQQButtonClick" class="icon"> <uni-icons color="#00b0fb" size="30" type="qq" /> </view> <!-- #endif --> <!-- 省略前面小节的代码... --> </view> </view> </template>
|
用户点击 QQ 登录的链接后会打开 QQ 客户端使用用户的 QQ 进行登录,登录成功后跳转到 redirect_uri
指定的 URL 地址。
新建一个页面在 QQ 登录成功后绑定手机号,页面的模板如下所示:
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 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
| <!-- pages/login/qq.vue --> <script setup></script> <template> <view class="user-login"> <view class="welcome"> <image class="avatar" src="/static/uploads/doctor-avatar-2.png"></image> <text class="nickname"> Hi,lotjol 欢迎来优医咨询,完成绑定后可以 QQ 账号一键登录哦~ </text> </view>
<uni-forms class="login-form" ref="form"> <uni-forms-item name="name"> <uni-easyinput :input-border="false" :clearable="false" placeholder=" 请输入手机号 " placeholder-style="color: #C3C3C5" /> </uni-forms-item> <uni-forms-item name="name"> <uni-easyinput :input-border="false" :clearable="false" placeholder=" 请输入验证码 " placeholder-style="color: #C3C3C5" /> <text class="text-button"> 获取验证码 </text> </uni-forms-item>
<view class="agreement"> <uni-icons v-if="true" size="18" color="#16C2A3" type="checkbox-filled" /> <uni-icons v-else size="18" color="#d1d1d1" type="circle" /> 我已同意 <text class="link"> 用户协议 </text> 及 <text class="link"> 隐私协议 </text> </view>
<button class="uni-button"> 绑定手机号 </button> <button class="uni-button secondary"> 暂不绑定 </button> </uni-forms> </view> </template>
<style lang="scss"> .user-login { padding: 120rpx 60rpx 0; }
.welcome { display: flex; align-items: center; margin-bottom: 60rpx;
.avatar { width: 100rpx; height: 100rpx; margin-right: 20rpx; border-radius: 10rpx; }
.nickname { flex: 1; font-size: 28rpx; color: #3c3e42; } }
.uni-forms-item { height: 80rpx; margin-bottom: 30rpx !important; border-bottom: 1rpx solid #ededed; box-sizing: border-box; position: relative; }
.agreement { font-size: 26rpx; color: #3c3e42; display: flex; align-items: center; margin-top: 50rpx; margin-left: -10rpx;
.link { color: #16c2a3; } }
:deep(.uni-forms-item__content) { display: flex; align-items: center; }
:deep(.uni-forms-item__error) { width: 100%; padding-top: 10rpx; padding-left: 10rpx; border-top: 2rpx solid #eb5757; color: #eb5757; font-size: 24rpx; transition: none; }
.text-button { display: flex; justify-content: flex-end; width: 240rpx; padding-left: 10rpx; font-size: 28rpx; color: #16c2a3; border-left: 2rpx solid #eee; }
.uni-button { margin-top: 50rpx;
&.secondary { background-color: #50c8fd; }
&[disabled] { background-color: #fafafa; color: #d9dbde; } }
.uni-navigator { margin-top: 30rpx; text-align: center; color: #848484; font-size: 28rpx; } </style>
|
不要忘记为新建的页面添加配置:
1 2 3 4 5 6 7 8 9 10 11 12
| { "pages": [ ... { "path": "pages/login/qq", "style": { "navigationBarTitleText": "QQ 登录 ", "enablePullDownRefresh": false } } ] }
|
2.1.3 获取用户 OpenID
OpenID 的获取借助于 uniCloud 的云函数,分两个步骤来实现:
- 新建名称为 qq-connect 的云函数,按 官方文档 QQ 互联的接口
- 接口地址:
https://graph.qq.com/oauth2.0/me
- 请求方法:
GET
- 请求参数
参数 |
含义 |
access_token |
在上一步中获取到的 access token |
fmt |
默认是 jsonpb 格式,如果填写 json,则返回 json 格式 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 'use strict' exports.main = async (event, context) => { const { data: { client_id, openid } } = await uniCloud.request({ url: 'https://graph.qq.com/oauth2.0/me', data: { access_token: event.access_token, fmt: 'json', }, })
return { openid } }
|
- 获取地址参数
access_token
然后调用云函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <!-- pages/login/qq.vue --> <script setup>
// 调用云函数获取用户信息 uniCloud.callFunction({ name: 'qq-connect', // 地址中的 access_token 参数 data: { access_token: location.hash.slice(14, 46) }, async success({ result }) { console.log(result) }, fail(error) { console.log(error) }, }) </script>
<template> ... </template>
|
2.1.4 OpenAPI 调用
OpenAPI 是 QQ 互联平台提供的供开发者获取 QQ 用户数据的方法,如获取个人信息,在调用 OpenAPI 时仍然会用到云函数,调用方式如 文档说明:
- 接口地址:
https://graph.qq.com/user/get_user_info
- 请求方式:
GET
- 请求参数
参数 |
含义 |
access_token |
|
oauth_consumer_key |
|
openid |
|
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
| 'use strict' exports.main = async (event, context) => { const { data: { client_id, openid }, } = await uniCloud.request({ url: 'https://graph.qq.com/oauth2.0/me', data: { access_token: event.access_token, fmt: 'json', }, })
const { data: { nickname, figureurl_2 }, } = await uniCloud.request({ url: 'https://graph.qq.com/user/get_user_info', data: { access_token: event.access_token, oauth_consumer_key: client_id, openid }, })
return { openid, nickname, avatar: figureurl_2 } }
|
2.2 绑定手机号
2.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
| <!-- pages/login/qq.vue --> <script setup> import { ref } from 'vue' // QQ 用户信息 const userInfo = ref({}) // 调用云函数获取用户信息 uniCloud.callFunction({ name: 'qq-connect', data: { access_token: location.hash.slice(14, 46) }, async success({ result }) { // QQ 用户数据 userInfo.value = result }, fail(error) { console.log(error) }, }) </script>
<template> <view class="user-login" v-if="!isBinding"> <view class="welcome"> <image class="avatar" :src="userInfo.avatar"></image> <text class="nickname"> Hi,{{ userInfo.nickname }} 欢迎来优医咨询,完成绑定后可以 QQ 账号一键登录哦~ </text> </view>
<!-- 省略前面小节的代码... --> </view> </template>
|
2.2.2 检测绑定状态
在绑定手机号后可以直接进行登录,没有绑定手机号时才会显示绑定手机号的页面,实现该功能需要 调用接口,分成两个步骤来实现:
- 封装调用接口的方法
1 2 3 4 5 6 7 8 9 10
|
export const QQLoginApi = (data) => { return http.post('/login/thirdparty', 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
| <!-- pages/login/qq.vue --> <script setup> import { ref } from 'vue' import { bindingMobileApi, QQLoginApi, verifyCodeApi } from '@/services/user' import { useUserStore } from '@/stores/user'
// 省略前面小节的代码...
// 是否绑定手机 const bindingFlag = ref(true)
// 调用云函数获取用户信息 uniCloud.callFunction({ name: 'qq-connect', data: { access_token: location.hash.slice(14, 46) }, async success({ result }) { // QQ 用户数据 userInfo.value = result // 调用接口,如果已绑定手机号则直接登录 const { code, data, message } = await QQLoginApi({ source: 'qq', ...userInfo.value, }) // 登录成功后跳转 if (code === 10000) return jumpRoute(data) // 是否绑定手机号 if (code === 10001) bindingFlag.value = data.bindingFlag }, fail(error) { console.log(error) }, }) // 跳转跳由 function jumpRoute(data) { // Pinia 用户数据 const userStore = useUserStore() // 记录登录状态 userStore.token = data.token // 用户 ID userStore.userId = data.id // 地址重定向或 switchTab if (userStore.openType === 'switchTab') { uni.switchTab({ url: userStore.redirectURL }) } else { uni.redirectTo({ url: userStore.redirectURL }) } } </script>
<template> <view class="user-login" v-if="!bindingFlag"> <!-- 省略前面小节的代码... --> </view> </template>
|
2.2.3 发送短信验证码
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
| <!-- pages/login/qq.vue --> <script setup> import { ref } from 'vue' import { verifyCodeApi } from '@/services/user' // 省略前面小节的代码... // 表单数据 const formData = ref({ mobile: '13212345678', code: '', })
// 省略前面小节的代码...
// 获取验证码 async function onTextButtonClick() { // 调用接口,发送短信... const { code, message } = await verifyCodeApi({ mobile: formData.value.mobile, /** 注意类型参数据 **/ type: 'bindMobile', /** 注意类型参数据 **/ }) // 检测接口是否调用成功 if (code !== 10000) return uni.utils.toast(message) uni.utils.toast(' 验证码已发送,请查收!') } </script>
<template> <view class="user-login" v-if="!isBinding"> <!-- 省略前面小节的代码... -->
<uni-forms class="login-form" ref="form"> <uni-forms-item name="mobile"> <uni-easyinput v-model="formData.mobile" :input-border="false" :clearable="false" placeholder=" 请输入手机号 " placeholder-style="color: #C3C3C5" /> </uni-forms-item> <uni-forms-item name="code"> <uni-easyinput v-model="formData.code" :input-border="false" :clearable="false" placeholder=" 请输入验证码 " placeholder-style="color: #C3C3C5" /> <text @click="onTextButtonClick" class="text-button"> 获取验证码 </text> </uni-forms-item>
<!-- 省略前面小节的代码... --> </uni-forms> </view> </template>
|
2.2.4 手机号绑定
调用 接口文档 为获取到的 openid 绑定手机号,分两个步骤实现:
- 封装调用接口的方法
1 2 3 4 5 6 7 8 9 10
|
export const bindingMobileApi = (data) => { return http.post('/login/binding', 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
| <!-- pages/login/qq.vue --> <script setup> import { ref } from 'vue' import { bindingMobileApi, verifyCodeApi } from '@/services/user' import { useUserStore } from '@/stores/user' // 省略前面小节的代码...
// 绑定手机号 async function onBindingButtonClick() { // 调用接口 const { code, data, message } = await bindingMobileApi({ ...formData.value, openId: userInfo.value.openid, }) // 检测接口是否调用成功 if (code !== 10000) return uni.utils.toast(message) // 登录成功后跳转 jumpRoute(data) } // 省略前面小节的代码... </script>
<template> <view class="user-login" v-if="!isBinding"> <!-- 省略前面小节的代码... --> <uni-forms class="login-form" ref="form"> <uni-forms-item name="mobile"> <uni-easyinput v-model="formData.mobile" :input-border="false" :clearable="false" placeholder=" 请输入手机号 " placeholder-style="color: #C3C3C5" /> </uni-forms-item> <uni-forms-item name="code"> <uni-easyinput v-model="formData.code" :input-border="false" :clearable="false" placeholder=" 请输入验证码 " placeholder-style="color: #C3C3C5" /> <text @click="onTextButtonClick" class="text-button"> 获取验证码 </text> </uni-forms-item> <!-- 省略前面小节的代码... --> <button @click="onBindingButtonClick" class="uni-button"> 绑定手机号 </button> </uni-forms> </view> </template>
|
注:在使用 Pinia 时页面会导致页面无法加载,错误的原因是地址中多了一个 ?
,怀疑是 uni-app 本身的一个 bug,暂时没有找到解决方法。
2.2.5 解除绑定
1 2 3 4 5 6 7 8 9 10
| <!-- pages/login/index.vue --> <script setup> // 省略前面小节的代码... // 开发测试解除手机号绑定 import { http } from '@/utils/http.js' http.put(`/unbound/13212345678`) // 省略前面小节的代码... </script>
|
四、打包发布
通过 HBuilderX 可以非常方便的打包项目到 H5 端、小程序端和 App 端。
3.1 H5 端
打包并发布 H5 端的步骤如下图所示:
以上步骤的操作会在本地打包好 H5 端的代码,位于项目根目录下的 unpackage/dist/build/h5
目录下,同时也可以将打包好的代码上传到服务空间。
注意:在 uniCloud 选择的阿里云免费服务空间,提供给大家一个免费的域名来使用,但是该域名的使用具有一定的限制。具体的说明大家可以 看这里。
打包到 H5 端并上传到服务器运行时,登录成功后无法正常跳转,检测发现代码出错位置在这里:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| <!-- pages/login/components/mobile.vue --> <script setup> import { ref, reactive } from 'vue' // 省略中间部分代码...
// 监听表单提交 async function onFormSubmit() { try { // 省略中间部分代码... // 这里的报错说 xxx 不是一个函数 uni[userStore.openType]({ url: userStore.redirectURL, }) } catch (err) { // ... } } </script>
|
代码语法是没有问题的,初步怀疑是 uni-app 打包的问题,解决该问题的方法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <!-- pages/login/components/mobile.vue --> <script setup> import { ref, reactive } from 'vue' // 省略中间部分代码...
// 监听表单提交 async function onFormSubmit() { try { // 省略中间部分代码... // 地址重定向或 switchTab if (userStore.openType === 'switchTab') { uni.switchTab({ url: userStore.redirectURL }) } else { uni.redirectTo({ url: userStore.redirectURL }) } } catch (err) { // ... } } </script>
|
3.2 小程序端
打包小程序的步骤如下图所示:
以上的方式会将小程序端的代码打包到 unpackage/dist/build/mp-weixin
目录中,并自动打开小程序开发者工具来运行打包好的微信小程序,此时在微信小程序开发工具中选择上传即可。
3.3 App 端
在发布 App 端时有本地打包和云打包两种方式,本地打包要求本地具有 Android Studio 或 XCode 的环境,这种方式对于前端人员来说成本较高,云打包是由 uni-app 平台提供的免费服务,我们选择此种方式实进行打包。
- 配置 App 的图标
- 配置启动界面(闪屏)
- 指定 SDKVersion,使用 Vue3 开发时要求最低为 21
- 配置模块
- 云打包
支付宝支付账号,密码为 111111
scobys4865@sandbox.com
askgxl8276@sandbox.com
超级医生:
https://zhoushugang.gitee.io/patient-h5-note/project/super-doctor.html