uni-app(优医咨询)项目实战 - 第 7 天
学习目标:
- 能够基于 WebSocket 完成问诊全流程
- 能够使用 uniCloud 云存储上传文件
- 能够完成查看电子处方的功能
- 能够完成医生评价的功能
一、问诊室
以对话聊天的方式向医生介绍病情并获取诊断方案,聊天的内容支持文字和图片两种形式。
首先新建一个页面并完成分包的配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| { "subPackages": [ { "root": "subpkg_consult", "pages": [ { "path": "room/index", "style": { "navigationBarTitleText": " 问诊室 " } } ] }, ] }
|
该页面的内容特别多我们分段来数据模板代码移到项目当中:
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
| <!-- subpkg_consult/room/index.vue --> <script setup></script>
<template> <view class="room-page">
<scroll-view refresher-enabled refresher-background="#f2f2f2" scroll-y style="flex: 1; overflow: hidden" > <view class="message-container"> <!-- 此处将来填充更多代码... --> </view> </scroll-view>
<!-- 发送消息 --> <view class="message-bar"> <template v-if="true"> <uni-easyinput disabled :clearable="false" :input-border="false" placeholder-style="font-size: 32rpx; color: #c3c3c5;" placeholder=" 问医生 " /> <view class="image-button"> <uni-icons size="40" color="#979797" type="image"></uni-icons> </view> </template> <button v-else class="uni-button"> 咨询其它医生 </button> </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
| .room-page { display: flex; flex-direction: column; height: 100vh; height: calc(100vh - 44px); overflow: hidden; box-sizing: border-box; background-color: #f2f2f2; }
.message-container { padding: 0 30rpx 60rpx; overflow: hidden; }
.message-bar { background-color: red; display: flex; padding: 30rpx 30rpx calc(env(safe-area-inset-bottom) + 40rpx); background-color: #fff;
:deep(.is-disabled) { background-color: transparent !important; }
:deep(.uni-easyinput__content-input) { height: 88rpx; padding: 0 44rpx !important; border-radius: 88rpx; color: #3c3e42; font-size: 32rpx; background-color: #f6f6f6; }
.image-button { display: flex; justify-content: center; align-items: center; height: 88rpx; width: 88rpx; margin-left: 30rpx; }
.uni-button { flex: 1; } }
|
1.1 WebSocket 连接
首先安装 Socket.IO
1
| npm install socket.io-client
|
然后建立连接,在建立连接进需要传入参数和登录信息:
auth
登录状态信息,即 token
query
建立连接时传递的参数
transports
建立连接时使用的协议
timeout
超时设置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <!-- subpkg_consult/room/index.vue --> <script setup> import { ref } from 'vue' import { io } from 'socket.io-client' import { useUserStore } from '@/stores/user'
// 用户登录信息(不具有响应式) const { token } = useUserStore()
// 获取地址中的参数 const props = defineProps({ orderId: String, })
// 建立 socket 连接 const socket = io('https://consult-api.itheima.net', { auth: { token: 'Bearer ' + token }, query: { orderId: props.orderId }, transports: ['websocket', 'polling'], timeout: 5000, }) </script>
|
1.2 接收消息
Socket.IO 是基于事件来实现数据通信的,事件的名称是由前后端商定好的,详见 接口文档 说明,消息的获取分成两种情况:
- 历史消息,事件名称为
chatMsgList
- 即时消息,事件名称为
receiveChatMsg
1.2.1 消息列表
在建立连接时服务端会通过 chatMsgList
传递历史数据,通过 on
方法进行监听来获取这些数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| <!-- subpkg_consult/room/index.vue --> <script setup> import { ref } from 'vue' import { io } from 'socket.io-client' import { useUserStore } from '@/stores/user'
// 省略前面小节的代码... // 消息列表 const messageList = ref([]) // 获取历史消息 socket.on('chatMsgList', ({ code, data }) => { // 没有返回数据 if (code !== 10000) return // 提取列表数据 data.forEach(({ items }) => { // 追加到消息列表中 messageList.value.push(...items) }) }) </script>
|
在消息列表数据中包含了不同类型的消息且展示的方式也不相同,因此在对数据进行遍历的过程中需要通过 v-if
来渲染不同的模板,不同的类型对应了一个数值:
消息类型 |
说明 |
备注 |
21 |
患者信息 |
|
22 |
处方信息 |
|
23 |
未提交评价 |
|
24 |
已提交评价 |
|
31 |
普通通知 |
白底黑字 |
32 |
温馨提示 |
|
33 |
取消订单 |
灰底黑字 |
4 |
图片消息 |
|
1 |
文字消息 |
|
首次进入问诊室返回的 3 条件的类型分别为患者信息(21)、普通通知(31)、温馨提示(32),我们逐个进行渲染。
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
| <!-- subpkg_consult/room/components/patient-info.vue --> <script setup></script> <template> <!-- 患者信息(21) --> <view class="patient-info"> <view class="header"> <view class="title"> 李富贵 男 31 岁 </view> <view class="note"> 一周内 | 未去医院就诊 </view> </view> <view class="content"> <view class="list-item"> <text class="label"> 病情描述 </text> <text class="note"> 头痛、头晕、恶心 </text> </view> <view class="list-item"> <text class="label"> 图片 </text> <text class="note"> 点击查看 </text> </view> </view> </view> </template>
<style lang="scss"> .patient-info { padding: 30rpx; margin-top: 60rpx; border-radius: 20rpx; box-sizing: border-box; background-color: #fff;
.header { padding-bottom: 20rpx; border-bottom: 1rpx solid #ededed;
.title { font-size: 32rpx; color: #121826; margin-bottom: 10rpx; }
.note { font-size: 26rpx; color: #848484; } }
.content { margin-top: 20rpx; font-size: 26rpx;
.list-item { display: flex; margin-top: 10rpx; }
.label { width: 130rpx; color: #3c3e42; }
.note { flex: 1; line-height: 1.4; color: #848484; } } } </style>
|
接下来分成 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
| <!-- subpkg_consult/room/components/patient-info.vue --> <script setup> // 定义属性接收外部传入的数据 const props = defineProps({ info: { type: Object, default: {}, }, }) // 患病时长 const illnessTimes = { 1: ' 一周内 ', 2: ' 一个月内 ', 3: ' 半年内 ', 4: ' 半年以上 ', } // 是否就诊过 const consultFlags = { 1: ' 就诊过 ', 0: ' 没有就诊过 ', } </script> <template> ... </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
| <!-- subpkg_consult/room/index.vue --> <script setup> import { ref } from 'vue' import { io } from 'socket.io-client' import { useUserStore } from '@/stores/user' // 引入患者信息组件 import patientInfo from './components/patient-info.vue'
// 省略前面小节的代码 </script> <template> <view class="room-page"> <!-- 此处将来填充更多代码... --> <scroll-view refresher-enabled refresher-background="#f2f2f2" scroll-y style="flex: 1; overflow: hidden" > <view class="message-container"> <template v-for="message in messageList" :key="message.id"> <!-- 患者信息(21) --> <patient-info v-if="message.msgType === 21" :info="message.msg.consultRecord" />
<!-- 此处将来填充更多代码... --> </template> </view> </scroll-view>
<!-- 发送消息 --> <view class="message-bar"> ... </view> </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
| <!-- subpkg_consult/room/components/patient-info.vue --> <script setup> // 省略前面小节的代码... </script>
<template> <!-- 患者信息(21) --> <view class="patient-info"> <view class="header"> <view class="title"> {{ props.info.patientInfo.name }} {{ props.info.patientInfo.genderValue }} {{ props.info.patientInfo.age }} 岁 </view> <view class="note"> {{ illnessTimes[props.info.illnessTime] }} | {{ consultFlags[props.info.illnessType] }} </view> </view> <view class="content"> <view class="list-item"> <text class="label"> 病情描述 </text> <text class="note">{{ props.info.illnessDesc }}</text> </view> <view class="list-item"> <text class="label"> 图片 </text> <text v-if="props.info.pictures?.length" class="note"> 点击查看 </text> <text v-else class="note"> 暂无图片 </text> </view> </view> </view> </template>
|
- 大图查看患者病情图片,uni-app 提供了大图查看图片的 API
uni.previewImage
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
| <script setup> // 省略前面小节的代码...
// 点击查看病情介绍图片 async function onPreviewClick(urls) { uni.previewImage({ urls: urls.map((item) => item.url), }) } </script>
<template> <!-- 患者信息(21) --> <view class="patient-info"> <view class="header"> ... </view> <view class="content"> <view class="list-item"> <text class="label"> 病情描述 </text> <text class="note">{{ props.info.illnessDesc }}</text> </view> <view class="list-item"> <text class="label"> 图片 </text> <text v-if="props.info.pictures?.length" @click="onPreviewClick(props.info.pictures)" class="note" > 点击查看 </text> <text v-else class="note"> 暂无图片 </text> </view> </view> </view> </template>
|
1.2.3 通知消息
通知消息分为 3 种,分别为:
消息类型 |
说明 |
备注 |
31 |
普通通知 |
白底黑字 |
32 |
温馨提示 |
|
33 |
取消订单 |
灰底黑字 |
首先创建消息通知组伯,通知消息的模板如下:
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
| <!-- subpkg_consult/room/components/notify-info.vue --> <script setup></script>
<template> <!-- 普通通知(31) --> <view class="message-tips"> <view class="wrapper"> 医护人员正在赶来,请耐心等候 </view> </view>
<!-- 温馨提示(32) --> <view class="message-tips"> <view class="wrapper"> <text class="label"> 温馨提示:</text> 在线咨询不能代替面诊,医护人员建议仅供参考 </view> </view> </template>
<style lang="scss"> .message-tips { display: flex; justify-content: center; margin-top: 60rpx;
&:first-child { margin-top: 30rpx; } }
.wrapper { line-height: 1; text-align: center; padding: 20rpx 30rpx; // margin-top: 60rpx; font-size: 24rpx; border-radius: 70rpx; color: #848484; background-color: #fff;
.label { color: #16c2a3; } } </style>
|
接下来分成 3 个步骤来实现:
- 定义组件的逻辑,要求能区分通知的类型并通过插槽来展示内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <!-- subpkg_consult/room/components/notify-info.vue --> <script setup> // 接收外部传入的数据 const props = defineProps({ type: { type: Number, default: 31, }, }) </script> <template> <!-- 温馨提示(32) --> <view class="message-tips"> <text class="label"> 温馨提示:</text> <slot /> </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
| <!-- subpkg_consult/room/index.vue --> <script setup> import { ref } from 'vue' import { io } from 'socket.io-client' import { useUserStore } from '@/stores/user' // 引入通知消息组件 import notifyInfo from './components/notify-info.vue'
// 省略前面小节的代码 </script> <template> <view class="room-page"> <!-- 此处将来填充更多代码... --> <scroll-view refresher-enabled refresher-background="#f2f2f2" scroll-y style="flex: 1; overflow: hidden" > <view class="message-container"> <template v-for="message in messageList" :key="message.id">
<!-- 消息通知 --> <notify-info v-if="message.msgType >= 31" :type="message.msgType"> {{ message.msg.content }} </notify-info>
<!-- 此处将来填充更多代码... --> </template> </view> </scroll-view>
<!-- 发送消息 --> <view class="message-bar"> ... </view> </view> </template>
|
- 接收并渲染组件数据
1 2 3 4 5 6 7 8 9 10 11
| <!-- subpkg_consult/room/components/notify-info.vue --> <script setup> // 省略前面小节的代码... </script> <template> <!-- 温馨提示(32) --> <view class="message-tips"> <text v-if="props.type === 32" class="label"> 温馨提示:</text> <slot /> </view> </template>
|
1.2.4 文字 / 图片消息
实时接收到医生发送过来的消息,包括文字消息和图片消息两种类型,使用 超级医生 来模拟医生端发送消息,根据订单 ID 来打通医生端和患者端的聊天连接。
首先接收医生端的回复的消息需要监听的事件为 receiveChatMsg
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <!-- subpkg_consult/room/index.vue --> <script setup> import { ref } from 'vue' import { io } from 'socket.io-client' import { useUserStore } from '@/stores/user'
// 省略前面小节的代码...
// 接收消息 socket.on('receiveChatMsg', (message) => { // 修改消息为已读 socket.emit('updateMsgStatus', message.id) // 接收到的消息追加到消息列表中 messageList.value.push(message) }) </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 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
| <!-- subpkg_consult/room/components/message-info.vue --> <script setup></script>
<template> <!-- 文字 / 图片消息 --> <view class="message-item reverse"> <image class="room-avatar" src="/static/uploads/doctor-avatar-2.png" /> <view class="room-message"> <view class="time">14:13</view> <view class="text"> 您好,我是医师王医生,已收到您的问诊信息,我会尽量及时、准确、负责的回复您的问题,请您稍等。 </view> <image v-if="false" class="image" src="/static/uploads/feed-1.jpeg" mode="widthFix" /> </view> </view> </template>
<style lang="scss"> .message-item { display: flex; align-self: flex-start; margin-top: 60rpx;
.room-avatar { width: 80rpx; height: 80rpx; border-radius: 50%; }
.room-message { margin-left: 20rpx; }
.time { font-size: 26rpx; color: #979797; }
.image { max-width: 420rpx; margin-top: 10rpx; }
.text { max-width: 420rpx; line-height: 1.75; padding: 30rpx 40rpx; margin-top: 16rpx; border-radius: 20rpx; font-size: 30rpx; color: #3c3e42; background-color: #fff; position: relative;
&::after { content: ''; position: absolute; top: 0; left: -25rpx; width: 26rpx; height: 52rpx; background-image: url(https://consult-patient.oss-cn-hangzhou.aliyuncs.com/static/images/im-arrow-1.png); background-size: contain; } }
&.reverse { flex-direction: row-reverse; align-self: flex-end;
.room-message { margin-left: 0; margin-right: 20rpx; }
.time { text-align: right; }
.text { background-color: #16c2a3; color: #fff;
&::after { left: auto; right: -25rpx; background-image: url(https://consult-patient.oss-cn-hangzhou.aliyuncs.com/static/images/im-arrow-2.png); } } } } </style>
|
接下来分成 3 个步骤来实现:
- 定义组件的逻辑,要求能接收外部传入的数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <!-- subpkg_consult/room/components/message-info.vue --> <script setup> // 接收外部传入的数据 const props = defineProps({ info: { type: Object, default: {}, }, type: { type: Number, default: 1, }, }) </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
| <!-- subpkg_consult/room/index.vue --> <script setup> import { ref } from 'vue' import { io } from 'socket.io-client' import { useUserStore } from '@/stores/user' // 引入通知消息组件 import messageInfo from './components/message-info.vue'
// 省略前面小节的代码 </script> <template> <view class="room-page"> <!-- 此处将来填充更多代码... --> <scroll-view refresher-enabled refresher-background="#f2f2f2" scroll-y style="flex: 1; overflow: hidden" > <view class="message-container"> <template v-for="message in messageList" :key="message.id">
<!-- 文字图片消息 --> <message-info v-if="message.msgType <= 4" :info="message" :type="message.msgType" />
<!-- 此处将来填充更多代码... --> </template> </view> </scroll-view>
<!-- 发送消息 --> <view class="message-bar"> ... </view> </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
| <!-- subpkg_consult/room/components/message-info.vue --> <script setup> // ... </script>
<template> <!-- 文字 / 图片消息 --> <view class="message-item"> <image class="room-avatar" :src="props.info.fromAvatar" /> <view class="room-message"> <view class="time">{{ props.info.createTime }}</view> <!-- 文字消息 --> <view v-if="props.type === 1" class="text"> {{ props.info.msg.content }} </view> <!-- 图片消息 --> <image v-if="props.type === 4" class="image" :src="props.info.msg.picture.url" mode="widthFix" /> </view> </view> </template>
|
- 处理消息的时间,安装
dayjs
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
| <!-- subpkg_consult/room/index.vue --> <script setup> import dayjs from 'dayjs' // 省略前面小节的代码...
// 格式化显示时间 function dateFormat(date) { return dayjs(date).format('hh:mm:ss') } </script>
<template> <!-- 文字 / 图片消息 --> <view class="message-item"> <image class="room-avatar" :src="props.info.fromAvatar" /> <view class="room-message"> <view class="time">{{ dateFormat(props.info.createTime) }}</view> <view v-if="props.type === 1" class="text"> {{ props.info.msg.content }} </view> <image v-if="props.type === 4" class="image" :src="props.info.msg.picture.url" mode="widthFix" /> </view> </view> </template>
|
1.2.5 处方消息
医生根据问诊的情况开具诊断结果即为处方消息,到消息的类型值为 22
,首先创建组件,布局模板如下所示:
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
| <!-- subpkg_consult/room/components/prescription-info.vue --> <script setup></script>
<template> <!-- 处方消息(22) --> <view class="e-prescription"> <view class="prescription-content"> <view class="list-title"> <view class="label"> 电子处方 </view> <view class="extra"> 原始处方 <uni-icons size="16" color="#848484" type="right" /> </view> </view> <view class="list-item"> 李富贵 男 31 岁 血管性头痛 </view> <view class="list-item"> 开方时间:2022-01-15 14:21:42</view>
<view class="dividing-line"></view>
<view class="list-title"> <view class="label"> <text class="name"> 优赛明 维生素 E 乳 </text> <text class="unit">85ml</text> <text class="quantity">x1</text> </view> </view> <view class="list-item"> 口服,每次 1 袋,每天 3 次,用药 3 天 </view> </view> <navigator class="uni-link" hover-class="none" url="/subpkg_medicine/payment/index" > 购买药品 </navigator> </view> </template>
<style lang="scss"> .e-prescription { width: 100%; margin-top: 60rpx; border-radius: 20rpx; background-color: #eaf8f6;
.prescription-content { padding: 30rpx; border-radius: 20rpx; background-color: #fff; }
.list-title { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10rpx; }
.list-item { margin-bottom: 10rpx; font-size: 26rpx; color: #848484; }
.label { display: flex; justify-content: space-between; flex: 1; font-size: 32rpx; color: #121826;
&.medicine { font-size: 30rpx; color: #666; } }
.unit { width: 200rpx; }
.extra { display: flex; align-items: center; font-size: 26rpx; color: #848484; }
:deep(.uniui-right) { margin-top: 4rpx; }
.dividing-line { padding-bottom: 20rpx; margin-bottom: 20rpx; border-bottom: 1rpx solid #ededed; }
.uni-link { text-align: center; line-height: 1; padding: 30rpx 0; color: #16c2a3; font-size: 32rpx; } } </style>
|
接下来分成 3 个步骤来实现:
- 定义组件逻辑,要求能接收组件外部传入的数据
1 2 3 4 5 6 7 8 9 10
| <!-- subpkg_consult/room/components/prescription-info.vue --> <script setup> // 接收组件外部传入的数据 const props = defineProps({ info: { type: Object, default: {}, }, }) </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
| <!-- subpkg_consult/room/index.vue --> <script setup> import { ref } from 'vue' import { io } from 'socket.io-client' import { useUserStore } from '@/stores/user' // 引入处方消息组件 import prescriptionInfo from './components/prescription-info.vue'
// 省略前面小节的代码 </script> <template> <view class="room-page"> <!-- 此处将来填充更多代码... --> <scroll-view refresher-enabled refresher-background="#f2f2f2" scroll-y style="flex: 1; overflow: hidden" > <view class="message-container"> <template v-for="message in messageList" :key="message.id">
<!-- 电子处方 --> <prescription-info v-if="message.msgType === 22" :info="message.msg.prescription" />
<!-- 此处将来填充更多代码... --> </template> </view> </scroll-view>
<!-- 发送消息 --> <view class="message-bar"> ... </view> </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
| <!-- subpkg_consult/room/components/prescription-info.vue --> <script setup> // ... </script>
<template> <!-- 处方消息(22)--> <view class="e-prescription"> <view class="prescription-content"> <view class="list-title"> <view class="label"> 电子处方 </view> <view class="extra"> 原始处方 <uni-icons size="16" color="#848484" type="right" /> </view> </view> <view class="list-item"> {{ props.info.name }} {{ props.info.genderValue }} {{ props.info.age }} 岁 {{ props.info.diagnosis }} </view> <view class="list-item"> 开方时间:{{ props.info.createTime }}</view>
<view class="dividing-line"></view>
<template v-for="medicine in props.info.medicines" :key="medicine.id"> <view class="list-title"> <view class="label"> <text class="name">{{ medicine.name }}</text> <text class="unit">85ml</text> <text class="quantity">x{{ medicine.quantity }}</text> </view> </view> <view class="list-item">{{ medicine.usageDosag }}</view> </template> </view> <navigator class="uni-link" hover-class="none" url="/subpkg_medicine/payment/index" > 购买药品 </navigator> </view> </template>
|
1.2.6 原始处方
在医生开完处方后会生成电子版的处方,通过调用接口进行查看。
1.2.7 医生评价
在医生端结束问诊后,患者可以对医生进行评价,医生评价的布局模板为:
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
| <!-- subpkg_consult/room/components/rate-info.vue --> <script setup></script> <template> <!-- 医生评价 --> <view class="doctor-rating"> <view class="title"> 医生服务评价 </view> <view class="subtitle"> 本次在线问诊服务您还满意吗?</view> <view class="rating"> <uni-rate :size="28" margin="12" :value="0" /> </view> <view class="text"> <uni-easyinput type="textarea" maxlength="150" :input-border="false" :styles="{ backgroundColor: '#f6f6f6' }" placeholder-style="font-size: 28rpx; color: #979797" placeholder=" 请描述您对医生的评价或是在医生看诊过程中遇到的问题 " /> <text class="word-count">0/150</text> </view> <view class="anonymous"> <uni-icons v-if="true" size="16" color="#16C2A3" type="checkbox-filled" /> <uni-icons v-else size="16" color="#d1d1d1" type="circle" /> <text class="label"> 匿名评价 </text> </view> <button disabled class="uni-button"> 提交 </button> </view> </template>
<script> export default { options: { styleIsolation: 'shared', }, } </script> <style lang="scss"> .doctor-rating { padding: 30rpx 30rpx 40rpx; border-radius: 20rpx; background-color: #fff; margin-top: 60rpx;
.title { text-align: center; font-size: 30rpx; color: #121826; }
.subtitle { text-align: center; font-size: 24rpx; color: #6f6f6f; margin: 10rpx 0 20rpx; }
.rating { display: flex; justify-content: center; }
.text { padding: 20rpx 30rpx; margin-top: 20rpx; background-color: #f6f6f6; border-radius: 20rpx; position: relative; }
:deep(.uni-easyinput__content-textarea) { font-size: 28rpx; }
.word-count { position: absolute; bottom: 20rpx; right: 30rpx; line-height: 1; font-size: 24rpx; color: #6f6f6f; }
.anonymous { display: flex; align-items: center; justify-content: center; margin: 30rpx 0; color: #6f6f6f; font-size: 24rpx;
.label { margin-left: 6rpx; } }
.uni-button[disabled] { color: #a6dbd5; background-color: #eaf8f6; } } </style>
|
接下来分成 5 个步骤来实现:
- 到页面中应用该组件,消息的类型值是
23
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
| <!-- subpkg_consult/room/index.vue --> <script setup> import { ref } from 'vue' import { io } from 'socket.io-client' import { useUserStore } from '@/stores/user' // 引入处方消息组件 import rateInfo from './components/rate-info.vue'
// 省略前面小节的代码 </script> <template> <view class="room-page"> <!-- 此处将来填充更多代码... --> <scroll-view refresher-enabled refresher-background="#f2f2f2" scroll-y style="flex: 1; overflow: hidden" > <view class="message-container"> <template v-for="message in messageList" :key="message.id">
<!-- 医生评价 --> <rate-info v-if="message.msgType === 23"></rate-info>
<!-- 此处将来填充更多代码... --> </template> </view> </scroll-view>
<!-- 发送消息 --> <view class="message-bar"> ... </view> </view> </template>
|
- 获取评价数据并对数据进行验证:
v-model
获取数据
- 字数统计使用计算属性
- 控制字数使用
maxlength
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
| <!-- subpkg_consult/room/components/rate-info.vue --> <script setup> import { computed, ref } from 'vue' // 评价内容 const formData = ref({ score: 0, content: '', anonymousFlag: 0, }) // 统计字数 const wordCount = computed(() => { return formData.value.content.length }) // 是否允许提交 const buttonEnable = computed(() => { return formData.value.score }) // 是否匿名评价 function onAnonymousClick() { formData.value.anonymousFlag = Math.abs(formData.value.anonymousFlag - 1) } </script>
<template> <!-- 医生评价 --> <view class="doctor-rating"> <view class="title"> 医生服务评价 </view> <view class="subtitle"> 本次在线问诊服务您还满意吗?</view> <view class="rating"> <uni-rate v-model="formData.score" :size="28" margin="12" /> </view> <view class="text"> <uni-easyinput type="textarea" maxlength="150" v-model="formData.content" :input-border="false" :styles="{ backgroundColor: '#f6f6f6' }" placeholder-style="font-size: 28rpx; color: #979797" placeholder=" 请描述您对医生的评价或是在医生看诊过程中遇到的问题 " /> <text class="word-count">{{ wordCount }}/150</text> </view> <view @click="onAnonymousClick" class="anonymous"> <uni-icons v-if="formData.anonymousFlag" size="16" color="#16C2A3" type="checkbox-filled" /> <uni-icons v-else size="16" color="#d1d1d1" type="circle" /> <text class="label"> 匿名评价 </text> </view> <button :disabled="!buttonEnable" class="uni-button"> 提交 </button> </view> </template>
|
- 在提交评价时,需要获取问诊订单详情,在问诊订单详情中包含了 医生的 ID,接口文档 在这里
1 2 3 4 5 6 7 8 9 10 11
| import { http } from '@/utils/http'
export const orderDetailApi = (orderId) => { return http.get('/patient/consult/order/detail', { params: { orderId } }) }
|
将订单 ID 和医生 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
| <!-- subpkg_consult/room/index.vue --> <script setup> import { ref } from 'vue' import { io } from 'socket.io-client' import { useUserStore } from '@/stores/user' import { orderDetailApi } from '@/services/consult' // 省略前面小节的代码...
// 问诊订单详情 const orderDetail = ref({}) // 省略前面小节的代码...
// 获取问诊订单详情 async function getOrderDetail() { // 调用接口 const { code, data, message } = await orderDetailApi(props.orderId) // 检测接口是否调用成功 if (code !== 10000) return uni.utils.toast(message) // 渲染问诊订单数据 orderDetail.value = data }
getOrderDetail() </script>
<template> <view class="room-page"> <!-- 此处将来填充更多代码... --> <scroll-view refresher-enabled refresher-background="#f2f2f2" scroll-y style="flex: 1; overflow: hidden" > <view class="message-container"> <template v-for="message in messageList" :key="message.id">
<!-- 医生评价 --> <rate-info :order-id="props.orderId" :doctor-id="orderDetail.docInfo?.id" v-if="message.msgType === 23" />
<!-- 此处将来填充更多代码... --> </template> </view> </scroll-view>
<!-- 发送消息 --> <view class="message-bar"> ... </view> </view> </template>
|
- 调用接口提交评价的数据,接口文档 在这里
1 2 3 4 5 6 7 8 9 10 11
| import { http } from '@/utils/http'
export const evaluateDoctorApi = (data) => { return http.post('/patient/order/evaluate', 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/room/components/rate-info.vue --> <script setup> import { computed, ref } from 'vue' import { evaluateDoctorApi } from '@/services/doctor'
// 接收组件外部的数据 const props = defineProps({ orderId: String, doctorId: String, })
// 提交表单 async function onFormSubmit() { // 调用接口 const { code, data, message } = await evaluateDoctorApi({ docId: props.doctorId, orderId: props.orderId, ...formData.value, }) // 检测接口是否调用成功 if (code !== 10000) return uni.utils.toast(message) uni.utils.toast(' 感谢您的评价!') // 标记已经评价过 hasEvaluate.value = true } </script>
<template> <!-- 医生评价 --> <view class="doctor-rating"> <view class="title"> 医生服务评价 </view> <view class="subtitle"> 本次在线问诊服务您还满意吗?</view> <view class="rating"> <uni-rate v-model="formData.score" :size="28" margin="12" /> </view> <view class="text"> <uni-easyinput type="textarea" maxlength="150" v-model="formData.content" :input-border="false" :styles="{ backgroundColor: '#f6f6f6' }" placeholder-style="font-size: 28rpx; color: #979797" placeholder=" 请描述您对医生的评价或是在医生看诊过程中遇到的问题 " /> <text class="word-count">{{ wordCount }}/150</text> </view> <view @click="onAnonymousClick" v-if="!hasEvaluate" class="anonymous"> <uni-icons v-if="formData.anonymousFlag" size="16" color="#16C2A3" type="checkbox-filled" /> <uni-icons v-else size="16" color="#d1d1d1" type="circle" /> <text class="label"> 匿名评价 </text> </view> <button v-if="!hasEvaluate" :disabled="!buttonEnable" @click="onFormSubmit" class="uni-button" > 提交 </button> </view> </template>
|
- 已评价状态,消息类型值 为 24
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_consult/room/components/rate-info.vue --> <script setup> import { ref, computed } from 'vue' import { evaluateDoctorApi } from '@/services/doctor'
// 接收组件外部的数据 const props = defineProps({ orderId: String, doctorId: String, // 是否已评价过 hasEvaluate: { type: Boolean, default: false, }, // 评价的内容 evaluateDoc: { type: Object, default: {}, }, })
// 评价内容 const formData = ref({ score: props.evaluateDoc.score, content: props.evaluateDoc.content, // 注意要指定一个默认值为 0 anonymousFlag: 0, }) // 是否已经评价过 const hasEvaluate = ref(props.hasEvaluate)
// 统计字数 const wordCount = computed(() => { // 通过 ? 来避免初始数据中 content 不存在的情况 return formData.value.content?.length || 0 }) </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
| <!-- subpkg_consult/room/index.vue --> <script setup> import { ref } from 'vue' import { io } from 'socket.io-client' import { useUserStore } from '@/stores/user' import { orderDetailApi } from '@/services/consult' // 省略前面小节的代码... </script>
<template> <view class="room-page"> <!-- 此处将来填充更多代码... --> <scroll-view refresher-enabled refresher-background="#f2f2f2" scroll-y style="flex: 1; overflow: hidden" > <view class="message-container"> <template v-for="message in messageList" :key="message.id">
<!-- 医生评价(已评价) --> <rate-info :evaluateDoc="message.msg.evaluateDoc" has-evaluate v-if="message.msgType === 24" />
<!-- 此处将来填充更多代码... --> </template> </view> </scroll-view>
<!-- 发送消息 --> <view class="message-bar">
</view> </view> </template>
|
1.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
| <!-- subpkg_consult/room/index.vue --> <script setup> // 省略前面小节的代码... // 订单状态为 3 时,表示 问诊中... // 监听订单状态变化 socket.on('statusChange', getOrderDetail) // 省略前面小节的代码... </script> <template> <view class="room-page"> <!-- 此处将来填充更多代码... --> <scroll-view refresher-enabled refresher-background="#f2f2f2" scroll-y style="flex: 1; overflow: hidden" > <view class="message-container"> <!-- 省略前面小节的代码... --> </view> </scroll-view>
<!-- 发送消息 --> <view class="message-bar"> <template v-if="true"> <uni-easyinput :disabled="orderDetail.status !== 3" :clearable="false" :input-border="false" placeholder-style="font-size: 32rpx; color: #c3c3c5;" placeholder=" 问医生 " /> <view class="image-button"> <uni-icons size="40" color="#979797" type="image"></uni-icons> </view> </template> <button v-else class="uni-button"> 咨询其它医生 </button> </view> </view> </template>
|
1.3.1 文字消息
发送文字消息分 3 个步骤来实现:
- 监听
uni-easyinput
组件的 confirm
事件并使用 v-model
获取表单的内容
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
| <!-- subpkg_consult/room/index.vue --> <script setup> // 省略前面小节的代码... // 文字消息 const textMessage = ref('')
// 省略前面小节的代码...
// 发送文字消息 function onInputConfirm() { console.log(textMessage.value) }
// 省略前面小节的代码... </script>
<template> <view class="room-page"> <!-- 此处将来填充更多代码... --> <scroll-view refresher-enabled refresher-background="#f2f2f2" scroll-y style="flex: 1; overflow: hidden" > <view class="message-container"> ... </view> </scroll-view>
<!-- 发送消息 --> <view class="message-bar"> <template v-if="true"> <uni-easyinput v-model="textMessage" @confirm="onInputConfirm" :disabled="orderDetail.status !== 3" :clearable="false" :input-border="false" placeholder-style="font-size: 32rpx; color: #c3c3c5;" placeholder=" 问医生 " /> <view class="image-button"> <uni-icons size="40" color="#979797" type="image"></uni-icons> </view> </template> <button v-else class="uni-button"> 咨询其它医生 </button> </view> </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
| <script setup> // 省略前面小节的代码... // 用户登录信息(不具有响应式) const { token, userId } = useUserStore() // 问诊订单详情 const orderDetail = ref({}) // 文字消息 const textMessage = ref('')
// 省略前面小节的代码...
// 发送文字消息 function onInputConfirm() { // 发送消息 socket.emit('sendChatMsg', { // 当前登录用户的 ID from: userId, to: orderDetail.value?.docInfo?.id, msgType: 1, msg: { content: textMessage.value, }, }) // 清空表单 textMessage.value = '' } // 省略前面小节的代码... </script>
|
在用户登录成功时,只记录了用户的 token
在患者向医生发送消息时还需要传递用户的 ID,在 Pinia 中添加数据来记录登录用户的 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
| import { ref } from 'vue' import { defineStore } from 'pinia'
export const useUserStore = defineStore( 'user', () => { const token = ref('') const redirectURL = ref('/pages/index/index') const openType = ref('switchTab') const userId = ref('')
return { token, userId, redirectURL, openType } }, { persist: { paths: ['token', 'userId', 'redirectURL', 'openType'], }, } )
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <!-- pages/login/index.vue --> <script setup> async function onFormSubmit() { // 判断是否勾选协议 if (!isAgree.value) return uni.utils.toast(' 请先同意协议!') // 调用 uniForms 组件验证数据的方法 try { // 省略前面小节的代码... // 持久化存储 token userStore.token = data.token // 存储登录用户的 ID userStore.userId = data.id
} catch (error) { console.log(error) } } </script>
|
- 调整消息的对齐方式,患者消息靠右显示
在消息中包含的属性 from
是消息发送者的 ID,如果与登录用户的 ID 一致,则表示是患者发送的消息,消息的内容要靠右显示,类名 reverse
可以控制靠右对齐。
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_consult/room/components/message-info.vue --> <script setup> import dayjs from 'dayjs' import { useUserStore } from '@/stores/user.js'
// 登录用户 ID const { userId } = useUserStore() // 省略前面小节的代码... </script>
<template> <!-- 文字 / 图片消息 --> <view :class="{ reverse: props.info.from === userId }" class="message-item"> <image class="room-avatar" :src="props.info.fromAvatar" /> <view class="room-message"> <view class="time">{{ dateFormat(props.info.createTime) }}</view> <view v-if="props.type === 1" class="text"> {{ props.info.msg.content }} </view> <image v-if="props.type === 4" class="image" :src="props.info.msg.picture.url" mode="widthFix" /> </view> </view> </template>
|
1.3.2 图片消息
发送图片消息需要将图片上传到云空间,需要调用 uniCloud
提供的 API chooseAndUploadFile
,我们分 x 步来实现:
- 判断问诊订单状态是否为问诊中
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
| <!-- subpkg_consult/room/index.vue --> <script setup> // 省略前面小节代码... // 发送图片消息 function onImageButtonClick() { // 是否在问诊状态中... if (orderDetail.value.status !== 3) { return uni.utils.toast(' 医生当前不在线!') } } // 省略前面小节代码... </script>
<template> <view class="room-page"> <!-- 此处将来填充更多代码... --> <scroll-view refresher-enabled refresher-background="#f2f2f2" scroll-y style="flex: 1; overflow: hidden" > <view class="message-container"> ... </view> </scroll-view>
<!-- 发送消息 --> <view class="message-bar"> <template v-if="true"> <uni-easyinput v-model="textMessage" @confirm="onInputConfirm" :disabled="orderDetail.status !== 3" :clearable="false" :input-border="false" placeholder-style="font-size: 32rpx; color: #c3c3c5;" placeholder=" 问医生 " /> <view @click="onImageButtonClick" class="image-button"> <uni-icons size="40" color="#979797" type="image"></uni-icons> </view> </template> <button v-else class="uni-button"> 咨询其它医生 </button> </view> </view> </template>
|
- 调用 API 上传到 uniCloud 存储空间
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
| <!-- subpkg_consult/room/index.vue --> <script setup> // 省略前面小节代码...
// 发送图片消息 function onImageButtonClick() { // 是否在问诊状态中... if (orderDetail.value.status !== 3) { return uni.utils.toast(' 医生当前不在线!') } // 上传图片到 uniCloud uniCloud.chooseAndUploadFile({ type: 'image', count: 1, extension: ['.jpg', '.png', '.gif'], success: ({ tempFiles }) => { console.log(tempFiles) }, }) } // 省略前面小节代码... </script>
<template> ... </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
| <!-- subpkg_consult/room/index.vue --> <script setup> // 省略前面小节代码... // 用户登录信息(不具有响应式) const { token, userId } = useUserStore() // 发送图片消息 function onImageButtonClick() { // 是否在问诊状态中... if (orderDetail.value.status !== 3) { return uni.utils.toast(' 医生当前不在线!') } // 上传图片到 uniCloud uniCloud.chooseAndUploadFile({ type: 'image', count: 1, extension: ['.jpg', '.png', '.gif'], success: ({ tempFiles }) => { // 上传成功的图片 const picture = { id: tempFiles[0].lastModified, url: tempFiles[0].url, } // 发送消息 socket.emit('sendChatMsg', { from: userId, to: orderDetail.value?.docInfo?.id, msgType: 4, msg: { picture }, }) }, }) } // 省略前面小节代码... </script>
|
1.4 问诊订单状态
患者在与医生对话的过程中问诊订单状态会发生改变,包括待支付、待接诊、咨询中、已完成、已取消,在页面的顶部要根据订单的状态展示不同的内容。
- 将问诊状态的布局模板独立到组件中,要求组件能接收 3 个数据
status
问诊订单的状态值
statusValue
问诊订单的文字描述
countdown
倒计时剩余时长
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
| <!-- subpkg_consult/room/components/room-status.vue --> <script setup> // 接收组件外部传入的数据 const props = defineProps({ status: Number, statusValue: String, countdown: Number, }) </script> <template> <!-- 咨询室状态 --> <view class="room-status"> <view class="status countdown" v-if="false"> <text class="label"> 咨询中 </text> <view class="time"> 剩余时间: <uni-countdown color="#3c3e42" :font-size="14" :show-day="false" :second="0" /> </view> </view> <view v-else-if="false" class="status waiting"> 已通知医生尽快接诊,24 小时内医生未回复将自动退款 </view> <view v-else class="status"> <uni-icons size="20" color="#121826" type="checkbox-filled" /> 已结束 </view> </view> </template>
<style lang="scss"> .room-status { font-size: 26rpx; position: sticky; top: 0; z-index: 99;
.status { display: flex; padding: 30rpx; background-color: #fff; }
.waiting { color: #16c2a3; background-color: #eaf8f6; }
.countdown { justify-content: space-between; }
.label { color: #16c2a3; }
.icon-done { color: #121826; font-size: 28rpx; margin-right: 5rpx; }
.time { display: flex; color: #3c3e42; }
:deep(.uni-countdown) { margin-left: 6rpx; } } </style>
|
- 在页面中应用组件并传入数据,查询订单状态的的 API 在前面小节中已经调用了,即
getOrderDetail
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
| <!-- subpkg_consult/room/index.vue --> <script setup> // 省略前面小节的代码... // 问诊订单详情 const orderDetail = ref({}) // 获取问诊订单详情 async function getOrderDetail() { // 调用接口 const { code, data, message } = await orderDetailApi(props.orderId) // 检测接口是否调用成功 if (code !== 10000) return uni.utils.toast(message) // 渲染问诊订单数据 orderDetail.value = data } // 省略前面小节的代码... </script> <template> <view class="room-page"> <!-- 问诊订单状态 --> <room-status :status-value="orderDetail.statusValue" :countdown="orderDetail.countdown" :status="orderDetail.status" /> <!-- 省略前面小节的代码 --> </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
| <!-- subpkg_consult/room/components/room-status.vue --> <template> <!-- 咨询室状态 --> <view class="room-status"> <!-- 待接诊(status: 2) --> <view v-if="props.status === 2" class="status waiting"> {{ props.statusValue }} </view>
<!-- 咨询中(status: 3) --> <view class="status" v-if="props.status === 3"> <text class="label">{{ props.statusValue }}</text> <view class="time"> 剩余时间: <uni-countdown color="#3c3e42" :font-size="14" :show-day="false" :second="props.countdown" /> </view> </view>
<!-- 已完成(status: 4) --> <view v-if="props.status === 4" class="status"> <view class="wrap"> <uni-icons size="20" color="#121826" type="checkbox-filled" /> {{ props.statusValue }} </view> </view> </view> </template>
|
1.5 消息分段
每次重新建立 Socket 连接后(刷新页面),后端都会对数据进行分组,前端在进行展示时也相应的需要展示分段的时间节点,这个时间节点按通知消息类型处理。
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_consult/room/index.vue --> <script setup> // 省略前面小节的代码... // 接收消息列表 socket.on('chatMsgList', ({ code, data }) => { // 没有返回数据 if (code !== 10000) return // 提取列表数据 const tempList = [] data.forEach(({ createTime, items }) => { // 追加到消息列表中 tempList.push( // 构造一条数据,显示时间节点 { msgType: 31, msg: { content: createTime }, id: createTime, }, ...items ) })
// 追加到消息列表中 messageList.value.unshift(...tempList) }) // 省略后面小节的代码... </script>
|
在返回的数据中 data
是一个数组,每个单元是一个消息的分组,在对该数组遍历时前端构造一条数据放到数组单元中,被构告的这条件数据仅仅是要显示一个时间节点。
1.6 历史消息
用户下拉操作时分页获取聊天记录,按以下几个步骤来实现:
- 启动下拉刷新并监听下拉操作
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
| <!-- subpkg_consult/room/index.vue --> <script setup> // 省略前面小节的代码... // 关闭下拉动画交互 const refreshTrigger = ref(false)
// 省略前面小节的代码...
// 下拉获取历史消息 function onPullDownRefresh() { // 开启下拉交互动画 refreshTrigger.value = true setTimeout(() => { // 关闭下拉交互动画 refreshTrigger.value = false }, 1000) }
// 省略前面小节的代码... </script>
<template> <view class="room-page"> <!-- 省略前面小节的代码... --> <scroll-view @refresherrefresh="onPullDownRefresh" refresher-enabled :refresher-triggered="refreshTrigger" background-color="#f2f2f2" > ... </scroll-view> <!-- 省略前面小节的代码... --> </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
| <!-- subpkg_consult/room/index.vue --> <script setup> // 省略前面小节的代码... // 关闭下拉动画交互 const refreshTrigger = ref(false) // 上次获取历史消息节点 const lastTime = ref(dayjs().format('YYYY-MM-DD HH:mm:ss'))
// 省略前面小节的代码...
// 下拉获取历史消息 function onPullDownRefresh() { // 开启下拉交互动画 refreshTrigger.value = true // 获取历史消息 socket.emit('getChatMsgList', 20, lastTime.value, props.orderId) }
// 省略前面小节的代码... </script>
<template> <view class="room-page"> <!-- 省略前面小节的代码... --> <scroll-page @refresherrefresh="onPullDownRefresh" refresher-enabled :refresher-triggered="refreshTrigger" background-color="#f2f2f2" > ... </scroll-page> <!-- 省略前面小节的代码... --> </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
| <!-- subpkg_consult/room/index.vue --> <script setup> // 省略前面小节的代码... // 接收消息列表 socket.on('chatMsgList', ({ code, data }) => { // 关闭下拉交互动画 refreshTrigger.value = false // 没有返回数据 if (code !== 10000) return // 提取列表数据 const tempList = [] data.forEach(({ createTime, items }, index) => { // 获取消息的时间节点 if (index === 0) lastTime.value = createTime // 追加到消息列表中 tempList.push( { msgType: 31, msg: { content: createTime }, id: createTime, }, ...items ) })
// 是否获取到新数据 if (tempList.length === 0) return uni.utils.toast(' 没有更多聊天记录了 ') // 追加到消息列表中 messageList.value.unshift(...tempList) }) // 省略前面小节的代码... </script>
|
注意事项:
- 历史消息是以从后往前的顺序获取,将历史消息中第 1 个分组的时间节点做为下一次获取历史消息的起始点
- 获取数据即表示请求结束,要关闭下拉交互的动画
- 判断是否还存在更多的历史消息
支付宝支付账号,密码为 111111
scobys4865@sandbox.com
askgxl8276@sandbox.com