diff --git a/src/App.vue b/src/App.vue index 34df63e..4be4546 100644 --- a/src/App.vue +++ b/src/App.vue @@ -59,6 +59,7 @@ const state = reactive({ showOpenSceneDialog: false, pendingScenePath: null as string | null, pendingAction: null as 'close' | 'openScene' | null, + hasOpenedFile: false, // 标记是否已打开过文件或添加过内容 }); // Computed title @@ -71,6 +72,12 @@ const hasContent = computed(() => { return sceneManager.elements.value.length > 0; }); +// Check if should show welcome overlay +// Only show when: no content AND no file has been opened/used +const showWelcome = computed(() => { + return !hasContent.value && !state.hasOpenedFile; +}); + // Selection box state const selectionBox = reactive({ visible: false, @@ -125,7 +132,9 @@ function getContextMenuItems(): MenuItem[] { items.push({ label: '', action: () => {}, separator: true }); // File operations - const canSave = hasContent.value; + // 允许保存:有内容 或 已打开文件(可以保存空文件) + const canSave = hasContent.value || state.hasOpenedFile; + const canCreateNew = hasContent.value || state.hasOpenedFile; items.push( { @@ -148,7 +157,7 @@ function getContextMenuItems(): MenuItem[] { action: () => handleNew(), shortcut: 'Ctrl+N', checked: false, - disabled: !canSave, + disabled: !canCreateNew, } ); @@ -212,6 +221,9 @@ async function handleCanvasInitialized() { try { const loaded = await sceneService.loadOnStartup(); if (loaded) { + // 标记已打开文件 + state.hasOpenedFile = true; + // Sync renderer and view transform after loading canvasViewRef.value?.syncRendererElements(); canvasViewRef.value?.syncViewTransform(); @@ -305,6 +317,9 @@ async function handleTauriFileDrop(paths: string[], dropX: number, dropY: number // Handle image files if (imageFiles.length === 0) return; + // 标记已开始使用场景,防止删除元素后显示欢迎界面 + state.hasOpenedFile = true; + // Record state for undo const currentState = sceneManager.getState(); historyManager.record(currentState.elements, currentState.viewTransform); @@ -335,6 +350,9 @@ async function loadSceneFromPath(path: string) { try { const success = await sceneService.loadFromPath(path); if (success) { + // 标记已打开文件,防止删除元素后显示欢迎界面 + state.hasOpenedFile = true; + canvasViewRef.value?.syncRendererElements(); canvasViewRef.value?.syncViewTransform(); canvasViewRef.value?.getRenderer()?.requestRender(); @@ -355,8 +373,12 @@ async function loadSceneFromPath(path: string) { async function handleConfirmOpenScene() { state.showOpenSceneDialog = false; if (state.pendingScenePath) { + // 从指定路径加载(拖放文件) await loadSceneFromPath(state.pendingScenePath); state.pendingScenePath = null; + } else { + // 通过对话框选择文件(Ctrl+L) + await performLoad(); } } @@ -553,8 +575,8 @@ function handleGlobalAction(action: string, data: unknown) { // ============================================ async function handleSave() { - // Don't save if no content - if (!hasContent.value) return; + // 允许保存:有内容 或 已打开文件(可以保存空文件) + if (!hasContent.value && !state.hasOpenedFile) return; try { const success = await sceneService.save(); @@ -567,8 +589,8 @@ async function handleSave() { } async function handleSaveAs() { - // Don't save if no content - if (!hasContent.value) return; + // 允许另存为:有内容 或 已打开文件(可以保存空文件) + if (!hasContent.value && !state.hasOpenedFile) return; try { const success = await sceneService.saveAs(); @@ -581,6 +603,29 @@ async function handleSaveAs() { } async function handleLoad() { + // 如果有未保存的更改,显示提示对话框 + if (state.isDirty && (hasContent.value || state.hasOpenedFile)) { + state.pendingAction = 'openScene'; + state.pendingScenePath = null; // null 表示通过对话框选择文件 + state.showUnsavedDialog = true; + return; + } + + // 如果有内容但没有未保存更改,显示确认对话框 + if (hasContent.value || state.hasOpenedFile) { + state.pendingScenePath = null; + state.showOpenSceneDialog = true; + return; + } + + // 没有内容,直接打开 + await performLoad(); +} + +/** + * 实际执行加载操作 + */ +async function performLoad() { try { // Record state for undo before loading const currentState = sceneManager.getState(); @@ -588,6 +633,9 @@ async function handleLoad() { const success = await sceneService.load(); if (success) { + // 标记已打开文件,防止显示欢迎界面 + state.hasOpenedFile = true; + // Sync renderer and view transform after loading canvasViewRef.value?.syncRendererElements(); canvasViewRef.value?.syncViewTransform(); @@ -619,14 +667,18 @@ async function handleBrowseFiles() { } function handleNew() { - // Don't create new scene if already empty - if (!hasContent.value) return; + // Don't create new scene if already empty and no file opened + if (!hasContent.value && !state.hasOpenedFile) return; - // Record state for undo - const currentState = sceneManager.getState(); - historyManager.record(currentState.elements, currentState.viewTransform); + // Record state for undo if there's content + if (hasContent.value) { + const currentState = sceneManager.getState(); + historyManager.record(currentState.elements, currentState.viewTransform); + } sceneService.newScene(); + // 重置标志,允许显示欢迎界面 + state.hasOpenedFile = false; canvasViewRef.value?.syncRendererElements(); } @@ -1005,8 +1057,14 @@ async function executePendingAction() { if (action === 'close') { await forceCloseWindow(); - } else if (action === 'openScene' && scenePath) { - await loadSceneFromPath(scenePath); + } else if (action === 'openScene') { + if (scenePath) { + // 从指定路径加载(拖放文件) + await loadSceneFromPath(scenePath); + } else { + // 通过对话框选择文件(Ctrl+L) + await performLoad(); + } } } @@ -1140,6 +1198,7 @@ onUnmounted(() => { :always-on-top="state.alwaysOnTop" :is-dirty="state.isDirty" :has-content="hasContent" + :has-opened-file="state.hasOpenedFile" @minimize="minimizeWindow" @maximize="toggleFullscreen" @close="closeWindow" @@ -1156,9 +1215,10 @@ onUnmounted(() => { @file-drop="handleFileDrop" /> - + diff --git a/src/components/CanvasView.vue b/src/components/CanvasView.vue index eb3c1cd..d5ba581 100644 --- a/src/components/CanvasView.vue +++ b/src/components/CanvasView.vue @@ -502,6 +502,7 @@ async function handleDelete() { if (sceneManager.getSelectedCount() > 0) { recordState(); sceneManager.deleteSelected(); + sceneService.markDirty(); await syncRendererElements(); // Force render update renderer?.requestRender(); diff --git a/src/components/TitleBar.vue b/src/components/TitleBar.vue index 9bc6cd4..12fd5bd 100644 --- a/src/components/TitleBar.vue +++ b/src/components/TitleBar.vue @@ -10,6 +10,7 @@ const props = defineProps<{ alwaysOnTop?: boolean; isDirty?: boolean; hasContent?: boolean; + hasOpenedFile?: boolean; }>(); const emit = defineEmits<{ @@ -133,7 +134,7 @@ onUnmounted(() => { @mousedown="handleDragStart" > - +