学习目标:

  • 能够基于 WebSocket 完成问诊全流程
  • 能够使用 uniCloud 云存储上传文件
  • 能够完成查看电子处方的功能
  • 能够完成医生评价的功能

一、问诊室

以对话聊天的方式向医生介绍病情并获取诊断方案,聊天的内容支持文字和图片两种形式。

首先新建一个页面并完成分包的配置:


{
    "subPackages": [
    {
      "root": "subpkg_consult",
      "pages": [
        {
          "path": "room/index",
          "style": {
            "navigationBarTitleText": "问诊室"
          }
        }
      ]
    },
  ]
}


该页面的内容特别多我们分段来数据模板代码移到项目当中:


<!-- 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>


// subpkg_consult/room/index.scss
.room-page {
  display: flex;
  flex-direction: column;
  height: 100vh;
  /* #ifdef H5 */
  height: calc(100vh - 44px);
  /* #endif */
  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


npm install socket.io-client


然后建立连接,在建立连接进需要传入参数和登录信息:

  • auth 登录状态信息,即 token
  • query 建立连接时传递的参数
  • transports 建立连接时使用的协议
  • timeout 超时设置
<!-- 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 方法进行监听来获取这些数据:


<!-- 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 患者消息

首先创建患者消息组件,组件的模板布局如下:


<!-- 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. 自定义组件的相关逻辑,要求组件能接收外部传入的数据
<!-- 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. 在页面应用组件并传入数据
<!-- 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. 在组件内部接收并渲染数据
<!-- 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>


  1. 大图查看患者病情图片,uni-app 提供了大图查看图片的 API uni.previewImage
<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

取消订单

灰底黑字

首先创建消息通知组伯,通知消息的模板如下:


<!-- 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. 定义组件的逻辑,要求能区分通知的类型并通过插槽来展示内容
<!-- 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. 在页面应用通知消息组件并传入数据
<!-- 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. 接收并渲染组件数据
<!-- 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


<!-- 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>


然后创建文字消息组件,组件模板如下:


<!-- 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. 定义组件的逻辑,要求能接收外部传入的数据
<!-- subpkg_consult/room/components/message-info.vue -->
<script setup>
  // 接收外部传入的数据
  const props = defineProps({
    info: {
      type: Object,
      default: {},
    },
    type: {
      type: Number,
      default: 1,
    },
  })
</script>


  1. 到页面中应用组件并传入数据
<!-- 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. 到组件是接收并渲染数据
<!-- 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>


  1. 处理消息的时间,安装 dayjs

npm install dayjs


<!-- 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


接下来分成3个步骤来实现:

  1. 定义组件逻辑,要求能接收组件外部传入的数据
<!-- subpkg_consult/room/components/prescription-info.vue -->
<script setup>
  // 接收组件外部传入的数据
  const props = defineProps({
    info: {
      type: Object,
      default: {},
    },
  })
</script>


  1. 在页面中应用组件并传入数据
<!-- 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. 在组件中接收并渲染数据
<!-- 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 医生评价

在医生端结束问诊后,患者可以对医生进行评价,医生评价的布局模板为:


<!-- 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个步骤来实现:

  1. 到页面中应用该组件,消息的类型值是 23
<!-- 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>


  1. 获取评价数据并对数据进行验证:
  • v-model 获取数据
  • 字数统计使用计算属性
  • 控制字数使用 maxlength
<!-- 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>

  1. 在提交评价时,需要获取问诊订单详情,在问诊订单详情中包含了医生的 ID接口文档在这里
// services/consult.js
import { http } from '@/utils/http'

// 省略前面小节的代码...

/**
 * 问诊订单详情
 */
export const orderDetailApi = (orderId) => {
  return http.get('/patient/consult/order/detail', { params: { orderId } })
}


将订单 ID 和医生 ID 传入组件


<!-- 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. 调用接口提交评价的数据,接口文档在这里
// services/doctor.js
import { http } from '@/utils/http'

// 省略了前面小节的代码...

/**
 * 评价医生
 */
export const evaluateDoctorApi = (data) => {
  return http.post('/patient/order/evaluate', data)
}


<!-- 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>


  1. 已评价状态,消息类型值 为 24
<!-- 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>


<!-- 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 发送消息

患者向医生告之病情及询问诊断方法,分为文字图片消息两种类型,且只有问诊订单状态处理咨询中时才以发送消息,问诊订单的状态包含在订单详情数据中。


<!-- 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个步骤来实现:

  1. 监听 uni-easyinput 组件的 confirm 事件并使用 v-model 获取表单的内容
<!-- 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. 触发服务端正在监听的事件类型,文档地址在这里
<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


// stores/user.js
import { ref } from 'vue'
import { defineStore } from 'pinia'

export const useUserStore = defineStore(
  'user',
  () => {
    // 记录用户登录状态
    const token = ref('')
    // 记录登录成功后要路转的地址(默认值为首页)
    const redirectURL = ref('/pages/index/index')
    // 跳转地址时采用的 API 名称
    const openType = ref('switchTab')
    
    // 用户ID
    const userId = ref('')

    return { token, userId, redirectURL, openType }
  },
  {
    persist: {
      paths: ['token', 'userId', 'redirectURL', 'openType'],
    },
  }
)


<!-- 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>


  1. 调整消息的对齐方式,患者消息靠右显示

在消息中包含的属性 from 是消息发送者的 ID,如果与登录用户的 ID 一致,则表示是患者发送的消息,消息的内容要靠右显示,类名 reverse 可以控制靠右对齐。


<!-- 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. 判断问诊订单状态是否为问诊中
<!-- 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>


  1. 调用 API 上传到 uniCloud 存储空间
<!-- 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. 向医生发送图片消息,文档地址在这里
<!-- 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 问诊订单状态

患者在与医生对话的过程中问诊订单状态会发生改变,包括待支付、待接诊、咨询中、已完成、已取消,在页面的顶部要根据订单的状态展示不同的内容。

  1. 将问诊状态的布局模板独立到组件中,要求组件能接收3个数据
  • status 问诊订单的状态值
  • statusValue 问诊订单的文字描述
  • countdown 倒计时剩余时长
<!-- 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>


  1. 在页面中应用组件并传入数据,查询订单状态的的 API 在前面小节中已经调用了,即 getOrderDetail
<!-- 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. 根据传入组件的订单状态展示数据
<!-- 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 连接后(刷新页面),后端都会对数据进行分组,前端在进行展示时也相应的需要展示分段的时间节点,这个时间节点按通知消息类型处理


<!-- 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. 启动下拉刷新并监听下拉操作
<!-- 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. 触发后端定义的事件类型获取历史消息,文档地址在这里。
<!-- 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. 更新时间节点,获取的历史消息会返回给客户端
<!-- 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个分组的时间节点做为下一次获取历史消息的起始点
  • 获取数据即表示请求结束,要关闭下拉交互的动画
  • 判断是否还存在更多的历史消息