主题
问诊室消息收发(一)
本页对应 问诊室消息收发,共 7 段视频,约 1 小时 5 分。
学习目标
- 能够使用 Socket.IO 监听
receive chat message事件,将医生单条消息追加到消息列表,并通过控制台和 Vue Devtools 验证消息内容与messageType。 - 能够封装文字/图片消息组件,通过
props接收消息数据与type,使用v-if区分type=1的文字消息和type=4的图片消息,并正确渲染头像、时间、文字内容及图片 URL。 - 能够为消息列表渲染添加
type<=4条件,避免非文字/图片消息误渲染,并使用reverse样式区分医生与患者消息的左右布局。 - 能够在接收到消息后通过 Socket.IO 向服务端发送已读消息 ID,并使用 dayjs 将消息时间格式化为
HH:mm:ss显示。 - 能够按
type=22条件加载处方消息组件,向组件传入处方数据并渲染患者信息与药品列表。 - 能够按
type=23加载医生评价组件,使用v-model获取评分和评价内容,使用计算属性统计字数并控制提交按钮启用状态,通过点击事件切换匿名状态。
知识点
先明确消息类型,再决定渲染方式
为什么需要:问诊室本质是患者和医生聊天,但消息不只有文字,还有图片、通知、处方、评价等。不同类型展示结构不同,不能用一个模板硬套。
怎么想:先看消息里的类型值,再分流。转写中明确:文字消息类型为 1,图片消息类型为 4;患者、通知等还有 21、31、32、33,处方是 22,医生评价是 23。
怎么做:在遍历消息列表时加 v-if,只有文字/图片消息才加载对应组件;后续处方、评价再按类型追加组件。
易错点:不要只处理当前已有的三条消息,必须模拟医生发文字和图片;类型判断不能漏,否则通知消息也会被当成聊天内容渲染,出现头像错乱、图片重复等问题。
模拟医生端发消息并接收
为什么需要:本地页面一开始只有患者信息和通知,没有医生发来的文字、图片,必须构造测试数据。
怎么想:转写中使用“测试抢单”客户端模拟医生。先复制当前问诊订单的 orderId,在测试端抢单,让这个医生和当前患者建立问诊关系,然后医生端发送文字和图片。
怎么做:前端用 socket.on 监听接收消息事件,把新消息 push 到消息列表,并打印调试。核心代码是:
js
socket.on('receive chat message', (message) => {
console.log(message)
messageList.value.push(message)
})易错点:接收消息用 on,给服务端发消息才用 emit;订单号要复制完整;列表追加要用 push。转写中一开始用了 unshift,导致刷新后消息顺序颠倒,后来改回 push 才正常。
用 message-info 组件渲染文字和图片
为什么需要:文字消息和图片消息结构相似,都有头像、时间、内容区,只是内容区一个放文字、一个放图片,封装组件更利于复用。
怎么想:组件接收两个关键数据:info 接收整条消息,type 接收消息类型。页面中只在类型为 1 或 4 时加载组件。转写中为省事写过 <=4,本质是只放行 1 和 4;更稳妥是显式判断。
怎么做:组件内按类型渲染:
vue
<img :src="info.fromAvatar" alt="医生头像">
<span>{{ dataFormat(info.createTime) }}</span>
<p v-if="type === 1">{{ info.msg.content }}</p>
<img
v-if="type === 4"
:src="info.msg.picture.url"
alt="医生发送的图片"
>页面使用时可写:
vue
<message-info
v-if="message.msg.type <= 4"
:type="message.msg.type"
:info="message"
/>易错点:info 要传整条消息,不能只传 message.msg.content,否则头像、时间拿不到;图片路径不能写死,要从 info.msg.picture.url 取;医生消息一般在左侧,患者消息在右侧,模板里的 reverse 用来控制左右,处理医生回复时先不要加。
已读状态和时间格式化
为什么需要:收到医生消息后,要告诉后端“用户已读”;聊天时间通常只展示时分秒,完整日期不友好。
怎么想:已读是前端给服务端发消息,所以用 emit,把消息 ID 传过去;时间格式化用 dayjs。
怎么做:接收消息后调用已读事件;安装 dayjs,封装格式化方法:
js
import dayjs from 'dayjs'
const dataFormat = (time) => dayjs(time).format('HH:mm:ss')组件中把 info.createTime 传给 dataFormat。
易错点:emit 和 on 不要混;格式化方法要放在使用它的组件里;格式大小写要按 dayjs 规则写,HH 是小时,mm 是分钟,ss 是秒。
处方与医生评价消息的扩展
为什么需要:医生开处方、结束问诊后评价,也会作为消息进入问诊室,必须能展示对应模板。
怎么想:处方类型为 22,评价类型为 23。处方新建组件并在 v-if 为 22 时加载,把 message.msg 传入;评价要区分“未评价/已评价”,转写中先处理未评价。
怎么做:评价页用 uni-rate 获取分数,用输入框获取评价内容,用点击事件切换匿名状态;字数统计和按钮可用状态用计算属性:
js
const wordCount = computed(() => formData.value.content.length)
const btnEnable = computed(() => formData.value.score > 0)匿名切换利用转写中的小算法:
js
formData.value.匿名状态 = Math.abs(formData.value.匿名状态 - 1)易错点:组件使用要写在循环内部,否则拿不到当前遍历消息;评价必须先选星,按钮才可提交;字数统计用 content.length;匿名状态在 0 和 1 之间切换,取绝对值可避免出现 -1。
先看消息类型:为什么不能把所有消息都当聊天内容渲染
为什么需要:问诊室里的消息不只文字和图片,还有患者信息、通知、处方、评价等。转写稿里反复强调“等于21展示患者,等于31/32/33展示通知”,所以渲染前必须先看 type。
怎么想:文字消息 type === 1,图片消息 type === 4;只有这两个值才交给文字/图片消息组件处理。
怎么做:在消息列表循环里先加条件,满足再加载 message-info 组件。
易错点:转写稿中一开始没加条件,结果其他类型消息也加载了组件,头像和内容错乱。不要图省事写 type <= 4,当前虽能“投机取巧”,但以后出现 2、3 类型会误渲染。
html
<message-info
v-if="message.msg.type === 1 || message.msg.type === 4"
:info="message"
:type="message.msg.type"
/>(原笔记此处配有问诊室消息列表截图,alt:问诊室文字与图片消息列表)
接收医生单条消息:从“拿列表”到“逐条追加”
为什么需要:建立连接时只拿到一次消息列表,医生后续发的消息要实时接收。
怎么想:接收服务端数据用 socket.on,事件名按转写稿为 receiveChatMessage;收到后追加到 messageList。
怎么做:先打印 message 看结构,再 push 到列表。转写稿里先看到文字消息 content,再看到图片消息 messageType === 4 且 msg.picture.url 有值。
易错点:转写稿中“an shift”应纠正为 unshift。最初列表用了 unshift 导致刷新后顺序颠倒,后改为 push 从后往前追加,顺序才正常。
js
socket.on('receiveChatMessage', (message) => {
console.log(message)
messageList.value.push(message)
})封装 message-info 组件:模板结构要与数据字段对应
为什么需要:文字和图片消息结构相似,都有头像、时间、内容区,封装组件便于复用。
怎么想:组件接收两个外部数据:info 接收整条消息,type 判断文字还是图片。转写稿中一开始只传了 msg.content,导致头像拿不到;后来改为传整条 message。
怎么做:头像取 info.fromAvatar,时间取 info.createTime,文字取 info.msg.content,图片取 info.msg.picture.url。
易错点:图片路径不要写死。转写稿中先写死图片,后来才发现真实路径在 msg.picture.url。
html
<div class="message-info">
<img :src="info.fromAvatar" alt="医生头像" />
<span>{{ dayjs(info.createTime).format('HH:mm:ss') }}</span>
<div v-if="type === 1">{{ info.msg.content }}</div>
<img v-if="type === 4" :src="info.msg.picture.url" alt="图片消息" />
</div>js
const props = defineProps({
info: { type: Object, required: true },
type: { type: Number, required: true }
})reverse 类:区分“医生发的”和“自己发的”
为什么需要:聊天界面通常医生在左、患者在右,需要靠样式区分方向。
怎么想:转写稿里的“reward”应纠正为 reverse,即反转布局。加上 reverse 内容靠右,不加靠左。
怎么做:当前先处理医生回复,医生消息在左侧,所以先不加 reverse;后续患者自己发的消息再加。
易错点:不要把所有消息都加 reverse,否则左右方向全反。
时间格式化与已读回执:两个补充处理
为什么需要:直接展示 createTime 不友好;收到消息后还要告诉服务端“已读”。
怎么想:转写稿里的“GGS”应纠正为 dayjs,用 format('HH:mm:ss') 只保留时分秒;已读用 emit 发消息 ID。
怎么做:安装并引入 dayjs,封装 dateFormat 方法;收到消息后按接口文档 emit 已读事件。
易错点:转写稿里的“imit”应纠正为 emit;dayjs 要在使用组件内引入,别放错文件。
js
import dayjs from 'dayjs'
const dateFormat = (time) => dayjs(time).format('HH:mm:ss')js
// 已读事件名以接口文档为准
socket.emit('已读事件名', message.id)调试顺序:先通数据,再调样式
转写稿的调试路径很清楚:先模拟医生抢单,拿到订单号;医生发“你好”和图片;页面打印 message;检查 messageList 最后一条;再检查组件 props.info 是否收到整条数据;最后改图片路径。这样能快速定位“是没收到、没传对、还是没渲染”。常见坑是:只传 msg 导致头像缺失、图片路径写死、条件没加导致头像错乱、列表用 unshift 导致刷新顺序颠倒。按“接收 → 追加 → 传参 → 条件渲染 → 格式化”的顺序写,问题最容易收敛。
先明确本节要补的两件事
文字、图片消息的接收和基础渲染跑通后,本节还要补两个细节:一是收到医生消息后,要通知服务端“这条消息用户已经读了”;二是聊天时间不能直接展示完整时间串,要格式化成时:分:秒。转写稿里这两个点都不复杂,但都容易放错位置,下面按“为什么需要→怎么想→怎么做→易错点”拆开。
知识点一:收到消息后发送已读回执
为什么需要:消息列表里展示出医生回复,只代表前端收到了数据,服务端并不知道用户已经读过。如果不回执,后端无法维护消息状态,未读数、已读状态都会不准。
怎么想:接收消息用 socket.on('receive chat message', ...);给服务端发消息则用 socket.emit。所以收到单条消息、把它追加进列表后,再发一次“已读”事件,把这条消息的 ID 传过去即可。转写稿里明确说:接口文档中有“修改信息已读”的事件,需要传消息 ID。
怎么做:在接收消息的回调里先 push,再 emit。事件名以接口文档为准,这里用常量占位,避免写死:
js
const READ_EVENT = '修改信息已读' // 事件名以接口文档为准
socket.on('receive chat message', (message) => {
messageList.value.push(message)
socket.emit(READ_EVENT, message.msg.id)
})易错点:不要只 console.log 不落列表;不要用 unshift 做初始化追加,转写稿后面发现刷新后顺序错乱,根因就是初始化列表用了 unshift,改成 push 后顺序才正常。已读事件别自己猜英文名,按文档来。
知识点二:用 dayjs 格式化时间显示
为什么需要:聊天场景里通常只关心“时:分:秒”,完整日期串太长,界面不友好。转写稿说“这个时间不是特别友好,只保留时分秒”。
怎么想:时间格式化交给 dayjs。给它一个时间,再用 format 指定格式:H 小时、m 分钟、s 秒;要两位补零就用 HH:mm:ss。不要自己手写补零逻辑。
怎么做:先安装 dayjs,在组件里引入并封装方法:
js
import dayjs from 'dayjs'
const dataFormat = (time) => dayjs(time).format('HH:mm:ss')模板里把 createTime 传进去:
html
<span>{{ dataFormat(info.createTime) }}</span>易错点:format 里大小写有含义,HH:mm:ss 和 hh:mm:ss 不一样;方法要放在使用它的组件里,不要放到页面外层;引入 dayjs 也要在组件内。转写稿里就出现过“放错地方”,后来把方法和引包都挪到组件里才生效。
知识点三:在组件里完成格式化的调试过程
为什么需要:消息模板已经拆成 message-info 组件,时间展示发生在组件内部,所以格式化逻辑也应跟着组件走。
怎么想:先确认数据从父页面传到组件:info 接收整条消息,type 接收消息类型。组件里拿到 info.createTime 后再调用 dataFormat。转写稿的调试路径是:先传 info,在组件里打印/查看 props,确认时间字段存在,再把模板里的原始时间替换成方法调用。
怎么做:组件定义 props,模板里调用方法;保存后发一条医生消息,观察时间是否变成 HH:mm:ss。同时保留文字/图片的条件判断:类型为 1 显示文字,类型为 4 显示图片,图片路径用 info.msg.picture.url,不要写死。
html
<img :src="info.msg.picture.url" alt="医生发送的图片消息" />易错点:不要把所有消息都套进文字/图片模板,否则头像和内容会重复错乱;v-if 要判断类型 1 或 4;图片地址不要固定成某张测试图。
易错点汇总
已读回执要“收到即发”,但事件名和字段以接口文档为准;时间格式化要放组件内,格式用 HH:mm:ss;消息列表初始化用 push,不要用 unshift;文字/图片渲染必须加类型条件。按这个顺序写,本节两个补充点就能稳定落地。
代码示例
1. 问诊室接收医生消息并追加到消息列表(room/index.vue)
vue
<template>
<view class="room">
<view class="msg-list">
<template v-for="item in messageList" :key="item.id">
<!-- 只有文字(1)和图片(4)才交给消息组件渲染 -->
<message-info v-if="item.msgType <= 4" :type="item.msgType" :info="item" />
</template>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import MessageInfo from './components/message-info.vue'
// 消息列表
const messageList = ref([])
// 接收医生端回复的单条消息
socket.on('receive chat message', (message) => {
console.log(message) // 先打印,核对 msgType 是 1 还是 4
// 追加到消息列表:必须用 push,不能用 unshift
messageList.value.push(message)
// 告诉服务端:这条消息用户已经收到了(已读)
socket.emit('update msg status', message.id)
})
</script>
<style lang="scss" scoped>
.room {
.msg-list {
padding: 20rpx;
}
}
</style>要点解释:
socket.on('receive chat message', cb)只负责接收别人的数据;要给服务端发数据必须用socket.emit(...),这是本节的第一个补充点(修改消息已读状态)。- 收到消息后 一定要
push。转写稿里最初写成了unshift,结果一刷新消息顺序整个颠倒,改成push才恢复正常,这是本节最典型的易错点。 console.log(message)不是多余的:先确认这条消息的msgType到底是 1(文字)还是 4(图片),再去写渲染条件,否则条件写错会“渲染出一堆重复头像”。- 外层遍历时加
v-if="item.msgType <= 4"做过滤。因为当前消息类型里没有 2、3,所以用<= 4就等价于“只有 1 和 4”,不加这个条件,21/22/23/31 这些通知、处方、评价消息也会被塞进消息组件里。
2. 文字 / 图片消息渲染组件(room/components/message-info.vue)
vue
<template>
<div class="message-info">
<!-- 头像 -->
<image class="avatar" :src="info.fromAvatar" mode="aspectFill" />
<div class="body">
<!-- 时间:用 dayjs 格式化,只留时分秒 -->
<p class="time">{{ dataFormat(info.createTime) }}</p>
<!-- 文字消息:type === 1 -->
<p class="content" v-if="type === 1">{{ info.msg.content }}</p>
<!-- 图片消息:type === 4,路径来自 info.msg.picture.url -->
<image
class="picture"
v-if="type === 4"
:src="info.msg.picture.url"
mode="widthFix"
/>
</div>
</div>
</template>
<script setup>
import dayjs from 'dayjs'
const props = defineProps({
// 整条消息(不是只传 msg.content),头像、时间都靠它
info: { type: Object, required: true },
// 1 文字消息,4 图片消息
type: { type: Number, required: true }
})
// 时间格式化:只显示 时:分:秒
const dataFormat = (time) => dayjs(time).format('HH:mm:ss')
</script>
<style lang="scss" scoped>
.message-info {
display: flex;
padding: 20rpx 0;
.avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
}
.body {
margin-left: 20rpx;
.time {
font-size: 24rpx;
color: #999;
}
.content {
margin-top: 10rpx;
padding: 16rpx 24rpx;
background: #fff;
border-radius: 12rpx;
}
.picture {
margin-top: 10rpx;
width: 320rpx;
border-radius: 12rpx;
}
}
}
</style>要点解释:
- 父组件传的是 整条消息
info,不是msg.content。转写稿里第一次只传了msg.content,结果头像拿不到,info.fromAvatar是 undefined,头像就乱了——这是易错点。 - 文字和图片用两个互斥的
v-if(type === 1/type === 4)区分,同一时刻只会渲染其中一个。 - 图片地址必须是
info.msg.picture.url,不能写死。转写稿里一开始写死了一个固定图片,页面能显示但不是医生发的那张。 - 时间不要直接输出原始时间戳,用
dayjs(time).format('HH:mm:ss'),HH小时、mm分钟、ss秒,两位补零用大写/双写。 - 医生消息默认在左侧,所以不带
reverse样式;reverse是留给“自己发出去的消息”靠右显示的,这里不要加。
3. 医生评价组件:表单数据获取、字数统计与按钮校验(room/components/rate-info.vue)
vue
<template>
<div class="rate-info">
<!-- 评分:v-model 拿到分数 -->
<uni-rate v-model="formData.score" :max="5" />
<!-- 评价内容:v-model 拿到文本,maxlength 限制 150 -->
<uni-easyinput
v-model="formData.content"
type="textarea"
:maxlength="150"
placeholder="请输入评价内容"
/>
<p class="count">{{ wordCount }}/150</p>
<!-- 是否匿名:点击切换图标 + 切换 0/1 -->
<div class="anonymous" @click="toggleAnonymous">
<uni-icons
:type="formData.anonymous === 1 ? 'checkbox-filled' : 'circle'"
size="20"
/>
<span>匿名评价</span>
</div>
<!-- 未评分时按钮禁用 -->
<button class="submit" :disabled="!btnEnable">提交评价</button>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
// 评价相关的三个数据
const formData = ref({
score: 0, // 星星分数
content: '', // 评价内容
anonymous: 0 // 是否匿名:0 不匿名,1 匿名
})
// 字数统计(计算属性)
const wordCount = computed(() => formData.value.content.length)
// 是否允许提交:分数大于 0 才允许
const btnEnable = computed(() => formData.value.score > 0)
// 匿名状态在 0 和 1 之间切换
const toggleAnonymous = () => {
formData.value.anonymous = Math.abs(formData.value.anonymous - 1)
}
</script>
<style lang="scss" scoped>
.rate-info {
padding: 20rpx;
.count {
text-align: right;
font-size: 24rpx;
color: #999;
}
.anonymous {
display: flex;
align-items: center;
span {
margin-left: 10rpx;
font-size: 26rpx;
}
}
.submit {
margin-top: 30rpx;
}
}
</style>要点解释:
score和content直接用v-model绑定到formData,就能双向拿到用户输入;评分不点就是初始值 0,输入框不填就是空串。- 字数统计用
computed,formData.value.content.length,输入时实时变化;输入框本身再用:maxlength="150"从源头限制长度,两件事不要混在一起做。 - 提交按钮的禁用状态用
computed的btnEnable(score > 0),模板里:disabled="!btnEnable"取反。易错点:disabled不要写死成固定值,否则选了星也不会高亮可点。 - 匿名切换用的是
Math.abs(formData.value.anonymous - 1):0 减 1 得 -1,取绝对值得 1;1 减 1 得 0,正好实现 0/1 来回切换。必须取绝对值,否则会出现 -1。 - 图标用
formData.anonymous === 1三元判断:勾选显示checkbox-filled,未勾选显示circle;点击事件绑在外层div上,点文字也能切换。
常见错误
- 刷新后消息顺序错乱,新消息跑到历史消息前面 → 历史消息接收时用了
unshift,与后续实时消息追加方向不一致 → 统一改为push,或接收后按时间字段统一排序。 - 文字/图片消息渲染后头像重复、通知消息也被套用聊天模板 → 模板加载时未判断
message.msg.type→ 在v-for内显式判断type === 1 || type === 4再加载消息组件。 - 图片消息显示的是固定图片,不是医生发送的图片 → 图片路径写死,未取消息对象中的真实地址 → 改为读取
info.msg.picture.url等接口返回字段。 - 时间显示为完整原始格式,聊天区不友好 → 直接渲染
createTime,未做格式化 → 引入dayjs,在组件内封装格式化方法,仅显示时分秒。 - 收到消息后后端仍显示未读 → 只监听了接收事件,未发送已读回执 → 接收消息后通过
emit发送消息 ID,通知服务端修改已读状态。 - 处方/评价组件拿不到当前循环项数据 → 组件写在
v-for循环外,作用域内没有当前message→ 将组件放入循环内,并通过props传入当前消息对象。 - 评价提交按钮一直禁用或一直可点 →
disabled写死,未根据评分动态计算 → 用计算属性判断score > 0,模板中绑定:disabled="!enable"。 - 匿名勾选切换无效或出现负值 → 直接对 0/1 做减法,未处理
-1→ 使用Math.abs(current - 1)在 0/1 间切换,并由单一状态驱动图标。 - 字数统计不随输入变化 → 模板中直接写
content.length或未使用响应式计算 → 用computed基于表单内容返回字数,再插值展示。 - 只给消息组件传了
msg.content,导致头像、时间缺失 → 传参范围过小 → 改为传整条消息对象,组件内按需取字段。
练习
- 实现问诊室接收医生文字消息和图片消息:接收后追加到消息列表,并验证刷新后顺序不再错乱。
- 封装消息展示组件:根据
type分别渲染文字或图片,头像、时间、图片地址均从传入的完整消息对象中获取。 - 完成消息已读回执:收到医生消息后发送消息 ID 给服务端,并将时间格式化为“时:分:秒”展示。
- 实现处方消息
type === 22的条件渲染:组件放入循环内,传入当前消息,并渲染处方中的药品列表。 - 完成医生评价交互:星级评分、评价内容、字数统计、匿名切换、提交按钮可用状态验证。
课堂画面






