主题
问诊室消息收发(四)
本页对应 问诊室消息收发,共 6 段视频,约 58 分钟。
学习目标
- 能够将订单详情中的
status、statusValue、countDown通过props传入问诊状态组件,并使用v-if按状态值渲染咨询中、待接诊、已完成等不同状态内容。 - 能够分析消息列表分段规则,说明“发送消息后刷新页面”与消息段生成的关系,并区分“仅刷新”或“仅发消息”不会新增分段。
- 能够遍历后端返回的嵌套消息数组,在每段消息前插入一条时间类型消息,构造并渲染消息分段显示所需的数据结构。
- 能够使用
scroll-view的下拉刷新配置与refresher-refresh事件实现下拉交互,并通过refresher-triggered在请求完成后关闭下拉动画。 - 能够调用历史消息接口,以订单 ID 和上一次时间节点作为分页依据获取历史消息,并更新下一次请求时间节点。
- 能够使用
unshift将历史消息插入列表前部,并在返回数据为空时提示“没有更多历史消息”。
知识点
一、订单状态数据渲染:先明确要传哪些字段
为什么需要:问诊订单在不同阶段要显示不同内容,比如“待接诊”“咨询中”“已完成”。如果只渲染一套静态结构,状态变化时页面不会跟着变。
怎么想:先看订单详情数据 orderDetail。转写中明确提到三个关键字段:status 表示状态值,statusValue 表示对应文字,countDown 表示咨询中的倒计时秒数。状态值对应关系为:2 待接诊、3 咨询中、4 已完成。
怎么做:在页面中向问诊组件传参,关键片段如下:
html
<room
:status="orderDetail.status"
:status-value="orderDetail.statusValue"
:count-down="orderDetail.countDown"
/>传完后在组件里通过 props 接收,再把 statusValue 放到文字位置,把 countDown 放到倒计时位置。状态切换用 v-if:props.status === 3 显示咨询中,=== 4 显示已完成,=== 2 显示待接诊。
易错点:转写中调试时倒计时没出来,最后发现是 status 相关拼写写错;另外 v-if 的条件值要和接口状态值一致。图标替换不影响功能,可后调。
二、消息为什么要分段:刷新页面与发消息共同决定
为什么需要:聊天列表如果把所有 items 直接展开,就看不出“哪一段是上次聊的、哪一段是这次聊的”。日常聊天工具会在每段消息前显示时间。
怎么想:转写中通过反复测试得出结论:只发消息不刷新,不分段;只刷新不发消息,也不分段;发过消息后再刷新页面,才会产生新的消息段。原因是刷新后 socket 连接和请求会重新走一遍,后端按段返回 date 数组,每个单元里有 items 和 createTime。
怎么做:遍历 date,在每一段 items 前插入一条“时间通知”消息,类型为 31,内容用该段 createTime,再把 items 展开追加,最后统一赋给消息列表。关键逻辑:
js
const tempList = []
date.forEach(({ createTime, items }) => {
tempList.push({ message: 31, content: createTime, id: createTime })
tempList.push(...items)
})
messageList.value = tempList易错点:createTime 要和 items 一起解构,否则会报未定义;tempList 要放在循环外,最后再赋值;历史消息倒序显示时用 unshift,不要用 push。
三、历史消息下拉交互:先做动画,再发请求
为什么需要:聊天记录多时,后端一次只返回约 20 条。用户往下拉应加载更早的历史消息,而不是一次性全部加载。
怎么想:先用 scroll-view 的下拉刷新能力做交互。转写中明确用到 refresher-enabled 开启下拉,@refresherrefresh 监听下拉事件,refresher-triggered 控制 loading 是否关闭。先用 setTimeout 模拟请求耗时,验证动画能关闭,再接入真实请求。
怎么做:下拉时把下拉状态设为 true,请求结束后设为 false。注意关闭动画要放在请求成功回调里,不是下拉函数里立刻关闭。转写中是在 chatMessageList 返回数据的地方关闭动画。
易错点:refresherrefresh 事件名容易写错;忘记关闭 refresher-triggered 会导致 loading 一直转;模拟定时器只是测试代码,真实请求后要删除。
四、下拉获取历史消息与分页时间点
为什么需要:只请求不更新分页参数,每次都会拿到同一批 20 条,数据重复。
怎么想:这个接口没有页码,而是用时间节点当“页码”。第一次用当前时间 lastTime 往前取 20 条;下一次用本次返回 date[0].createTime 作为新的 lastTime,继续往前取。转写中强调:每请求一次,就要更新下一次请求的时间点。
怎么做:调用 getChatMessageList(20, lastTime.value, orderId)。lastTime 初始用 dayjs 格式化为当前时间。请求成功后在消息列表回调里判断 date,遍历时取 index === 0 的 createTime 更新 lastTime。若返回空数组,提示“没有更多历史消息”并 return。
易错点:不更新 lastTime 会重复数据;空数据不 return 会继续追加;历史消息要 unshift 到顶部,否则顺序反了。转写开头还提到发消息后自动滚动到底部的问题,因要兼容小程序和 App,暂未找到理想方案,先保留。
一、先明确:消息为什么要分段显示
为什么需要:聊天列表如果从头到尾一条接一条铺开,用户很难判断“哪几条是同一时间段说的”。日常聊天工具都会在消息段前插入时间提示,让阅读有节奏。本节要解决的就是:让优医咨询的问诊聊天列表也具备这种时间段显示。
怎么想:不要一上来就在前端按时间差硬切。转写稿里先验证了后端返回结构:messageList.data 是数组,每个单元是一段,每段里有 items。分段不是前端随便算出来的,而是和“发消息后刷新页面”有关。结论要记牢:发过一次消息,再刷新页面,就会多一段;只刷新不发消息不会新增段;只发消息不刷新也不会新增段;发很多条再刷新一次,仍然只产生一段。
怎么做:先承认后端已经按段返回,前端要做的是“保留段”,而不是把段拆平。每段数据形如:一段里有若干 items,并且这一段带有 createTime。我们最终要在每段 items 前面插入一条“时间消息”。
易错点:不要把“刷新几次就有几段”当成规则;真正规则是“发消息 + 刷新”才产生新段。也不要在前端自己按分钟切割,否则会偏离项目已有数据结构。
二、旧代码为什么看不出分段
为什么需要:转写稿里指出,之前聊天列表虽然能显示消息,但看不出段。原因是旧代码遍历 data 后,把每个单元的 items 直接展开,全部塞进同一个数组。四段数据被展开后,视觉上就变成了一长串。
怎么想:先定位“分段信息在哪一步丢的”。不是样式问题,而是数据结构被拍平了。旧逻辑类似:遍历每一段,拿到 items,然后 ...items 展开合并。这样段与段之间的边界就消失了。
怎么做:保留“段”的概念。不要只得到一个扁平消息数组,而是构造一个新数组:每段先放一条时间消息,再放这一段的 items。这样渲染时自然会在每段前显示时间。
易错点:不要只给第一段加时间;不要把 items 再嵌套成二维数组;插入时间消息后仍要保证原消息顺序不变。
三、在每段前插入时间消息
为什么需要:分段边界需要一个可见的“时间节点”。转写稿的方案是构造一条通知类型消息,内容就是这一段的 createTime,类型按项目里时间消息的枚举写 31(以实际项目字段为准)。
怎么想:遍历 data,每个单元都能解构出 items 和 createTime。先把时间消息 push 进临时数组,再把 ...items 展开 push 进去。遍历结束后,把临时数组赋给 messageList。
怎么做:示意代码如下,字段名以项目实际为准:
js
const tempList = []
data.forEach(item => {
const { items, createTime } = item
tempList.push(
{
messageType: 31, // 时间通知消息,按项目枚举
content: createTime,
id: createTime
},
...items
)
})
messageList.value = tempList易错点:转写稿调试时出现过 createTime is not defined,原因是只解构了 items,没有解构同级的 createTime。另外,tempList 的赋值要放在循环外,不能每轮覆盖;push 时要用 ...items 展开,否则会把整个数组当成一条消息。
四、调试与历史消息分页的衔接
为什么需要:消息分段做好后,还要和历史消息下拉衔接。下拉时请求更多历史消息,若分页时间点不更新,每次都会拿到重复数据。
怎么想:历史消息接口按时间节点往前取 20 条。第一次用当前时间;之后每次请求完,用返回 data 第 0 个单元的 createTime 更新 lastTime,相当于“下一页页码”。历史消息要倒序插到列表头部,所以用 unshift,不是 push。如果临时数组为空,说明没有更多历史消息,要提示并 return。
怎么做:下拉刷新用 refresher-enabled 和 @refresherrefresh,请求成功后在 socket 返回 messageList 的回调里关闭动画;分页时更新 lastTime,无数据时提示“没有更多历史消息”。
易错点:下拉动画不要在 setTimeout 里关,要在真正拿到数据的回调里关;历史消息不要用 push 加到尾部;没有数据时不要继续追加。
先看问题:状态渲染与自动滚动
为什么需要:问诊订单有不同状态,组件要根据状态展示“待接诊、咨询中、已完成”等不同内容;聊天发消息后若能自动滚到底部会更友好,但本节明确说明:uni-app 还要兼容小程序和 App,不能简单用 DOM 操作,暂时没有理想方案,先保留问题。
怎么想:页面把订单详情中的 status、statusValue、countDown 传给消息组件。status 控制显示哪一块,statusValue 显示状态文字,countDown 给“咨询中”的倒计时使用。
怎么做:页面传参,组件用 props 接收,再用 v-if 判断:
html
<room
:status="orderDetail.status"
:status-value="orderDetail.statusValue"
:count-down="orderDetail.countDown"
/>组件中大致写:
html
<!-- 咨询中:status === 3 -->
<view v-if="status === 3">
<text>{{ statusValue }}</text>
<text>{{ countDown }}</text>
</view>
<!-- 待接诊:status === 2 -->
<view v-if="status === 2">{{ statusValue }}</view>
<!-- 已完成:status === 4 -->
<view v-if="status === 4">{{ statusValue }}</view>易错点:props 名字和传参名要对应;status、statusValue 拼写要一致;状态值和文案对应关系别写反。转写中调试时就是因为变量拼写/粘贴出错,倒计时一开始没出来,改正后才正常。
消息为什么分段:刷新与发送的关系
为什么需要:聊天工具通常会在消息列表里显示时间段,让用户知道哪些消息属于同一段会话。当前代码把每段 items 展开后直接放进一个数组,段信息就丢失了。
怎么想:后端返回的 date 是数组,每个单元是一段消息,每段里有 items。转写中反复验证出一个原则:只发消息不刷新,不会新增段;只刷新不发消息,也不会新增段;发过消息后再刷新页面,才会产生新的消息段。页面刷新后 socket 连接会重新建立,代码会重新走一遍。
怎么做:先观察原逻辑:遍历 date,把每段 items 展开,导致所有消息混在一起。要保留分段,就在每段 items 前插入一条“时间通知”消息。
易错点:不要误以为“刷新几次就有几段”;分段取决于发送消息与刷新的组合。原代码的展开操作本身没错,但它会让段与段之间的边界消失。
在每段前插入时间通知
为什么需要:每段消息前要显示一个时间节点,才能体现“分段显示”。
怎么想:遍历 date,每遇到一段,先构造一条时间消息,再展开这一段的 items。所有内容先放进临时数组,循环结束后再合并到消息列表。历史消息是倒着取的,显示时希望从顶部插入,所以用 unshift。
怎么做:核心逻辑如下:
js
const timeList = []
date.forEach((item, index) => {
const { createTime, items } = item
// 每段前插入一条时间通知
timeList.push({
messageType: 31,
content: createTime,
id: createTime
})
// 展开当前段的消息
timeList.push(...items)
// 用第 0 条数据的时间更新下一次请求时间点
if (index === 0) {
lastTime.value = createTime
}
})
// 历史消息从顶部插入
messageList.value.unshift(...timeList)易错点:createTime 和 items 是平级字段,必须从每段数据中解构出来;timeList 要定义在循环外,否则会反复覆盖;插入时间消息时不要乱写字段,转写中先用 AAA:111 测试,后来改成 messageType: 31、content: createTime。
下拉交互:refresher
为什么需要:打开聊天界面时后端不会一次返回全部历史消息,通常一次给 20 条,需要下拉加载更多。
怎么想:用 scroll-view 的下拉刷新能力。refresher-enabled 开启下拉,@refresherrefresh 监听下拉事件,refresher-triggered 控制 loading 状态。
怎么做:
html
<scroll-view
refresher-enabled
:refresher-triggered="refresherTriggered"
@refresherrefresh="onRefresh"
>js
const refresherTriggered = ref(false)
function onRefresh() {
refresherTriggered.value = true
// 先模拟请求耗时,后续换成真实请求
setTimeout(() => {
refresherTriggered.value = false
}, 1000)
}易错点:事件名容易写错,建议直接复制;下拉后必须把 refresherTriggered 改回 false,否则 loading 一直存在。真实请求时,关闭动画应放在服务端返回数据的回调里,而不是随便写在请求发出后。
历史消息请求与分页
为什么需要:每次请求都从同一个时间点取 20 条,数据会重复。必须像页码一样,每次更新“上一次的时间节点”。
怎么想:接口需要条数、时间节点、订单 ID。第一次以当前时间为节点往前取;拿到数据后,用返回数组第 0 条的 createTime 作为下一次请求的时间点。
怎么做:
js
// 初始时间节点:当前时间
const lastTime = ref(dayjs().format('YYYY-MM-DD HH:mm:ss'))
// 下拉时请求历史消息
getChatMessageList(20, lastTime.value, orderId)
// 服务端返回消息列表的回调中
onChatMessageList((date) => {
const timeList = []
date.forEach((item, index) => {
const { createTime, items } = item
timeList.push({
messageType: 31,
content: createTime,
id: createTime
})
timeList.push(...items)
if (index === 0) {
lastTime.value = createTime
}
})
if (timeList.length === 0) {
// 提示:没有更多历史消息了
refresherTriggered.value = false
return
}
messageList.value.unshift(...timeList)
refresherTriggered.value = false
})易错点:不更新 lastTime 会一直拿到重复数据;历史消息要用 unshift 从顶部插入,不能直接 push 到底部;没有数据时要提示“没有更多历史消息”并 return;关闭下拉动画要放在真正拿到返回数据之后。转写中先用 setTimeout 模拟请求,再改为在 on('chat message list') 回调里关闭动画,调试过程正好说明了这一点。
代码示例
示例一:问诊订单状态数据渲染(父传子 + 三种状态 + 倒计时)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>问诊订单状态渲染</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<style>
body { font-family: sans-serif; padding: 20px; }
.room-status { padding: 16px; background: #fff; border: 1px solid #eee; border-radius: 8px; }
.count-down { color: #16c79a; font-weight: bold; }
.done-icon { color: #999; }
</style>
</head>
<body>
<div id="app">
<!-- 页面中向组件传数据:kebab-case 写法 -->
<room-status
:status="orderDetail.status"
:status-value="orderDetail.statusValue"
:count-down="orderDetail.countDown"
></room-status>
</div>
<script>
const { createApp, ref, computed, onUnmounted } = Vue
/* ===== 子组件 room-status ===== */
const RoomStatus = {
// 1. 声明 props,模板里用 camelCase,传值用 kebab-case
props: {
status: { type: Number, default: 0 },
statusValue: { type: String, default: '' },
countDown: { type: Number, default: 0 }
},
template: `
<div class="room-status">
<!-- 状态 2:待接诊 -->
<div v-if="status === 2">
医生已接诊,等待 {{ statusValue }}
</div>
<!-- 状态 3:咨询中,需要倒计时 -->
<div v-if="status === 3">
咨询中,剩余 <span class="count-down">{{ timeText }}</span>
</div>
<!-- 状态 4:已完成 -->
<div v-if="status === 4">
<span class="done-icon">✔</span> 已完成 {{ statusValue }}
</div>
</div>
`,
setup(props) {
// 2. 倒计时:用 countDown(秒数)做初始值
const rest = ref(props.countDown)
const timer = setInterval(() => {
if (rest.value > 0) rest.value--
}, 1000)
// 3. 组件销毁时务必清除定时器
onUnmounted(() => clearInterval(timer))
const timeText = computed(() => {
const m = String(Math.floor(rest.value / 60)).padStart(2, '0')
const s = String(rest.value % 60).padStart(2, '0')
return `${m}:${s}`
})
return { timeText }
}
}
/* ===== 页面 ===== */
createApp({
components: { RoomStatus },
setup() {
// 数据都来自 orderDetail
const orderDetail = ref({
status: 3, // 3 = 咨询中
statusValue: '咨询中',
countDown: 300 // 秒
})
return { orderDetail }
}
}).mount('#app')
</script>
</body>
</html>关键行与易错点
- 传值必须从
orderDetail里取::status="orderDetail.status"、:status-value="orderDetail.statusValue"、:count-down="orderDetail.countDown",不要写成:status="status"。 - 模板写 kebab-case(
status-value/count-down),props里写 camelCase(statusValue/countDown),两者混写是最常见报错原因。 - 三个状态用
v-if并列控制,值分别是2(待接诊)、3(咨询中)、4(已完成);判断用===,注意后端返回的是数字而不是字符串。 - 倒计时必须在
onUnmounted里clearInterval,否则离开页面后定时器仍在跑。 - 转写稿里踩过的坑:
status拼写不一致(写成s7atus、sters之类)会导致倒计时不显示,改动后要顺手刷新验证。
示例二:消息分段显示(在每段数据前插入一条时间节点)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>消息分段显示</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<style>
body { font-family: sans-serif; padding: 20px; }
.msg-time { text-align: center; color: #999; font-size: 12px; margin: 12px 0; }
.msg-item { padding: 8px 12px; margin: 6px 0; background: #f2f2f2; border-radius: 6px; }
</style>
</head>
<body>
<div id="app">
<div v-for="(item, i) in messageList" :key="i">
<!-- msgType === 31 表示时间/通知消息 -->
<div v-if="item.msgType === 31" class="msg-time">{{ item.content }}</div>
<div v-else class="msg-item">{{ item.content }}</div>
</div>
</div>
<script>
const { createApp, ref, onMounted } = Vue
createApp({
setup() {
const messageList = ref([])
// 后端返回的分段数据:每段包含 createTime + items
const data = [
{
createTime: '2023-10-09 18:00:00',
items: [
{ msgType: 1, content: '你好' },
{ msgType: 1, content: '请问哪里不舒服' },
{ msgType: 2, content: '我头疼' }
]
},
{
createTime: '2023-10-09 18:10:00',
items: [
{ msgType: 1, content: '多久了' },
{ msgType: 2, content: '两天了' }
]
},
{
createTime: '2023-10-09 18:20:00',
items: [
{ msgType: 1, content: '有发烧吗' }
]
}
]
onMounted(() => {
// 1. 临时数组,先收集所有消息
const timeList = []
data.forEach(({ createTime, items }) => {
// 2. 每段数据前插一条时间消息,再把 items 展开
timeList.push(
{ msgType: 31, content: createTime, id: createTime },
...items
)
})
// 3. 循环结束后一次性赋值给 messageList
messageList.value = timeList
})
return { messageList }
}
}).mount('#app')
</script>
</body>
</html>关键行与易错点
- 核心逻辑是
push(时间对象, ...items):时间对象排在前面,...items展开在同一个push里,最终就相当于“在每段数据头部追加了一条时间消息”。 createTime必须在forEach的回调参数里解构(({ createTime, items })),否则会报createTime is not defined—— 这是转写稿里实际踩到的错。- 不能写成
push([时间对象, ...items]),那样会塞进一个二维数组,渲染时取不到字段。 messageList的赋值要放在forEach循环外面,循环里只往timeList里塞。- 时间消息用
msgType === 31区分,渲染时单独走时间样式,其它消息走普通气泡样式。 - 分段规则要理解清楚:发一次消息 + 刷新一次页面才产生一个新段;只刷新不发消息、或只发消息不刷新,都不会新增段。
示例三:下拉获取历史消息(下拉动画 + 时间节点分页 + 没有更多提示)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>下拉获取历史消息</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<style>
body { font-family: sans-serif; padding: 20px; }
.refresh-bar { text-align: center; height: 40px; line-height: 40px; color: #16c79a; }
.tip { text-align: center; color: #999; font-size: 12px; margin: 10px 0; }
.msg-time { text-align: center; color: #999; font-size: 12px; margin: 12px 0; }
.msg-item { padding: 8px 12px; margin: 6px 0; background: #f2f2f2; border-radius: 6px; }
</style>
</head>
<body>
<div id="app">
<!-- 真实 uni-app 中:
<scroll-view scroll-y refresher-enabled
:refresher-triggered="isTriggered"
@refresherrefresh="onRefresh"> -->
<div class="refresh-bar" v-if="isTriggered">正在加载历史消息...</div>
<div class="tip" v-if="noMore">没有更多历史消息了</div>
<div v-for="(item, i) in messageList" :key="i">
<div v-if="item.msgType === 31" class="msg-time">{{ item.content }}</div>
<div v-else class="msg-item">{{ item.content }}</div>
</div>
<button @click="onRefresh">↓ 下拉刷新(模拟)</button>
</div>
<script>
const { createApp, ref } = Vue
// 模拟后端:按时间节点往前取,返回分段结构
const MOCK_SEGMENTS = [
{ createTime: '2023-10-09 18:30:00', items: [{ msgType: 1, content: '消息 A1' }, { msgType: 1, content: '消息 A2' }] },
{ createTime: '2023-10-09 18:20:00', items: [{ msgType: 1, content: '消息 B1' }] },
{ createTime: '2023-10-09 18:10:00', items: [{ msgType: 1, content: '消息 C1' }, { msgType: 1, content: '消息 C2' }, { msgType: 1, content: '消息 C3' }] },
{ createTime: '2023-10-09 18:00:00', items: [{ msgType: 1, content: '消息 D1' }] }
]
// 模拟接口:以 lastTime 为界往前取(真实接口一次取 20 条)
function mockRequest(lastTime) {
const seg = MOCK_SEGMENTS.find(s => s.createTime < lastTime)
return seg ? [seg] : []
}
// 当前时间,格式 YYYY-MM-DD HH:mm:ss
function formatNow() {
const d = new Date()
const p = n => String(n).padStart(2, '0')
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ` +
`${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`
}
createApp({
setup() {
const messageList = ref([])
const isTriggered = ref(false) // 下拉动画状态,初始 false
const noMore = ref(false) // 是否还有历史消息
const lastTime = ref(formatNow()) // 上一次的时间节点,相当于页码
const orderId = '1001'
// 下拉触发
const onRefresh = () => {
if (noMore.value) return
isTriggered.value = true // 开启下拉动画
// 真实项目:socket.emit('getChatMessageList', 20, lastTime.value, orderId)
setTimeout(() => {
handleData(mockRequest(lastTime.value))
}, 500)
}
// 数据真正返回后的处理(对应 socket 的 chat message list 回调)
const handleData = (data) => {
// 1. 请求结束,关闭下拉动画
isTriggered.value = false
const timeList = []
data.forEach(({ createTime, items }, index) => {
// 2. 用第 0 条数据的时间更新下一次请求的时间节点
if (index === 0) lastTime.value = createTime
// 3. 每段前面插时间消息
timeList.push({ msgType: 31, content: createTime, id: createTime }, ...items)
})
// 4. 没有拿到数据 → 提示没有更多,并 return
if (timeList.length === 0) {
noMore.value = true
return
}
// 5. 历史消息是往前取的,要倒序插到头部
messageList.value.unshift(...timeList)
}
return { messageList, isTriggered, noMore, onRefresh }
}
}).mount('#app')
</script>
</body>
</html>关键行与易错点
- 下拉动画依赖两样东西:
refresher-enabled(开启下拉能力)和:refresher-triggered="isTriggered"(控制动画开关),isTriggered初始值必须是false。 - 关闭动画的位置很容易写错:不是发请求时关,而是在数据返回的回调里关(
handleData第一行isTriggered.value = false)。 - 分页的“页码”就是时间节点
lastTime:用data[0].createTime更新,等价于“从这一段的第 0 条继续往前取 20 条”。不改这个值,每次请求拿到的都是同一批数据。 - 历史消息是倒着取的,插入列表必须用
unshift(...timeList),用push会导致新拿到的旧消息跑到最底下。 timeList.length === 0的判断要在unshift之前做并return,否则会往列表里插一个空数组,同时提示“没有更多”。- 转写稿里强调:
dayjs是默认导出,import dayjs from 'dayjs'不能加大括号,否则报未定义。 - 数据必须“发一次消息 + 刷新一次页面”才会产生新段,所以测试分页前要先造足够多的分段数据(超过 20 条)才能看到效果。
常见错误
- 状态组件不显示或显示错状态 →
status、statusValue、countDown字段名/大小写传错,或v-if状态值对应错 → 核对 props 名与订单详情字段,按 2/3/4 逐一校验分支。 - 倒计时不出现 → 只传了状态值,未把
countDown传入倒计时组件或组件内未使用 → 补齐 props,并在倒计时位置替换为传入值。 - 聊天列表没有时间分段 → 直接遍历
date后展开每段items,丢失段边界 → 按段构造时间消息,再与每段items合并。 - 构造时间消息时报
createTime未定义 → 只解构了items,未解构同级的createTime→ 在遍历段数据时同时解构所需字段。 - 分段数据构造后页面不更新 → 临时数组只在循环内处理,未在循环外赋值给消息列表 → 循环结束后统一赋值给列表数据。
- 下拉刷新动画不关闭 → 在模拟定时器里关闭,未在真正拿到服务端消息的回调里关闭 → 在消息列表返回成功后设置
refresher-triggered = false。 - 下拉多次拿到重复历史消息 → 请求时间节点未更新,始终从同一时间点取数据 → 每次取到数据后,用本段第 0 条消息时间更新下一次请求时间。
- 历史消息顺序颠倒 → 新取到的旧消息用
push追加到末尾 → 改为unshift插入到列表头部。 - 没有历史消息时仍继续加载/无提示 → 未判断本次返回数据是否为空 → 判断临时列表长度为 0,提示“没有更多历史消息”并
return。
练习
- 给问诊订单状态组件补齐
status、statusValue、countDown三个 props,并分别验证状态 2、3、4 的展示结果。 - 将聊天消息按
date分段,在每一段前插入一条type: 31的时间消息,并确保时间取自该段第 0 条消息。 - 实现下拉刷新交互:开启
refresher-enabled,监听刷新事件,并在服务端消息返回后关闭刷新动画。 - 实现历史消息分页:每次请求后更新时间节点,使用
unshift插入旧消息,并处理“没有更多历史消息”的空数据场景。 - 自测并记录:连续下拉 3 次时,消息是否重复、顺序是否正确、刷新动画是否及时关闭。
课堂画面






原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 优医咨询-问诊订单状态数据渲染 | 10:49 |
| 2 | 优医咨询-消息分段显示分析 | 08:48 |
| 3 | 优医咨询-消息分段显示 | 12:19 |
| 4 | 优医咨询-历史消息下拉交互 | 07:06 |
| 5 | 优医咨询-下拉获取历史消息 | 10:37 |
| 6 | 优医咨询-下拉获取历史消息分页 | 08:21 |
小结
- 状态渲染的核心是“传参正确 + 分支状态值正确 + 倒计时单独传入”。
- 消息分段不是简单展开数组,而是要在每段数据前显式插入时间消息。
- 构造分段消息时,必须同时拿到段内
items和该段的时间字段。 - 下拉刷新动画的关闭时机应绑定真实数据返回,而不是模拟定时器。
- 历史消息分页的关键是持续更新请求时间节点,否则必然重复。
- 旧消息应插入列表头部,空数据要及时提示并终止后续加载。
- 跨端自动滚动到底部在当前方案中未妥善解决,不应作为本节验收阻塞项。