1
0

useChatScroll.ts 2.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748
  1. import { nextTick, onBeforeUnmount } from 'vue';
  2. import type ChatMessages from '/@/views/ventAI/manageAssistent/components/chat/ChatMessages.vue';
  3. /**
  4. * 消息区滚动控制:流式增量用 rAF 合并到每帧一次,显式调用走 async 版本(等 nextTick 再滚)。
  5. * 滚动目标实例由调用方以 getter 传入,composable 不直接依赖组件 ref。
  6. */
  7. export const useChatScroll = (getChatMessages: () => InstanceType<typeof ChatMessages> | undefined) => {
  8. let scrollRafId: number | null = null;
  9. const scrollToBottomThrottled = (force = false) => {
  10. if (scrollRafId) cancelAnimationFrame(scrollRafId);
  11. scrollRafId = requestAnimationFrame(async () => {
  12. scrollRafId = null;
  13. await nextTick();
  14. if (!force && !getChatMessages()?.isAtBottom) return;
  15. const el = getChatMessages()?.messagesRef;
  16. if (el) {
  17. el.scrollTop = el.scrollHeight - el.clientHeight;
  18. }
  19. });
  20. };
  21. // Async version for explicit await calls
  22. const scrollToBottom = async (force = false) => {
  23. await nextTick();
  24. if (!force && !getChatMessages()?.isAtBottom) return;
  25. const el = getChatMessages()?.messagesRef;
  26. if (el) {
  27. el.scrollTop = el.scrollHeight - el.clientHeight;
  28. }
  29. };
  30. // [审计-第四轮 R4-8] 卸载时取消挂起的 rAF。
  31. // 原行为:scrollRafId 仅在重入时 cancel 自身,组件卸载后不 cancel,残留回调会在卸载后
  32. // 再执行一次(内部全 optional chaining,无崩溃、无累积,但无意义)。
  33. // 现行为:onBeforeUnmount 里若有挂起 rAF 则 cancel 并复位。
  34. // 为什么:定时器/动画帧挂摘对称,消除卸载后的空转回调。
  35. // 注:本 composable 仅在组件 setup 内调用(AiAssistantModal),onBeforeUnmount 注册到该组件上。
  36. // 上游重构(master)在同一位置也加了等价实现(destroyOnClose 语义),此处按「冲突以审计分支为主」取审计版本。
  37. onBeforeUnmount(() => {
  38. if (scrollRafId !== null) {
  39. cancelAnimationFrame(scrollRafId);
  40. scrollRafId = null;
  41. }
  42. });
  43. return { scrollToBottom, scrollToBottomThrottled };
  44. };