feat(文件管理): 添加已打开文件状态标记及相应逻辑
引入hasOpenedFile状态标记,用于跟踪用户是否已打开或操作过文件 - 修改欢迎界面显示逻辑,仅在无内容且未打开文件时显示 - 调整保存/另存为/新建场景等操作的权限判断 - 更新标题栏显示逻辑,支持已打开文件但无内容的场景 - 在文件拖放、加载等操作时自动标记文件打开状态
This commit is contained in:
84
src/App.vue
84
src/App.vue
@@ -59,6 +59,7 @@ const state = reactive({
|
|||||||
showOpenSceneDialog: false,
|
showOpenSceneDialog: false,
|
||||||
pendingScenePath: null as string | null,
|
pendingScenePath: null as string | null,
|
||||||
pendingAction: null as 'close' | 'openScene' | null,
|
pendingAction: null as 'close' | 'openScene' | null,
|
||||||
|
hasOpenedFile: false, // 标记是否已打开过文件或添加过内容
|
||||||
});
|
});
|
||||||
|
|
||||||
// Computed title
|
// Computed title
|
||||||
@@ -71,6 +72,12 @@ const hasContent = computed(() => {
|
|||||||
return sceneManager.elements.value.length > 0;
|
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
|
// Selection box state
|
||||||
const selectionBox = reactive({
|
const selectionBox = reactive({
|
||||||
visible: false,
|
visible: false,
|
||||||
@@ -125,7 +132,9 @@ function getContextMenuItems(): MenuItem[] {
|
|||||||
items.push({ label: '', action: () => {}, separator: true });
|
items.push({ label: '', action: () => {}, separator: true });
|
||||||
|
|
||||||
// File operations
|
// File operations
|
||||||
const canSave = hasContent.value;
|
// 允许保存:有内容 或 已打开文件(可以保存空文件)
|
||||||
|
const canSave = hasContent.value || state.hasOpenedFile;
|
||||||
|
const canCreateNew = hasContent.value || state.hasOpenedFile;
|
||||||
|
|
||||||
items.push(
|
items.push(
|
||||||
{
|
{
|
||||||
@@ -148,7 +157,7 @@ function getContextMenuItems(): MenuItem[] {
|
|||||||
action: () => handleNew(),
|
action: () => handleNew(),
|
||||||
shortcut: 'Ctrl+N',
|
shortcut: 'Ctrl+N',
|
||||||
checked: false,
|
checked: false,
|
||||||
disabled: !canSave,
|
disabled: !canCreateNew,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -212,6 +221,9 @@ async function handleCanvasInitialized() {
|
|||||||
try {
|
try {
|
||||||
const loaded = await sceneService.loadOnStartup();
|
const loaded = await sceneService.loadOnStartup();
|
||||||
if (loaded) {
|
if (loaded) {
|
||||||
|
// 标记已打开文件
|
||||||
|
state.hasOpenedFile = true;
|
||||||
|
|
||||||
// Sync renderer and view transform after loading
|
// Sync renderer and view transform after loading
|
||||||
canvasViewRef.value?.syncRendererElements();
|
canvasViewRef.value?.syncRendererElements();
|
||||||
canvasViewRef.value?.syncViewTransform();
|
canvasViewRef.value?.syncViewTransform();
|
||||||
@@ -305,6 +317,9 @@ async function handleTauriFileDrop(paths: string[], dropX: number, dropY: number
|
|||||||
// Handle image files
|
// Handle image files
|
||||||
if (imageFiles.length === 0) return;
|
if (imageFiles.length === 0) return;
|
||||||
|
|
||||||
|
// 标记已开始使用场景,防止删除元素后显示欢迎界面
|
||||||
|
state.hasOpenedFile = true;
|
||||||
|
|
||||||
// Record state for undo
|
// Record state for undo
|
||||||
const currentState = sceneManager.getState();
|
const currentState = sceneManager.getState();
|
||||||
historyManager.record(currentState.elements, currentState.viewTransform);
|
historyManager.record(currentState.elements, currentState.viewTransform);
|
||||||
@@ -335,6 +350,9 @@ async function loadSceneFromPath(path: string) {
|
|||||||
try {
|
try {
|
||||||
const success = await sceneService.loadFromPath(path);
|
const success = await sceneService.loadFromPath(path);
|
||||||
if (success) {
|
if (success) {
|
||||||
|
// 标记已打开文件,防止删除元素后显示欢迎界面
|
||||||
|
state.hasOpenedFile = true;
|
||||||
|
|
||||||
canvasViewRef.value?.syncRendererElements();
|
canvasViewRef.value?.syncRendererElements();
|
||||||
canvasViewRef.value?.syncViewTransform();
|
canvasViewRef.value?.syncViewTransform();
|
||||||
canvasViewRef.value?.getRenderer()?.requestRender();
|
canvasViewRef.value?.getRenderer()?.requestRender();
|
||||||
@@ -355,8 +373,12 @@ async function loadSceneFromPath(path: string) {
|
|||||||
async function handleConfirmOpenScene() {
|
async function handleConfirmOpenScene() {
|
||||||
state.showOpenSceneDialog = false;
|
state.showOpenSceneDialog = false;
|
||||||
if (state.pendingScenePath) {
|
if (state.pendingScenePath) {
|
||||||
|
// 从指定路径加载(拖放文件)
|
||||||
await loadSceneFromPath(state.pendingScenePath);
|
await loadSceneFromPath(state.pendingScenePath);
|
||||||
state.pendingScenePath = null;
|
state.pendingScenePath = null;
|
||||||
|
} else {
|
||||||
|
// 通过对话框选择文件(Ctrl+L)
|
||||||
|
await performLoad();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -553,8 +575,8 @@ function handleGlobalAction(action: string, data: unknown) {
|
|||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
async function handleSave() {
|
async function handleSave() {
|
||||||
// Don't save if no content
|
// 允许保存:有内容 或 已打开文件(可以保存空文件)
|
||||||
if (!hasContent.value) return;
|
if (!hasContent.value && !state.hasOpenedFile) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const success = await sceneService.save();
|
const success = await sceneService.save();
|
||||||
@@ -567,8 +589,8 @@ async function handleSave() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleSaveAs() {
|
async function handleSaveAs() {
|
||||||
// Don't save if no content
|
// 允许另存为:有内容 或 已打开文件(可以保存空文件)
|
||||||
if (!hasContent.value) return;
|
if (!hasContent.value && !state.hasOpenedFile) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const success = await sceneService.saveAs();
|
const success = await sceneService.saveAs();
|
||||||
@@ -581,6 +603,29 @@ async function handleSaveAs() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleLoad() {
|
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 {
|
try {
|
||||||
// Record state for undo before loading
|
// Record state for undo before loading
|
||||||
const currentState = sceneManager.getState();
|
const currentState = sceneManager.getState();
|
||||||
@@ -588,6 +633,9 @@ async function handleLoad() {
|
|||||||
|
|
||||||
const success = await sceneService.load();
|
const success = await sceneService.load();
|
||||||
if (success) {
|
if (success) {
|
||||||
|
// 标记已打开文件,防止显示欢迎界面
|
||||||
|
state.hasOpenedFile = true;
|
||||||
|
|
||||||
// Sync renderer and view transform after loading
|
// Sync renderer and view transform after loading
|
||||||
canvasViewRef.value?.syncRendererElements();
|
canvasViewRef.value?.syncRendererElements();
|
||||||
canvasViewRef.value?.syncViewTransform();
|
canvasViewRef.value?.syncViewTransform();
|
||||||
@@ -619,14 +667,18 @@ async function handleBrowseFiles() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleNew() {
|
function handleNew() {
|
||||||
// Don't create new scene if already empty
|
// Don't create new scene if already empty and no file opened
|
||||||
if (!hasContent.value) return;
|
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();
|
const currentState = sceneManager.getState();
|
||||||
historyManager.record(currentState.elements, currentState.viewTransform);
|
historyManager.record(currentState.elements, currentState.viewTransform);
|
||||||
|
}
|
||||||
|
|
||||||
sceneService.newScene();
|
sceneService.newScene();
|
||||||
|
// 重置标志,允许显示欢迎界面
|
||||||
|
state.hasOpenedFile = false;
|
||||||
canvasViewRef.value?.syncRendererElements();
|
canvasViewRef.value?.syncRendererElements();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1005,8 +1057,14 @@ async function executePendingAction() {
|
|||||||
|
|
||||||
if (action === 'close') {
|
if (action === 'close') {
|
||||||
await forceCloseWindow();
|
await forceCloseWindow();
|
||||||
} else if (action === 'openScene' && scenePath) {
|
} else if (action === 'openScene') {
|
||||||
|
if (scenePath) {
|
||||||
|
// 从指定路径加载(拖放文件)
|
||||||
await loadSceneFromPath(scenePath);
|
await loadSceneFromPath(scenePath);
|
||||||
|
} else {
|
||||||
|
// 通过对话框选择文件(Ctrl+L)
|
||||||
|
await performLoad();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1140,6 +1198,7 @@ onUnmounted(() => {
|
|||||||
:always-on-top="state.alwaysOnTop"
|
:always-on-top="state.alwaysOnTop"
|
||||||
:is-dirty="state.isDirty"
|
:is-dirty="state.isDirty"
|
||||||
:has-content="hasContent"
|
:has-content="hasContent"
|
||||||
|
:has-opened-file="state.hasOpenedFile"
|
||||||
@minimize="minimizeWindow"
|
@minimize="minimizeWindow"
|
||||||
@maximize="toggleFullscreen"
|
@maximize="toggleFullscreen"
|
||||||
@close="closeWindow"
|
@close="closeWindow"
|
||||||
@@ -1156,9 +1215,10 @@ onUnmounted(() => {
|
|||||||
@file-drop="handleFileDrop"
|
@file-drop="handleFileDrop"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Welcome Overlay (when scene is empty) -->
|
<!-- Welcome Overlay (when scene is empty and no file opened) -->
|
||||||
<WelcomeOverlay
|
<WelcomeOverlay
|
||||||
v-if="state.isInitialized && !hasContent"
|
v-if="state.isInitialized && showWelcome"
|
||||||
|
:key="'welcome-' + hasContent + '-' + state.hasOpenedFile"
|
||||||
@browse="handleBrowseFiles"
|
@browse="handleBrowseFiles"
|
||||||
@help="state.showHelp = true"
|
@help="state.showHelp = true"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -502,6 +502,7 @@ async function handleDelete() {
|
|||||||
if (sceneManager.getSelectedCount() > 0) {
|
if (sceneManager.getSelectedCount() > 0) {
|
||||||
recordState();
|
recordState();
|
||||||
sceneManager.deleteSelected();
|
sceneManager.deleteSelected();
|
||||||
|
sceneService.markDirty();
|
||||||
await syncRendererElements();
|
await syncRendererElements();
|
||||||
// Force render update
|
// Force render update
|
||||||
renderer?.requestRender();
|
renderer?.requestRender();
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const props = defineProps<{
|
|||||||
alwaysOnTop?: boolean;
|
alwaysOnTop?: boolean;
|
||||||
isDirty?: boolean;
|
isDirty?: boolean;
|
||||||
hasContent?: boolean;
|
hasContent?: boolean;
|
||||||
|
hasOpenedFile?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -133,7 +134,7 @@ onUnmounted(() => {
|
|||||||
@mousedown="handleDragStart"
|
@mousedown="handleDragStart"
|
||||||
>
|
>
|
||||||
<span class="title-bar__title">
|
<span class="title-bar__title">
|
||||||
<template v-if="hasContent">{{ isDirty ? '*' : '' }}{{ title || 'Untitled' }}</template>
|
<template v-if="hasContent || hasOpenedFile">{{ isDirty ? '*' : '' }}{{ title || 'Untitled' }}</template>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user