@@ -6,9 +6,9 @@
< / view >
<!-- ✅ 滚动区域 -- >
< scroll-view class = "flex-full overflow-hidden" scroll -y :scroll-into-view = "scrollIntoViewId" scroll -with -animation >
< scroll-view class = "flex-full overflow-hidden chat-scroll " scroll -y :scroll-into-view = "scrollIntoViewId" scroll -with -animation @scroll ="onScroll" @touchstart ="onTouchStart" @touchend ="onTouchEnd" @touchcancel ="onTouchEnd" >
< view class = "pt-12 px-12 pb-24 border-box" >
<!-- ✅ 答案 内容, 支持markdown -- >
<!-- ✅ 内容 , 支持markdown -- >
< ChatMarkdown :text = "answerText" / >
<!-- ✅ 底部锚点 ( 必须存在 ) -- >
@@ -40,20 +40,91 @@ let unsubscribe = null;
/** ✅ scroll-into-view 控制 */
const scrollIntoViewId = ref ( "" ) ;
/** 滚动控制状态 */
const isNearBottom = ref ( true ) ;
const scrollViewHeight = ref ( 0 ) ;
const SCROLL _THRESHOLD = 150 ; // px
/** 用户交互状态,用户滚动/触摸时临时禁用自动滚动 */
const userInteracting = ref ( false ) ;
let interactionTimer = null ;
/** 是否已完成(从 URL 参数判断),完成状态下不初始初自动滚到底部 */
let isFinishedOnInit = false ;
/** ✅ 防抖 */
let scrollTimer = null ;
function scrollToBottom ( ) {
const measureScrollViewHeight = ( ) => {
try {
// 使用 uni.createSelectorQuery 获取 scroll-view 的准确高度
uni . createSelectorQuery ( )
. select ( ".chat-scroll" )
. boundingClientRect ( ( rect ) => {
if ( rect && rect . height ) {
scrollViewHeight . value = rect . height ;
}
} )
. exec ( ) ;
} catch ( e ) { }
}
/** 生成展示用标题:去除前导 `#` 并截取前 6 字符(超过加省略号) */
const computeTitle = ( text = "" ) => {
const t = ( text || "" ) . replace ( /^#+\s*/ , "" ) ;
return t . length > 8 ? t . substring ( 0 , 8 ) + "..." : t ;
}
const onScroll = ( e ) => {
try {
const { scrollTop = 0 , scrollHeight = 0 } = e . detail || { } ;
// 计算距离底部的距离(使用准确的 scroll-view 高度)
const viewHeight = scrollViewHeight . value ;
const distanceToBottom = scrollHeight - scrollTop - viewHeight ;
// 判断是否在底部附近(允许 SCROLL_THRESHOLD 的误差范围)
// 注意:只更新 isNearBottom, 不在滚动时强制改变 userInteracting
const atBottom = distanceToBottom <= SCROLL _THRESHOLD ;
isNearBottom . value = atBottom ;
} catch ( e ) { }
}
const onTouchStart = ( ) => {
// 触摸开始时,立即标记为用户交互状态
userInteracting . value = true ;
clearTimeout ( interactionTimer ) ;
}
const onTouchEnd = ( ) => {
// 触摸结束后延迟一段时间再取消交互状态
// 这样即使用户快速滚动,也不会被中途打断
clearTimeout ( interactionTimer ) ;
interactionTimer = setTimeout ( ( ) => {
userInteracting . value = false ;
} , 600 ) ;
}
const scrollToBottom = ( ) => {
if ( scrollTimer ) return ;
if ( isFinishedOnInit ) return ;
scrollTimer = setTimeout ( ( ) => {
// ❗关键:强制触发滚动(小程序必须这样)
// 如果用户正在交互,则跳过本次自动滚动
if ( userInteracting . value ) {
scrollTimer = null ;
return ;
}
scrollIntoViewId . value = "" ;
nextTick ( ( ) => {
// 再次 nextTick + 延迟,兼容 markdown 渲染延迟
setTimeout ( ( ) => {
scrollIntoViewId . value = "bottom-anchor" ;
// 测量高度以便后续滚动判断准确
measureScrollViewHeight ( ) ;
} , 50 ) ;
} ) ;
@@ -61,32 +132,47 @@ function scrollToBottom() {
} , 100 ) ;
}
onLoad ( ( { message = "" , streamId = "" } ) => {
onLoad ( ( { message = "" , streamId = "" , finished = "0" } ) => {
// 记录初始完成状态
isFinishedOnInit = finished === "1" ;
console . log ( "LongAnswer onLoad with params:" , { message , streamId , finished } ) ;
// 初次测量 scroll-view 高度
nextTick ( ( ) => {
measureScrollViewHeight ( ) ;
} ) ;
if ( streamId ) {
// ✅ 流式数据
unsubscribe = StreamManager . subscribe (
streamId ,
( text = "" , finished = false ) => {
answerText . value = text || "" ;
if ( answerText . value . length > 6 ) {
title . value = answerText . value . substring ( 0 , 6 ) + "..." ;
}
title . value = computeTitle ( answerText . value ) ;
nextTick ( ( ) => {
scrollToBottom ( ) ;
// 每次接收数据都重新测量高度( content size 可能变化,比如加载图)
measureScrollViewHeight ( ) ;
// 流式完成时强制滚动到底部
if ( finished ) {
scrollToBottom ( ) ;
}
// 流式中的数据更新:只有在用户未交互且接近底部时才自动滚动
else if ( ! userInteracting . value && isNearBottom . value ) {
scrollToBottom ( ) ;
}
} ) ;
}
) ;
} else {
// ✅ 非流式
answerText . value = decodeURIComponent ( message || "" ) ;
if ( answerText . value . length > 6 ) {
title . value = answerText . value . substring ( 0 , 6 ) + "..." ;
}
title . value = computeTitle ( answerText . value ) ;
nextTick ( ( ) => {
// 只有在初始化为非完成状态时才自动滚到底部
scrollToBottom ( ) ;
} ) ;
}
@@ -96,6 +182,11 @@ onUnload(() => {
try {
unsubscribe && unsubscribe ( ) ;
} catch ( e ) { }
// 清理定时器,避免内存泄漏
try { clearTimeout ( scrollTimer ) ; } catch ( e ) { }
try { clearTimeout ( interactionTimer ) ; } catch ( e ) { }
scrollTimer = null ;
interactionTimer = null ;
} ) ;
< / script >