fix(quickdock): 修复快捷窗口 live 链路、raw 对象响应式与刷新误播动画

This commit is contained in:
JOJO 2026-07-22 19:33:34 +08:00
parent 165bcdb264
commit eb037ccb7c
5 changed files with 59 additions and 43 deletions

View File

@ -121,7 +121,11 @@ function scrollElIntoView(el: HTMLElement): Promise<void> {
});
}
async function playEnter(row: Row) {
async function playEnter(row: Row, delay = 0) {
if (delay > 0) {
//
await new Promise((resolve) => setTimeout(resolve, delay));
}
await nextTick();
const listEl = listRef.value;
const index = rows.value.indexOf(row);
@ -156,18 +160,24 @@ watch(
const existing = new Set(rows.value.map((row) => row.path));
const added = list.filter((item) => !existing.has(item.path));
if (added.length) {
if (live && !windowEnterShown && !rows.value.length) {
const playWindow = live && !windowEnterShown && !rows.value.length;
if (playWindow) {
windowEnterShown = true;
windowEntering.value = true;
setTimeout(() => {
windowEntering.value = false;
}, 320);
}
added.forEach((item) => {
added.forEach((item, i) => {
const row: Row = { path: item.path, entering: false, pendingEnter: live };
rows.value.push(row);
if (live) {
void playEnter(row);
// 0.3s stagger 60ms
//
// reactive rows.value raw
// raw trigger opacity:0
const delay = (playWindow ? 300 : 0) + i * 60;
void playEnter(rows.value[rows.value.length - 1], delay);
}
});
}

View File

@ -58,7 +58,6 @@ import { computed, nextTick, ref, watch } from 'vue';
import { storeToRefs } from 'pinia';
import { useSubAgentStore } from '@/stores/subAgent';
import { useBackgroundCommandStore } from '@/stores/backgroundCommand';
import { useConversationStore } from '@/stores/conversation';
import { useQuickDockStore } from '@/stores/quickDock';
/**
@ -102,7 +101,6 @@ const props = defineProps<{ kind: 'agent' | 'cmd' }>();
const subAgentStore = useSubAgentStore();
const bgStore = useBackgroundCommandStore();
const quickDock = useQuickDockStore();
const convStore = useConversationStore();
const { detail } = storeToRefs(quickDock);
const rows = ref<Row[]>([]);
@ -110,16 +108,6 @@ const windowEntering = ref(false);
const listRef = ref<HTMLElement | null>(null);
let windowEnterShown = false;
/** /
* 之后运行期间新出现的条目才播初始 true 保证首帧加载即静态 */
const staticNext = ref(true);
watch(
() => convStore.currentConversationId,
() => {
staticNext.value = true;
}
);
interface SourceItem {
id: string;
name: string;
@ -221,7 +209,11 @@ function scrollElIntoView(el: HTMLElement): Promise<void> {
});
}
async function playEnter(row: Row) {
async function playEnter(row: Row, delay = 0) {
if (delay > 0) {
//
await new Promise((resolve) => setTimeout(resolve, delay));
}
await nextTick();
const listEl = listRef.value;
const index = rows.value.indexOf(row);
@ -245,11 +237,6 @@ async function playEnter(row: Row) {
watch(
sourceItems,
(list) => {
//
//
const isStatic = staticNext.value;
staticNext.value = false;
const byId = new Map(list.map((item) => [item.id, item]));
//
@ -262,28 +249,39 @@ watch(
}
});
//
//
// /
const existingIds = new Set(rows.value.map((row) => row.id));
const added = list.filter((item) => !existingIds.has(item.id));
// running idle/
//
// running idle/done/ended
// //
const animate = added.some((item) => item.state === 'running');
if (added.length) {
if (!isStatic && !windowEnterShown && !rows.value.length) {
const playWindow = animate && !windowEnterShown && !rows.value.length;
if (playWindow) {
windowEnterShown = true;
windowEntering.value = true;
setTimeout(() => {
windowEntering.value = false;
}, 320);
}
added.forEach((item) => {
added.forEach((item, i) => {
const row: Row = {
id: item.id,
name: item.name,
state: item.state,
entering: false,
pendingEnter: !isStatic
pendingEnter: animate
};
rows.value.push(row);
if (!isStatic) {
void playEnter(row);
if (animate) {
// 0.3s stagger 60ms
//
// reactive raw raw
const delay = (playWindow ? 300 : 0) + i * 60;
void playEnter(rows.value[rows.value.length - 1], delay);
}
});
}

View File

@ -125,7 +125,7 @@ async function playLeave() {
await wait(current.length * LEAVE_STAGGER + LEAVE_DURATION + 20);
}
function renderEntering(tasks: TodoTask[]) {
function renderEntering(tasks: TodoTask[], baseDelay = 0) {
rows.value = tasks.map((t, i) => ({
key: `${t.index}:${t.title}`,
title: t.title,
@ -133,11 +133,12 @@ function renderEntering(tasks: TodoTask[]) {
entering: true,
leaving: false,
justDone: false,
enterDelay: i * ENTER_STAGGER,
// baseDelay
enterDelay: baseDelay + i * ENTER_STAGGER,
leaveDelay: 0
}));
// setTimeout animationend
const total = tasks.length * ENTER_STAGGER + 340 + 30;
const total = baseDelay + tasks.length * ENTER_STAGGER + 340 + 30;
const myGen = gen;
setTimeout(() => {
if (myGen !== gen) {
@ -238,7 +239,8 @@ watch(
setTimeout(() => {
windowEntering.value = false;
}, 320);
renderEntering(newTasks);
// 0.3s
renderEntering(newTasks, 300);
} else {
renderStatic(newTasks);
}

View File

@ -737,24 +737,26 @@ export async function initializeLegacySocket(ctx: any) {
if (data.conversation_id !== ctx.currentConversationId) {
return;
}
ctx.fileSetTodoList(data.todo_list || null);
// 实时事件必须带 live=truesocket 先于任务轮询到达,
// 若静态消费live=false会抢先把状态写掉等轮询的 live
// 事件再到时已无差异,快捷窗口的进入/打勾动画永远播不出来。
ctx.fileSetTodoList(data.todo_list || null, true);
});
// 快捷窗口:本次对话编辑/创建文件记录更新
ctx.socket.on('edited_files_updated', (data) => {
socketLog('收到编辑文件记录更新事件:', data);
if (!data) {
// 全局广播:与 todo_updated 同款过滤——事件必须属于当前对话;
// /new 等无对话场景直接丢弃(文件记录一定归属某个对话)。
if (!data || !data.conversation_id) {
return;
}
// 全局广播带 conversation_id只响应当前对话的更新
if (
data.conversation_id &&
ctx.currentConversationId &&
data.conversation_id !== ctx.currentConversationId
) {
if (data.conversation_id !== ctx.currentConversationId) {
return;
}
useQuickDockStore().setEditedFiles(data.edited_files || []);
// 同 todo_updatedsocket 是实时通道,带 live=true 让新增文件
// 播进入动画并滚动露出(静态插入在 5 行可视区外用户看不见)。
useQuickDockStore().setEditedFiles(data.edited_files || [], true);
});
// 系统就绪

View File

@ -176,16 +176,20 @@ export const useFileStore = defineStore('file', {
const response = await fetch(url);
const data = await response.json();
if (data && data.success) {
this.todoList = data.data || null;
// 顺序同 setTodoList先写 live 标记REST 拉取一律静态),再写数据
this.todoListLive = false;
this.todoList = data.data || null;
}
} catch (error) {
console.error('获取待办列表失败:', error);
}
},
setTodoList(payload: TodoList | null, live = false) {
this.todoList = payload;
// 必须先写 live 标记再写数据TodoWindow 的 watch 是 flush:'sync'
// todoList 一赋值就同步触发并读取 todoListLive顺序颠倒会让 watch
// 读到上一次调用残留的旧值,动画路径(划线/圆点弹跳)永远走不到。
this.todoListLive = live;
this.todoList = payload;
},
showContextMenu(payload: { node: FileNode; event: MouseEvent }) {
if (!payload || !payload.node) {