fix: render monitor desktop immediately on load

This commit is contained in:
JOJO 2025-12-16 00:39:36 +08:00
parent fce6fb0eb8
commit fc88a22272
2 changed files with 137 additions and 13 deletions

View File

@ -228,13 +228,13 @@
</template>
<script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue';
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { storeToRefs } from 'pinia';
import { useMonitorStore } from '@/stores/monitor';
import { MonitorDirector, type MonitorElements } from '@/components/chat/monitor/MonitorDirector';
const monitorStore = useMonitorStore();
const { statusLabel, progressIndicator } = storeToRefs(monitorStore);
const { statusLabel, progressIndicator, lastTreeSnapshot } = storeToRefs(monitorStore);
const currentProgressLabel = computed(() => progressIndicator.value?.label || '');
const screenEl = ref<HTMLElement | null>(null);
@ -384,12 +384,27 @@ const mountDirector = async () => {
const instance = new MonitorDirector(elements, assets);
director.value = instance;
monitorStore.registerDriver(instance);
//
if (Array.isArray(monitorStore.lastTreeSnapshot) && monitorStore.lastTreeSnapshot.length) {
instance.setDesktopRoots(monitorStore.lastTreeSnapshot, { immediate: true });
}
};
onMounted(() => {
mountDirector();
});
// /
watch(
lastTreeSnapshot,
roots => {
if (director.value && Array.isArray(roots)) {
director.value.setDesktopRoots(roots, { immediate: true });
}
},
{ immediate: true }
);
onBeforeUnmount(() => {
monitorStore.unregisterDriver();
director.value?.destroy();

View File

@ -195,6 +195,8 @@ export class MonitorDirector implements MonitorDriver {
private desktopRoots: string[] = [];
private screenRect: DOMRect;
private pointerBase = { x: 60, y: 120 };
private pendingPointerTransform: { x: number; y: number; duration: number } | null = null;
private screenObserver: ResizeObserver | null = null;
private bubbleTimer: number | null = null;
private windowAnchors = new Map<HTMLElement, { x: number; y: number }>();
private destroyFns: Array<() => void> = [];
@ -254,6 +256,10 @@ export class MonitorDirector implements MonitorDriver {
private refreshScreenRect() {
const prev = this.screenRect;
const rect = this.elements.screen.getBoundingClientRect();
// 隐藏状态下 rect 可能为 0避免把指针归零
if (rect.width < 1 || rect.height < 1) {
return;
}
this.screenRect = rect;
if (prev && prev.width > 0 && prev.height > 0) {
const relX = this.pointerBase.x / prev.width;
@ -285,6 +291,7 @@ export class MonitorDirector implements MonitorDriver {
this.setupScenes();
this.bindTerminalInteractions();
this.populateDesktop();
this.setupScreenObserver();
this.extractionAnchor = this.windowAnchors.get(this.elements.extractionWindow) || this.extractionAnchor;
this.prepareExtractionTemplate();
this.layoutFloatingWindows();
@ -294,6 +301,7 @@ export class MonitorDirector implements MonitorDriver {
const resizeHandler = () => {
this.refreshScreenRect();
this.layoutFloatingWindows();
this.flushPendingPointerTransform();
};
window.addEventListener('resize', resizeHandler, { passive: true });
this.destroyFns.push(() => window.removeEventListener('resize', resizeHandler));
@ -312,7 +320,10 @@ export class MonitorDirector implements MonitorDriver {
preservePointer?: boolean;
preserveWindows?: boolean;
}) {
// 清理挂起的指针位置,避免后续尺寸变化时覆盖当前重置
this.pendingPointerTransform = null;
const preserveWindows = !!options?.preserveWindows;
const preservePointer = options?.preservePointer !== false; // 默认保留指针位置
this.cancelManualDrag();
if (!preserveWindows) {
this.resetManualPositions();
@ -327,7 +338,7 @@ export class MonitorDirector implements MonitorDriver {
} else {
this.stopBubbleTimers();
}
if (!options?.preservePointer) {
if (!preservePointer) {
this.pointerBase = { x: 60, y: 120 };
this.elements.mousePointer.style.transform = 'translate3d(60px, 120px, 0)';
}
@ -357,6 +368,9 @@ export class MonitorDirector implements MonitorDriver {
}
if (Array.isArray(options?.desktopRoots) && options?.desktopRoots.length) {
this.setDesktopRoots(options.desktopRoots, { immediate: true });
} else if (this.desktopRoots.length) {
// 保持当前根目录重新渲染,避免在重置后桌面空白
this.setDesktopRoots(this.desktopRoots, { immediate: true });
}
this.renderTerminalTabs();
}
@ -724,6 +738,10 @@ export class MonitorDirector implements MonitorDriver {
}
private positionBubble() {
// 隐藏状态display: none时不更新气泡与指针避免坐标被重置到原点
if (this.elements.screen.clientWidth < 1 || this.elements.screen.clientHeight < 1) {
return;
}
const bubble = this.elements.speechBubble;
const bubbleRect = bubble.getBoundingClientRect();
const width = bubbleRect.width || 220;
@ -1306,9 +1324,12 @@ export class MonitorDirector implements MonitorDriver {
await this.focusCommandInput();
const toDelete = this.commandCurrentText;
if (toDelete) {
for (let i = toDelete.length; i > 0; i -= 1) {
const len = toDelete.length;
const boost = 1 + Math.min(len / 40, 6); // 行越长删除越快
const interval = Math.max(6, Math.floor(18 / boost));
for (let i = len; i > 0; i -= 1) {
target.textContent = toDelete.slice(0, i - 1);
await sleep(18);
await sleep(interval);
}
this.commandCurrentText = '';
}
@ -2533,12 +2554,15 @@ export class MonitorDirector implements MonitorDriver {
execution: payload?.executionId || payload?.id,
path: rawPath
});
const editorAlreadyVisible = this.isWindowVisible(this.elements.editorWindow);
if (!editorAlreadyVisible) {
const targetEntry = await this.revealFileTarget(rawPath, { spawnDesktopFile: true });
if (targetEntry?.element) {
await this.openFileMenuAction(targetEntry.element, 'edit');
} else {
await this.movePointerToDesktop();
}
}
this.openEditorWindow(filename);
this.renderEditorPlaceholder('正在读取文件内容...');
const payloadBeforeLines = this.resolveEditorBeforeLines(payload);
@ -3409,6 +3433,9 @@ export class MonitorDirector implements MonitorDriver {
return;
}
this.refreshScreenRect();
if (this.elements.screen.clientWidth < 1 || this.elements.screen.clientHeight < 1) {
return;
}
this.raiseWindowForTarget(target);
if (!this.progressBubbleBase) {
this.dismissBubble(true);
@ -3419,6 +3446,9 @@ export class MonitorDirector implements MonitorDriver {
}
const { offsetX = 0, offsetY = 0, duration = 900 } = options;
const rect = target.getBoundingClientRect();
if (rect.width < 1 && rect.height < 1) {
return;
}
const desiredX = rect.left - this.screenRect.left + rect.width / 2 + offsetX;
const desiredY = rect.top - this.screenRect.top + rect.height / 2 + offsetY;
const pointerX = desiredX - POINTER_TIP_OFFSET.x;
@ -3494,9 +3524,26 @@ export class MonitorDirector implements MonitorDriver {
};
}
private flushPendingPointerTransform() {
if (!this.pendingPointerTransform) {
return;
}
const pending = this.pendingPointerTransform;
this.pendingPointerTransform = null;
this.updatePointerTransform(pending.x, pending.y, pending.duration);
}
private updatePointerTransform(x: number, y: number, duration = 0) {
const clampedX = Math.min(Math.max(0, x), Math.max(0, this.elements.screen.clientWidth - 10));
const clampedY = Math.min(Math.max(0, y), Math.max(0, this.elements.screen.clientHeight - 10));
if (!Number.isFinite(x) || !Number.isFinite(y)) {
return;
}
const screenWidth = this.elements.screen.clientWidth || this.screenRect?.width || 0;
const screenHeight = this.elements.screen.clientHeight || this.screenRect?.height || 0;
if (screenWidth < 1 || screenHeight < 1) {
return;
}
const clampedX = Math.min(Math.max(0, x), Math.max(0, screenWidth - 10));
const clampedY = Math.min(Math.max(0, y), Math.max(0, screenHeight - 10));
this.elements.mousePointer.style.setProperty('--mouse-duration', `${Math.max(duration, 0)}ms`);
this.elements.mousePointer.style.transform = `translate3d(${clampedX}px, ${clampedY}px, 0)`;
this.pointerBase = { x: clampedX, y: clampedY };
@ -3511,6 +3558,24 @@ export class MonitorDirector implements MonitorDriver {
};
}
private setupScreenObserver() {
if (typeof ResizeObserver === 'undefined') {
return;
}
this.screenObserver = new ResizeObserver(entries => {
const entry = Array.isArray(entries) ? entries[0] : null;
const width = entry?.contentRect?.width || this.elements.screen.clientWidth;
const height = entry?.contentRect?.height || this.elements.screen.clientHeight;
if (width < 1 || height < 1) {
return;
}
this.refreshScreenRect();
this.flushPendingPointerTransform();
});
this.screenObserver.observe(this.elements.screen);
this.destroyFns.push(() => this.screenObserver?.disconnect());
}
private showWindow(el: HTMLElement) {
el.classList.remove('closing');
el.classList.add('visible');
@ -4230,15 +4295,30 @@ export class MonitorDirector implements MonitorDriver {
private async animateEditorTransition(nextLines: any) {
const currentLines = this.editorScene.lines.slice();
const targetLines = this.sanitizeEditorLines(nextLines);
const lineCount = targetLines.length;
editorDebug('animate:start', {
currentLines: currentLines.length,
targetLines: targetLines.length
targetLines: lineCount
});
if (!currentLines.length && !targetLines.length) {
editorDebug('animate:both-empty');
this.renderEditorPlaceholder('(文件当前为空)');
return;
}
// 规则:
// - > 30 行:直接瞬间渲染
if (lineCount > 30) {
editorDebug('animate:skip-gt-30', { lineCount });
this.renderEditorSnapshot(targetLines);
return;
}
// - 9~30 行:逐行动画(不逐字符)
if (lineCount > 8) {
editorDebug('animate:line-fill', { lineCount });
await this.animateEditorLineFill(targetLines);
return;
}
// - ≤8 行:保留原逐字符/行内阈值逻辑
if (
currentLines.length > EDITOR_DIFF_LIMIT ||
targetLines.length > EDITOR_DIFF_LIMIT
@ -4296,6 +4376,35 @@ export class MonitorDirector implements MonitorDriver {
this.editorSpeedBoost = 1;
}
private async animateEditorLineFill(lines: string[]) {
const normalized = this.sanitizeEditorLines(lines);
if (!normalized.length) {
this.renderEditorPlaceholder('(文件当前为空)');
return;
}
const container = this.elements.editorBody;
container.innerHTML = '';
// 先放空行占位,再逐行填充文本,避免逐字符动画
normalized.forEach((_, index) => {
const row = this.buildEditorLineElement('', index, undefined, { prefill: false });
container.appendChild(row);
});
container.scrollTo({ top: 0 });
const delay = 28;
const rows = Array.from(container.children) as HTMLElement[];
for (let i = 0; i < rows.length; i += 1) {
const row = rows[i];
row.textContent = normalized[i] || ' ';
row.classList.add('visible');
this.adjustEditorScrollForLine(row);
await sleep(delay);
}
this.editorScene.lines = normalized.slice();
this.editorScene.placeholder = false;
this.syncEditorIndices();
this.editorSpeedBoost = 1;
}
private buildEditorDiff(before: string[], after: string[]): EditorOperation[] {
const m = before.length;
const n = after.length;