From 75c26d479abfe8fc91c02060bd8628e92cab5fc8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=B0=8F=E7=85=9C?= <2082529121@qq.com>
Date: Fri, 19 Dec 2025 20:46:30 +0800
Subject: [PATCH] =?UTF-8?q?feat(=E6=96=87=E4=BB=B6=E7=AE=A1=E7=90=86):=20?=
=?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=B7=B2=E6=89=93=E5=BC=80=E6=96=87=E4=BB=B6?=
=?UTF-8?q?=E7=8A=B6=E6=80=81=E6=A0=87=E8=AE=B0=E5=8F=8A=E7=9B=B8=E5=BA=94?=
=?UTF-8?q?=E9=80=BB=E8=BE=91?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
引入hasOpenedFile状态标记,用于跟踪用户是否已打开或操作过文件
- 修改欢迎界面显示逻辑,仅在无内容且未打开文件时显示
- 调整保存/另存为/新建场景等操作的权限判断
- 更新标题栏显示逻辑,支持已打开文件但无内容的场景
- 在文件拖放、加载等操作时自动标记文件打开状态
---
src/App.vue | 90 +++++++++++++++++++++++++++++------
src/components/CanvasView.vue | 1 +
src/components/TitleBar.vue | 3 +-
3 files changed, 78 insertions(+), 16 deletions(-)
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"
>
- {{ isDirty ? '*' : '' }}{{ title || 'Untitled' }}
+ {{ isDirty ? '*' : '' }}{{ title || 'Untitled' }}