原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 优医咨询-问诊室接收文字消息 | 07:28 |
| 2 | 优医咨询-问诊室文字图片消息渲染 | 15:53 |
| 3 | 优医咨询-格式化时显示和消息已读状态 | 07:24 |
| 4 | 优医咨询-问诊室处方消息 | 09:15 |
| 5 | 优医咨询-医生评价模板 | 07:04 |
| 6 | 优医咨询-获取医生评价数据 | 13:07 |
| 7 | 优医咨询-医生评价字数统计和数据验证 | 05:28 |
小结
- 消息渲染要先按
type做显式分流,避免用模糊条件把通知、处方、评价等消息误套进聊天模板。 - 列表追加方向必须与历史消息加载策略一致,否则实时消息和刷新后的顺序会冲突。
- 消息组件应接收完整消息对象,而不是只传内容字段,否则头像、时间、图片等扩展字段容易丢失。
- 实时消息链路要形成“接收消息 + 已读回执”的闭环,不能只做前端展示。
- 时间、字数、按钮可用状态等展示逻辑应集中到工具函数或计算属性中,避免模板里堆判断。
- 循环内使用的子组件必须放在
v-for作用域内,并显式传入当前项,否则条件渲染和取数都会出问题。 - 交互状态要由单一数据源驱动,如评分、匿名状态、按钮禁用状态,避免写死属性或直接操作 DOM。
- 排查渲染问题前先验证数据流:看接口消息结构、看
props是否传入、看v-for当前项,再改模板。