diff --git a/static/src/App.vue b/static/src/App.vue index 4636bd5..15cc52a 100644 --- a/static/src/App.vue +++ b/static/src/App.vue @@ -343,11 +343,11 @@ > -
-
+
+
+ new Promise((resolve) => { + window.setTimeout(resolve, durationMs); + }); debugLog('新对话创建成功:', newConversationId); traceLog('createNewConversation:created', { newConversationId }); @@ -571,10 +578,21 @@ export const conversationMethods = { total_messages: 0, total_tools: 0 }; + this.conversationInsertAnimations = { + ...this.conversationInsertAnimations, + [newConversationId]: 'create' + }; + this.conversationListAnimationMode = 'create'; this.conversations = [ placeholder, ...this.conversations.filter((conv) => conv && conv.id !== newConversationId) ]; + window.setTimeout(() => { + const rest = { ...this.conversationInsertAnimations }; + delete rest[newConversationId]; + this.conversationInsertAnimations = rest; + this.conversationListAnimationMode = 'idle'; + }, 700); await this.applyPendingVersioningToConversation(newConversationId); @@ -586,6 +604,9 @@ export const conversationMethods = { currentConversationId: this.currentConversationId }); + // 等待顶部插入动画完成,避免刷新列表时打断“整体下移 + 从左到右出现”。 + await waitForAnimation(540); + // 刷新对话列表获取最新统计 this.conversationsOffset = 0; await this.loadConversationsList(); @@ -632,6 +653,14 @@ export const conversationMethods = { return; } + const waitForAnimation = (durationMs) => + new Promise((resolve) => { + window.setTimeout(resolve, durationMs); + }); + + // 确认弹窗自身有 250ms 退出动画;等弹窗完全消失后,再开始列表删除动画。 + await waitForAnimation(270); + debugLog('删除对话:', conversationId); this.logMessageState('deleteConversation:start', { conversationId }); @@ -657,9 +686,22 @@ export const conversationMethods = { history.replaceState({}, '', '/new'); } + // 先从本地列表移除,让侧边栏执行删除动画;随后再刷新服务端列表补齐分页状态 + this.conversationListAnimationMode = 'delete'; + this.conversations = this.conversations.filter( + (conversation) => conversation.id !== conversationId + ); + this.searchResults = this.searchResults.filter( + (conversation) => conversation.id !== conversationId + ); + + // 等待“左滑离场 + 0.1s 后集体上移”动画结束,避免刷新列表打断过渡。 + await waitForAnimation(560); + // 刷新对话列表 this.conversationsOffset = 0; await this.loadConversationsList(); + this.conversationListAnimationMode = 'idle'; } else { console.error('删除对话失败:', result.message); this.uiPushToast({ @@ -689,12 +731,58 @@ export const conversationMethods = { if (response.ok && result.success) { const newId = result.duplicate_conversation_id; + const waitForAnimation = (durationMs) => + new Promise((resolve) => { + window.setTimeout(resolve, durationMs); + }); if (newId) { - await this.loadConversation(newId, { force: true }); - } + const sourceIndex = this.conversations.findIndex( + (conv) => conv && conv.id === conversationId + ); + const sourceConversation = sourceIndex >= 0 ? this.conversations[sourceIndex] : null; + const duplicateTitle = + result.load_result?.title || sourceConversation?.title || '复制的对话'; + const duplicatePlaceholder = { + id: newId, + title: duplicateTitle, + updated_at: new Date().toISOString(), + total_messages: sourceConversation?.total_messages || 0, + total_tools: sourceConversation?.total_tools || 0 + }; - this.conversationsOffset = 0; - await this.loadConversationsList(); + this.conversationInsertAnimations = { + ...this.conversationInsertAnimations, + [conversationId]: 'duplicateSource', + [newId]: 'duplicate' + }; + this.conversationListAnimationMode = 'duplicate'; + + const withoutDuplicate = this.conversations.filter((conv) => conv && conv.id !== newId); + const insertIndex = withoutDuplicate.findIndex( + (conv) => conv && conv.id === conversationId + ); + if (insertIndex >= 0) { + this.conversations = [ + ...withoutDuplicate.slice(0, insertIndex + 1), + duplicatePlaceholder, + ...withoutDuplicate.slice(insertIndex + 1) + ]; + } else { + this.conversations = [duplicatePlaceholder, ...withoutDuplicate]; + } + + window.setTimeout(() => { + const rest = { ...this.conversationInsertAnimations }; + delete rest[conversationId]; + delete rest[newId]; + this.conversationInsertAnimations = rest; + this.conversationListAnimationMode = 'idle'; + }, 860); + + // 先让“目标下方钻出 + 下方整体下移”动画播完,再加载复制出的对话。 + await waitForAnimation(860); + await this.loadConversation(newId, { force: true, preserveListPosition: true }); + } } else { const message = result.message || result.error || '复制失败'; this.uiPushToast({ diff --git a/static/src/components/panels/LeftPanel.vue b/static/src/components/panels/LeftPanel.vue index 16a8f95..170b4ec 100644 --- a/static/src/components/panels/LeftPanel.vue +++ b/static/src/components/panels/LeftPanel.vue @@ -310,16 +310,12 @@ const emits = defineEmits<{ const panelMenuWrapper = ref(null); const statusLogoSvg = statusLogoSvgRaw.replace(/fill="#000000"/g, 'fill="currentColor"'); const panelStyle = computed(() => { - if (props.collapsed) { - return { - width: '0px', - minWidth: '0px' - }; - } const px = `${props.width}px`; + const layoutWidth = props.collapsed ? '0px' : px; return { - width: px, - minWidth: px + '--workspace-content-width': px, + width: layoutWidth, + minWidth: layoutWidth }; }); diff --git a/static/src/components/sidebar/ConversationSidebar.vue b/static/src/components/sidebar/ConversationSidebar.vue index f06dda3..a8d167a 100644 --- a/static/src/components/sidebar/ConversationSidebar.vue +++ b/static/src/components/sidebar/ConversationSidebar.vue @@ -113,12 +113,23 @@
{{ searchActive ? '未找到匹配对话' : '暂无对话记录' }}
-
+