fix: unify collapse animation and scroll lock reset

This commit is contained in:
JOJO 2026-01-02 12:39:23 +08:00
parent 95285747c0
commit d34fbe963a
5 changed files with 56 additions and 10 deletions

View File

@ -57,6 +57,7 @@ import {
scrollToBottom as scrollToBottomHelper,
conditionalScrollToBottom as conditionalScrollToBottomHelper,
toggleScrollLock as toggleScrollLockHelper,
normalizeScrollLock,
scrollThinkingToBottom as scrollThinkingToBottomHelper
} from './composables/useScrollControl';
import {
@ -294,7 +295,7 @@ const appOptions = {
});
},
async mounted() {
async mounted() {
debugLog('Vue应用已挂载');
if (window.ensureCsrfToken) {
window.ensureCsrfToken().catch((err) => {
@ -308,6 +309,8 @@ const appOptions = {
await socketPromise;
this.$nextTick(() => {
this.ensureScrollListener();
// 刷新后若无输出,自动解锁滚动锁定
normalizeScrollLock(this);
});
setupShowImageObserver();

View File

@ -79,7 +79,10 @@
</div>
<span class="status-text">{{ group.action.streaming ? '正在思考...' : '思考过程' }}</span>
</div>
<div class="collapsible-content">
<div
class="collapsible-content"
:ref="el => registerCollapseContent(group.action.blockId || `${index}-thinking-${group.actionIndex}`, el)"
>
<div
class="content-inner thinking-content"
:ref="el => registerThinkingRef(group.action.blockId || `${index}-thinking-${group.actionIndex}`, el)"
@ -208,7 +211,10 @@
</div>
<span class="status-text">{{ action.streaming ? '正在思考...' : '思考过程' }}</span>
</div>
<div class="collapsible-content">
<div
class="collapsible-content"
:ref="el => registerCollapseContent(action.blockId || `${index}-thinking-${actionIndex}`, el)"
>
<div
class="content-inner thinking-content"
:ref="el => registerThinkingRef(action.blockId || `${index}-thinking-${actionIndex}`, el)"
@ -319,7 +325,10 @@
</div>
<span class="status-text">系统消息</span>
</div>
<div class="collapsible-content">
<div
class="collapsible-content"
:ref="el => registerCollapseContent(`system-${index}`, el)"
>
<div class="content-inner">
{{ msg.content }}
</div>
@ -359,6 +368,17 @@ const stackedBlocksEnabled = computed(() => personalization.experiments.stackedB
const DEFAULT_GENERATING_TEXT = '生成中…';
const rootEl = ref<HTMLElement | null>(null);
const thinkingRefs = new Map<string, HTMLElement | null>();
const registerCollapseContent = (key: string, el: Element | null) => {
if (!(el instanceof HTMLElement)) {
return;
}
requestAnimationFrame(() => {
const h = el.scrollHeight || el.offsetHeight || 0;
if (h > 0) {
el.style.setProperty('--collapse-max', `${h}px`);
}
});
};
function registerThinkingRef(key: string, el: Element | null) {
if (el instanceof HTMLElement) {

View File

@ -20,7 +20,10 @@
<span class="status-text">{{ getToolStatusText(action.tool) }}</span>
<span class="tool-desc">{{ getToolDescription(action.tool) }}</span>
</div>
<div class="collapsible-content">
<div
class="collapsible-content"
:ref="el => registerCollapseContent && registerCollapseContent(collapseKey || action.tool.id || action.id || 'tool', el)"
>
<div class="content-inner">
<div v-if="action.tool.name === 'web_search' && action.tool.result">
<div class="search-meta">
@ -75,6 +78,8 @@ defineProps<{
formatSearchTopic: (filters: Record<string, any>) => string;
formatSearchTime: (filters: Record<string, any>) => string;
streamingMessage: boolean;
registerCollapseContent?: (key: string, el: Element | null) => void;
collapseKey?: string;
}>();
defineEmits<{ (event: 'toggle'): void }>();

View File

@ -10,6 +10,8 @@ type ScrollContext = {
autoScrollEnabled?: boolean;
userScrolling?: boolean;
isOutputActive?: () => boolean;
streamingMessage?: boolean;
hasPendingToolActions?: () => boolean;
};
type ScrollOptions = {
@ -106,6 +108,21 @@ export function toggleScrollLock(ctx: ScrollContext) {
return nextState;
}
/**
* /
*
*/
export function normalizeScrollLock(ctx: ScrollContext) {
const active =
(typeof ctx.isOutputActive === 'function' ? ctx.isOutputActive() : false) ||
!!ctx.streamingMessage ||
(typeof ctx.hasPendingToolActions === 'function' ? ctx.hasPendingToolActions() : false);
if (!active && ctx.autoScrollEnabled) {
ctx.chatSetScrollState?.({ autoScrollEnabled: false, userScrolling: false });
}
}
export function scrollThinkingToBottom(ctx: ScrollContext, blockId: string) {
if (!blockId) {
return;

View File

@ -266,11 +266,12 @@
overflow: hidden;
opacity: 0;
transition:
max-height 0.26s cubic-bezier(0.4, 0, 0.2, 1),
opacity 0.26s cubic-bezier(0.4, 0, 0.2, 1);
max-height 260ms cubic-bezier(0.4, 0, 0.2, 1),
opacity 220ms cubic-bezier(0.4, 0, 0.2, 1);
/* 始终隐藏滚动条,避免堆叠模式展开时闪现 */
scrollbar-width: none;
-ms-overflow-style: none;
will-change: max-height, opacity;
}
.collapsible-content::-webkit-scrollbar {
@ -278,7 +279,7 @@
}
.collapsible-block.expanded .collapsible-content {
max-height: 600px;
max-height: var(--collapse-max, 600px);
overflow-y: auto;
opacity: 1;
}
@ -439,8 +440,8 @@
.stacked-block .collapsible-content {
transition:
max-height 280ms cubic-bezier(0.25, 0.9, 0.3, 1),
opacity 220ms ease;
max-height 260ms cubic-bezier(0.4, 0, 0.2, 1),
opacity 220ms cubic-bezier(0.4, 0, 0.2, 1);
}
.progress-indicator {