feat(文件管理): 添加已打开文件状态标记及相应逻辑

引入hasOpenedFile状态标记,用于跟踪用户是否已打开或操作过文件
- 修改欢迎界面显示逻辑,仅在无内容且未打开文件时显示
- 调整保存/另存为/新建场景等操作的权限判断
- 更新标题栏显示逻辑,支持已打开文件但无内容的场景
- 在文件拖放、加载等操作时自动标记文件打开状态
This commit is contained in:
2025-12-19 20:46:30 +08:00
parent f5b8a563cb
commit 75c26d479a
3 changed files with 78 additions and 16 deletions

View File

@@ -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"
/>
<!-- Welcome Overlay (when scene is empty) -->
<!-- Welcome Overlay (when scene is empty and no file opened) -->
<WelcomeOverlay
v-if="state.isInitialized && !hasContent"
v-if="state.isInitialized && showWelcome"
:key="'welcome-' + hasContent + '-' + state.hasOpenedFile"
@browse="handleBrowseFiles"
@help="state.showHelp = true"
/>

View File

@@ -502,6 +502,7 @@ async function handleDelete() {
if (sceneManager.getSelectedCount() > 0) {
recordState();
sceneManager.deleteSelected();
sceneService.markDirty();
await syncRendererElements();
// Force render update
renderer?.requestRender();

View File

@@ -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"
>
<span class="title-bar__title">
<template v-if="hasContent">{{ isDirty ? '*' : '' }}{{ title || 'Untitled' }}</template>
<template v-if="hasContent || hasOpenedFile">{{ isDirty ? '*' : '' }}{{ title || 'Untitled' }}</template>
</span>
</div>