fix(frontend): restore conversation delete animation
Use a two-phase delete flow for conversation list items. Instead of immediately removing the deleted conversation from the array and relying only on transition-group leave hooks, mark the target conversation as pending deletion first so it can play the left-slide animation, then remove it after the animation completes. Also keep the list container mounted during delete mode so empty/loading branches do not interrupt the exit transition. Validation: npm run build --silent 2>&1 | tail -n 20
This commit is contained in:
parent
08d9812f45
commit
b1cf4b9ff2
@ -58,6 +58,7 @@ export const computed = {
|
|||||||
'searchResults',
|
'searchResults',
|
||||||
'conversationInsertAnimations',
|
'conversationInsertAnimations',
|
||||||
'conversationListAnimationMode',
|
'conversationListAnimationMode',
|
||||||
|
'pendingDeletingConversationIds',
|
||||||
'searchActive',
|
'searchActive',
|
||||||
'searchInProgress',
|
'searchInProgress',
|
||||||
'searchMoreAvailable',
|
'searchMoreAvailable',
|
||||||
|
|||||||
@ -631,17 +631,31 @@ export const conversationMethods = {
|
|||||||
history.replaceState({}, '', '/new');
|
history.replaceState({}, '', '/new');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 先从本地列表移除,让侧边栏执行删除动画;随后再刷新服务端列表补齐分页状态
|
// 先给目标项加 deleting 类执行左滑动画,再从本地列表移除。
|
||||||
|
// 这比完全依赖 transition-group leave 更稳定:即使列表即将为空或分支切换,也能先播放离场动画。
|
||||||
this.conversationListAnimationMode = 'delete';
|
this.conversationListAnimationMode = 'delete';
|
||||||
|
const deletingIds = Array.isArray(this.pendingDeletingConversationIds)
|
||||||
|
? this.pendingDeletingConversationIds
|
||||||
|
: [];
|
||||||
|
if (!deletingIds.includes(conversationId)) {
|
||||||
|
this.pendingDeletingConversationIds = [...deletingIds, conversationId];
|
||||||
|
}
|
||||||
|
await this.$nextTick();
|
||||||
|
await waitForAnimation(430);
|
||||||
|
|
||||||
this.conversations = this.conversations.filter(
|
this.conversations = this.conversations.filter(
|
||||||
(conversation) => conversation.id !== conversationId
|
(conversation) => conversation.id !== conversationId
|
||||||
);
|
);
|
||||||
this.searchResults = this.searchResults.filter(
|
this.searchResults = this.searchResults.filter(
|
||||||
(conversation) => conversation.id !== conversationId
|
(conversation) => conversation.id !== conversationId
|
||||||
);
|
);
|
||||||
|
this.pendingDeletingConversationIds = (Array.isArray(this.pendingDeletingConversationIds)
|
||||||
|
? this.pendingDeletingConversationIds
|
||||||
|
: []
|
||||||
|
).filter((id) => id !== conversationId);
|
||||||
|
|
||||||
// 等待“左滑离场 + 0.1s 后集体上移”动画结束,避免刷新列表打断过渡。
|
// 等待“左滑离场 + 0.1s 后集体上移”动画结束,避免刷新列表打断过渡。
|
||||||
await waitForAnimation(560);
|
await waitForAnimation(180);
|
||||||
|
|
||||||
// 刷新对话列表
|
// 刷新对话列表
|
||||||
this.conversationsOffset = 0;
|
this.conversationsOffset = 0;
|
||||||
@ -656,6 +670,11 @@ export const conversationMethods = {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
this.pendingDeletingConversationIds = (Array.isArray(this.pendingDeletingConversationIds)
|
||||||
|
? this.pendingDeletingConversationIds
|
||||||
|
: []
|
||||||
|
).filter((id) => id !== conversationId);
|
||||||
|
this.conversationListAnimationMode = 'idle';
|
||||||
console.error('删除对话异常:', error);
|
console.error('删除对话异常:', error);
|
||||||
this.uiPushToast({
|
this.uiPushToast({
|
||||||
title: '删除对话异常',
|
title: '删除对话异常',
|
||||||
|
|||||||
@ -129,12 +129,12 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="loading && !displayConversations.length && !searchActive"
|
v-if="loading && !displayConversations.length && !searchActive && !isDeletingConversation"
|
||||||
class="loading-conversations"
|
class="loading-conversations"
|
||||||
>
|
>
|
||||||
正在加载...
|
正在加载...
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="!displayConversations.length && !loading" class="no-conversations">
|
<div v-else-if="!displayConversations.length && !loading && !isDeletingConversation" class="no-conversations">
|
||||||
{{ searchActive ? '未找到匹配对话' : '暂无对话记录' }}
|
{{ searchActive ? '未找到匹配对话' : '暂无对话记录' }}
|
||||||
</div>
|
</div>
|
||||||
<transition-group
|
<transition-group
|
||||||
@ -153,7 +153,8 @@
|
|||||||
running: isConversationActive(conv.id) || isConversationCompleted(conv.id),
|
running: isConversationActive(conv.id) || isConversationCompleted(conv.id),
|
||||||
'insert-from-left': conversationInsertAnimations[conv.id] === 'create',
|
'insert-from-left': conversationInsertAnimations[conv.id] === 'create',
|
||||||
'insert-from-under': conversationInsertAnimations[conv.id] === 'duplicate',
|
'insert-from-under': conversationInsertAnimations[conv.id] === 'duplicate',
|
||||||
'duplicate-source-mask': conversationInsertAnimations[conv.id] === 'duplicateSource'
|
'duplicate-source-mask': conversationInsertAnimations[conv.id] === 'duplicateSource',
|
||||||
|
deleting: pendingDeletingConversationIds.includes(conv.id)
|
||||||
}"
|
}"
|
||||||
@click="
|
@click="
|
||||||
openActionMenuId = null;
|
openActionMenuId = null;
|
||||||
@ -338,6 +339,7 @@ const {
|
|||||||
conversations,
|
conversations,
|
||||||
conversationInsertAnimations,
|
conversationInsertAnimations,
|
||||||
conversationListAnimationMode,
|
conversationListAnimationMode,
|
||||||
|
pendingDeletingConversationIds,
|
||||||
conversationsLoading,
|
conversationsLoading,
|
||||||
hasMoreConversations: hasMore,
|
hasMoreConversations: hasMore,
|
||||||
loadingMoreConversations: loadingMore,
|
loadingMoreConversations: loadingMore,
|
||||||
@ -375,6 +377,7 @@ const displayConversations = computed(() =>
|
|||||||
const loading = computed(() =>
|
const loading = computed(() =>
|
||||||
searchActive.value ? searchInProgress.value : conversationsLoading.value
|
searchActive.value ? searchInProgress.value : conversationsLoading.value
|
||||||
);
|
);
|
||||||
|
const isDeletingConversation = computed(() => conversationListAnimationMode.value === 'delete');
|
||||||
const runningTaskItems = computed(() =>
|
const runningTaskItems = computed(() =>
|
||||||
Array.isArray(props.runningTasks)
|
Array.isArray(props.runningTasks)
|
||||||
? props.runningTasks.filter(
|
? props.runningTasks.filter(
|
||||||
|
|||||||
@ -13,6 +13,7 @@ interface ConversationState {
|
|||||||
searchResults: ConversationSummary[];
|
searchResults: ConversationSummary[];
|
||||||
conversationInsertAnimations: Record<string, 'create' | 'duplicate' | 'duplicateSource'>;
|
conversationInsertAnimations: Record<string, 'create' | 'duplicate' | 'duplicateSource'>;
|
||||||
conversationListAnimationMode: 'idle' | 'create' | 'duplicate' | 'delete';
|
conversationListAnimationMode: 'idle' | 'create' | 'duplicate' | 'delete';
|
||||||
|
pendingDeletingConversationIds: string[];
|
||||||
conversationsLoading: boolean;
|
conversationsLoading: boolean;
|
||||||
hasMoreConversations: boolean;
|
hasMoreConversations: boolean;
|
||||||
loadingMoreConversations: boolean;
|
loadingMoreConversations: boolean;
|
||||||
@ -37,6 +38,7 @@ export const useConversationStore = defineStore('conversation', {
|
|||||||
searchResults: [],
|
searchResults: [],
|
||||||
conversationInsertAnimations: {},
|
conversationInsertAnimations: {},
|
||||||
conversationListAnimationMode: 'idle',
|
conversationListAnimationMode: 'idle',
|
||||||
|
pendingDeletingConversationIds: [],
|
||||||
conversationsLoading: false,
|
conversationsLoading: false,
|
||||||
hasMoreConversations: false,
|
hasMoreConversations: false,
|
||||||
loadingMoreConversations: false,
|
loadingMoreConversations: false,
|
||||||
@ -60,6 +62,7 @@ export const useConversationStore = defineStore('conversation', {
|
|||||||
this.searchResults = [];
|
this.searchResults = [];
|
||||||
this.conversationInsertAnimations = {};
|
this.conversationInsertAnimations = {};
|
||||||
this.conversationListAnimationMode = 'idle';
|
this.conversationListAnimationMode = 'idle';
|
||||||
|
this.pendingDeletingConversationIds = [];
|
||||||
this.searchActive = false;
|
this.searchActive = false;
|
||||||
this.searchInProgress = false;
|
this.searchInProgress = false;
|
||||||
this.searchMoreAvailable = false;
|
this.searchMoreAvailable = false;
|
||||||
|
|||||||
@ -394,6 +394,15 @@ body[data-theme='dark']
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.conversation-sidebar .conversation-item.deleting {
|
||||||
|
transform: translateX(-112%);
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition:
|
||||||
|
transform 420ms cubic-bezier(0.32, 0, 0.67, 0),
|
||||||
|
opacity 260ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
.conversation-sidebar .conversation-item:hover,
|
.conversation-sidebar .conversation-item:hover,
|
||||||
.conversation-sidebar .conversation-item:focus-within,
|
.conversation-sidebar .conversation-item:focus-within,
|
||||||
.conversation-sidebar .conversation-item.active:hover,
|
.conversation-sidebar .conversation-item.active:hover,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user