重构: MessageList 拆分(提取 MessageItem+useMessageScroll, 1552->1386行)

- 提取 useMessageScroll.ts(滚动跟随/锁存/回底按钮)

- 提取 MessageItem.vue(按 role 渲染单条消息)

- 清理 MessageList.vue 中已移至子组件/ composable 的死代码
This commit is contained in:
2026-07-01 12:21:40 +08:00
parent 6771d396f0
commit 2c2b2d710e
4 changed files with 307 additions and 205 deletions

View File

@@ -0,0 +1,89 @@
//! 消息列表滚动控制 — 跟随/锁存/回底按钮
//!
//! 职责:悬浮底部按钮显隐、跟随意图锁存(isFollowingBottom)、
//! scrollToBottom 回底操作、onContentChange 内容变化滚动策略。
//!
//! 不涉及工具卡片折叠逻辑(父组件注入 onNearBottom 回调解耦)。
import { ref, nextTick, type Ref } from 'vue'
/** 底部阈值:距底部 <80px 视为"在底部" */
const NEAR_BOTTOM_PX = 80
/** 内容溢出阈值:scrollHeight - clientHeight >100px 认为内容溢出容器 */
const OVERFLOW_PX = 100
export function useMessageScroll(containerRef: Ref<HTMLDivElement | undefined>) {
// 回到底部按钮:内容溢出且不在底部时显示
const showBackToBottom = ref(false)
// B-260618-24: 跟随意图锁存。点回到底部/滚到底/发新消息→true;用户上滑→false。
const isFollowingBottom = ref(true)
// 边沿检测:上一帧是否在底部(用于触发 onNearBottom 回调)
let wasNearBottom = true
/** 用户是否在底部附近(上滑查看历史时不强制拉回) */
function isNearBottom(): boolean {
const el = containerRef.value
if (!el) return true
return el.scrollHeight - el.scrollTop - el.clientHeight < NEAR_BOTTOM_PX
}
/** 刷新回到底部按钮状态 */
function refreshBackToBottom(): void {
const el = containerRef.value
if (!el) { showBackToBottom.value = false; return }
const overflow = el.scrollHeight - el.clientHeight > OVERFLOW_PX
showBackToBottom.value = overflow && !isNearBottom()
}
/**
* 滚动事件处理:刷新回底按钮 + 底部边沿检测触发回调。
* @param onNearBottom 滚近底部时的回调(父组件传入,如收起工具卡片)
*/
function onMessagesScroll(onNearBottom?: () => void): void {
refreshBackToBottom()
const near = isNearBottom()
if (near && !wasNearBottom) {
onNearBottom?.()
}
wasNearBottom = near
isFollowingBottom.value = near
}
/** 滚动到容器底部 */
function scrollToBottom(smooth = false): void {
const el = containerRef.value
if (!el) return
isFollowingBottom.value = true // 显式回底 = 跟随意图
el.scrollTo({
top: el.scrollHeight,
behavior: smooth ? 'smooth' : 'instant',
})
showBackToBottom.value = false
}
/** 消息/流式内容变化时:跟随意图则自动滚,否则只刷新回底按钮 */
function onContentChange(): void {
if (isFollowingBottom.value) {
nextTick(scrollToBottom)
} else {
refreshBackToBottom()
}
}
/** 显式标记跟随意图(发新消息/切换对话时调用) */
function setFollowing(v: boolean): void {
isFollowingBottom.value = v
}
return {
showBackToBottom,
isFollowingBottom,
isNearBottom,
refreshBackToBottom,
onMessagesScroll,
scrollToBottom,
onContentChange,
setFollowing,
}
}