feat(文件管理): 添加已打开文件状态标记及相应逻辑
引入hasOpenedFile状态标记,用于跟踪用户是否已打开或操作过文件 - 修改欢迎界面显示逻辑,仅在无内容且未打开文件时显示 - 调整保存/另存为/新建场景等操作的权限判断 - 更新标题栏显示逻辑,支持已打开文件但无内容的场景 - 在文件拖放、加载等操作时自动标记文件打开状态
This commit is contained in:
84
src/App.vue
84
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
|
||||
// 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) {
|
||||
} 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"
|
||||
/>
|
||||
|
||||
@@ -502,6 +502,7 @@ async function handleDelete() {
|
||||
if (sceneManager.getSelectedCount() > 0) {
|
||||
recordState();
|
||||
sceneManager.deleteSelected();
|
||||
sceneService.markDirty();
|
||||
await syncRendererElements();
|
||||
// Force render update
|
||||
renderer?.requestRender();
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user