diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 15f2a913..7e2217e8 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -24,7 +24,7 @@ "papaparse": "^5.5.3", "pinia": "^3.0.4", "swiper": "^12.1.4", - "tdesign-icons-vue-next": "^0.4.4", + "tdesign-icons-vue-next": "0.4.4", "tdesign-vue-next": "^1.19.2", "vue": "^3.5.34", "vue-demi": "^0.14.10", diff --git a/frontend/package.json b/frontend/package.json index e7f4226b..b36fb558 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -28,7 +28,7 @@ "papaparse": "^5.5.3", "pinia": "^3.0.4", "swiper": "^12.1.4", - "tdesign-icons-vue-next": "^0.4.4", + "tdesign-icons-vue-next": "0.4.4", "tdesign-vue-next": "^1.19.2", "vue": "^3.5.34", "vue-demi": "^0.14.10", @@ -54,11 +54,13 @@ "overrides": { "lightningcss": "none", "esbuild": "^0.25.0", - "serialize-javascript": "^7.0.3" + "serialize-javascript": "^7.0.3", + "tdesign-icons-vue-next": "0.4.4" }, "resolutions": { "lightningcss": "none", "esbuild": "^0.25.0", - "serialize-javascript": "^7.0.3" + "serialize-javascript": "^7.0.3", + "tdesign-icons-vue-next": "0.4.4" } } diff --git a/frontend/src/utils/tdesign-icon-offline.ts b/frontend/src/utils/tdesign-icon-offline.ts index ffa2753d..5700b790 100644 --- a/frontend/src/utils/tdesign-icon-offline.ts +++ b/frontend/src/utils/tdesign-icon-offline.ts @@ -24,15 +24,15 @@ const SVG_SCRIPT_CLASS = "t-svg-js-stylesheet--unique-class"; const ICONFONT_LINK_CLASS = "t-iconfont-stylesheet--unique-class"; // 对齐 tdesign-icons-vue-next 0.4.x 内部硬编码的地址;多版本号并存以兼容潜在升级。 -const BLOCKED_SCRIPT_URLS = [ - "https://tdesign.gtimg.com/icon/0.4.0/fonts/index.js", - "https://tdesign.gtimg.com/icon/0.4.1/fonts/index.js", -]; +const BLOCKED_ICON_VERSIONS = ["0.4.0", "0.4.1", "0.4.2", "0.4.3", "0.4.4"]; -const BLOCKED_LINK_URLS = [ - "https://tdesign.gtimg.com/icon/0.4.0/fonts/index.css", - "https://tdesign.gtimg.com/icon/0.4.1/fonts/index.css", -]; +const BLOCKED_SCRIPT_URLS = BLOCKED_ICON_VERSIONS.map( + (version) => `https://tdesign.gtimg.com/icon/${version}/fonts/index.js`, +); + +const BLOCKED_LINK_URLS = BLOCKED_ICON_VERSIONS.map( + (version) => `https://tdesign.gtimg.com/icon/${version}/fonts/index.css`, +); let installed = false; diff --git a/frontend/src/views/chat/index.vue b/frontend/src/views/chat/index.vue index 41234654..e11d8567 100644 --- a/frontend/src/views/chat/index.vue +++ b/frontend/src/views/chat/index.vue @@ -274,6 +274,16 @@ const getUserQuery = (index) => { } return ''; }; + +const findLastMessage = (predicate) => { + for (let i = messagesList.length - 1; i >= 0; i--) { + const item = messagesList[i]; + if (predicate(item)) { + return item; + } + } + return undefined; +}; watch([() => route.params], (newvalue) => { isFirstEnter.value = true; if (newvalue[0].chatid) { @@ -732,7 +742,7 @@ onChunk((data) => { }); // 检查是否是继续流式传输(消息已存在) - const existingMessage = messagesList.findLast((item) => item.id === data.id || item.request_id === data.id); + const existingMessage = findLastMessage((item) => item.id === data.id || item.request_id === data.id); if (!existingMessage) { // 新消息,设置 loading 状态 loading.value = true; @@ -783,7 +793,7 @@ onChunk((data) => { return; } // 非 Agent 模式:将 references 保存到消息中供 botmsg 使用 - let existingMessage = messagesList.findLast((item) => item.request_id === data.id || item.id === data.id); + let existingMessage = findLastMessage((item) => item.request_id === data.id || item.id === data.id); // 如果消息还不存在,先创建一个空的 assistant 消息 if (!existingMessage) { @@ -831,7 +841,7 @@ onChunk((data) => { // 文案拼进 content,保留用户点停止时已经流式输出的内容不变。 if (data.response_type === 'stop') { console.log('[Stop Event] Non-agent generation stopped'); - const stoppedMessage = messagesList.findLast((item) => { + const stoppedMessage = findLastMessage((item) => { if (item.request_id === data.id) return true; return item.id === data.id; }); @@ -846,7 +856,7 @@ onChunk((data) => { } // 检查消息是否已经完成,如果已完成则忽略后续的完成事件(防止空内容覆盖) - const existingMessage = messagesList.findLast((item) => { + const existingMessage = findLastMessage((item) => { if (item.request_id === data.id) { return true } @@ -902,7 +912,7 @@ onChunk((data) => { }) // 处理 Agent 流式数据 (Cursor-style UI) const handleAgentChunk = (data) => { - let message = messagesList.findLast((item) => item.request_id === data.id || item.id === data.id); + let message = findLastMessage((item) => item.request_id === data.id || item.id === data.id); if (!message) { // 创建新的 Assistant 消息 - 此时开始显示内容,关闭 loading @@ -1266,7 +1276,7 @@ const handleAgentChunk = (data) => { }; const updateAssistantSession = (payload) => { - const message = messagesList.findLast((item) => { + const message = findLastMessage((item) => { if (item.request_id === payload.id) { return true } @@ -1549,16 +1559,19 @@ onBeforeRouteUpdate((to, from, next) => { width: 100%; /* - 给每条消息加 containment: - - contain: layout paint style → 一条消息的内部布局/重绘不会让浏览器去 invalidate 整个文档 - (之前 hover 到 session 列表也变白就是因为 invalidation 跨边界扩散)。 - - content-visibility: auto + contain-intrinsic-size → 视口外的消息直接跳过渲染, - 极大降低长会话的 layout / paint 成本。 + 给每条消息加 layout/style containment: + - 一条消息的内部布局变化不再让浏览器去 invalidate 整个文档, + 这是修掉"hover 到 session 列表也变白"那个问题的关键。 + - 不要再用 content-visibility: auto / contain-intrinsic-size: + agent 消息真实高度差异巨大(几百 ~ 数千 px),估的占位高度会让消息进入视口时 + 反复发生"占位 -> 真实高度"的大幅 layout shift + 首次 paint 滞后, + 反而在向上滚动时制造"未画完"的白屏闪烁。 + 当前 handleMsgList 全流程 ~50ms,根本无需跳过渲染,老老实实正常渲染最稳。 + - 不开 contain: paint:AgentStreamDisplay 里有 tooltip / popover 等会溢出的浮层, + paint containment 会把它们裁掉。 */ .msg-item-wrapper { - contain: layout paint style; - content-visibility: auto; - contain-intrinsic-size: auto 400px; + contain: layout style; } .botanswer_laoding_gif {