This commit is contained in:
Baobhan Sith
2025-04-21 21:39:26 +08:00
parent 0774ba94ab
commit a378ca98f4
4 changed files with 217 additions and 270 deletions
@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, watch, type Ref } from 'vue'; // Re-added computed import { ref, computed, watch, type Ref, nextTick } from 'vue'; // Import nextTick
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useLayoutStore, type LayoutNode, type PaneName } from '../stores/layout.store'; import { useLayoutStore, type LayoutNode, type PaneName } from '../stores/layout.store';
import draggable from 'vuedraggable'; import draggable from 'vuedraggable';
@@ -22,9 +22,14 @@ const layoutStore = useLayoutStore();
// --- State --- // --- State ---
const localLayoutTree: Ref<LayoutNode | null> = ref(null); const localLayoutTree: Ref<LayoutNode | null> = ref(null);
const hasChanges = ref(false); // State for current edits
// const localLayoutTree: Ref<LayoutNode | null> = ref(null); // REMOVE DUPLICATE
const localSidebarPanes: Ref<{ left: PaneName[], right: PaneName[] }> = ref({ left: [], right: [] }); const localSidebarPanes: Ref<{ left: PaneName[], right: PaneName[] }> = ref({ left: [], right: [] });
const localAvailablePanes: Ref<PaneName[]> = ref([]); // New state for available panes const localAvailablePanes: Ref<PaneName[]> = ref([]);
// State for original values to compare against
const originalLayoutTree: Ref<LayoutNode | null> = ref(null);
const originalSidebarPanes: Ref<{ left: PaneName[], right: PaneName[] }> = ref({ left: [], right: [] });
// --- Dialog State --- // --- Dialog State ---
const dialogRef = ref<HTMLElement | null>(null); const dialogRef = ref<HTMLElement | null>(null);
@@ -32,80 +37,44 @@ const dialogRef = ref<HTMLElement | null>(null);
// --- Watchers --- // --- Watchers ---
watch(() => props.isVisible, (newValue) => { watch(() => props.isVisible, (newValue) => {
if (newValue) { if (newValue) {
// Load main layout // --- Load initial data and create original copies ---
if (layoutStore.layoutTree) { // Main layout
localLayoutTree.value = JSON.parse(JSON.stringify(layoutStore.layoutTree)); const initialLayout = layoutStore.layoutTree ? JSON.parse(JSON.stringify(layoutStore.layoutTree)) : null;
} else { localLayoutTree.value = initialLayout;
localLayoutTree.value = null; // Ensure it's null if store is null originalLayoutTree.value = JSON.parse(JSON.stringify(initialLayout)); // Deep copy for original
}
// Load sidebar config
if (layoutStore.sidebarPanes) {
localSidebarPanes.value = JSON.parse(JSON.stringify(layoutStore.sidebarPanes));
} else {
localSidebarPanes.value = { left: [], right: [] }; // Default
}
// Initialize available panes: Always include non-terminal panes, include terminal only if not already used.
const initialUsed = getAllLocalUsedPaneNames(localLayoutTree.value, localSidebarPanes.value);
const nonTerminalPanes = layoutStore.allPossiblePanes.filter(p => p !== 'terminal');
const available = [...nonTerminalPanes]; // Start with all non-terminal panes
if (!initialUsed.has('terminal')) {
// Add terminal only if it's not used in the current layout
// Try to insert it at its original position for consistency
const terminalOriginalIndex = layoutStore.allPossiblePanes.indexOf('terminal');
let inserted = false;
for (let i = 0; i < available.length; i++) {
const currentPane = available[i];
const currentOriginalIndex = layoutStore.allPossiblePanes.indexOf(currentPane);
if (terminalOriginalIndex < currentOriginalIndex) {
available.splice(i, 0, 'terminal');
inserted = true;
break;
}
}
if (!inserted) {
available.push('terminal'); // Add to end if needed
}
}
localAvailablePanes.value = available;
hasChanges.value = false; // Reset changes flag on open // Sidebar config
console.log('[LayoutConfigurator] Dialog opened, initialized available panes (non-terminals always present).'); const initialSidebars = layoutStore.sidebarPanes ? JSON.parse(JSON.stringify(layoutStore.sidebarPanes)) : { left: [], right: [] };
localSidebarPanes.value = initialSidebars;
originalSidebarPanes.value = JSON.parse(JSON.stringify(initialSidebars)); // Deep copy for original
// Initialize available panes: Include 'terminal' only if it's not already used.
const initialUsed = getAllLocalUsedPaneNames(localLayoutTree.value, localSidebarPanes.value);
if (initialUsed.has('terminal')) {
// Terminal is used, available list excludes it
localAvailablePanes.value = layoutStore.allPossiblePanes.filter(p => p !== 'terminal');
} else { } else {
localLayoutTree.value = null; // Clear main layout // Terminal is not used, available list includes all
localSidebarPanes.value = { left: [], right: [] }; // Clear sidebars localAvailablePanes.value = [...layoutStore.allPossiblePanes];
localAvailablePanes.value = []; // Clear available panes }
console.log('[LayoutConfigurator] Dialog closed.'); // Ensure original order is maintained
localAvailablePanes.value.sort((a, b) =>
layoutStore.allPossiblePanes.indexOf(a) - layoutStore.allPossiblePanes.indexOf(b)
);
console.log('[LayoutConfigurator] Dialog opened, initial data loaded and original copies created.');
} else {
// --- Clear all state on close ---
localLayoutTree.value = null;
originalLayoutTree.value = null;
localSidebarPanes.value = { left: [], right: [] };
originalSidebarPanes.value = { left: [], right: [] };
localAvailablePanes.value = [];
console.log('[LayoutConfigurator] Dialog closed, state cleared.');
} }
}); });
// Watch for changes in the main layout tree
watch(localLayoutTree, (newValue, oldValue) => {
// Check if it's not the initial load and the dialog is visible
if (oldValue !== undefined && oldValue !== null && props.isVisible) {
// Use stringify for a simple deep comparison
if (JSON.stringify(newValue) !== JSON.stringify(oldValue)) {
console.log('[LayoutConfigurator] Main layout tree changed.');
hasChanges.value = true;
}
}
}, { deep: true });
// Watch for changes in the sidebar configuration
watch(localSidebarPanes, (newValue, oldValue) => {
// Check if it's not the initial load and the dialog is visible
if (oldValue !== undefined && props.isVisible) {
const newJson = JSON.stringify(newValue);
const oldJson = JSON.stringify(oldValue);
console.log('[LayoutConfigurator Watcher] localSidebarPanes changed.');
// Use stringify for a simple deep comparison, including order changes
if (newJson !== oldJson) {
console.log('[LayoutConfigurator Watcher] Sidebar panes changed, setting hasChanges.');
hasChanges.value = true;
}
}
}, { deep: true });
// --- Helper Functions --- // --- Helper Functions ---
function getMainLayoutUsedPaneNames(node: LayoutNode | null): Set<PaneName> { function getMainLayoutUsedPaneNames(node: LayoutNode | null): Set<PaneName> {
const usedNames = new Set<PaneName>(); const usedNames = new Set<PaneName>();
@@ -128,32 +97,56 @@ function getAllLocalUsedPaneNames(mainNode: LayoutNode | null, sidebars: { left:
return usedNames; return usedNames;
} }
// Helper to add pane back to available list if not present // --- Restore Helper Functions for Terminal ---
// Helper to add 'terminal' back to available list if not present
function addPaneToAvailableList(paneName: PaneName) { function addPaneToAvailableList(paneName: PaneName) {
if (!localAvailablePanes.value.includes(paneName) && layoutStore.allPossiblePanes.includes(paneName)) { // Only act if the pane is 'terminal' and it's not already available
// Maintain original order if possible, otherwise just add if (paneName === 'terminal' && !localAvailablePanes.value.includes('terminal')) {
// Find the original index in allPossiblePanes // Maintain original order if possible
const originalIndex = layoutStore.allPossiblePanes.indexOf(paneName); const originalIndex = layoutStore.allPossiblePanes.indexOf('terminal');
let inserted = false; let inserted = false;
// Try to insert based on original order relative to existing available panes
for (let i = 0; i < localAvailablePanes.value.length; i++) { for (let i = 0; i < localAvailablePanes.value.length; i++) {
const currentAvailablePane = localAvailablePanes.value[i]; const currentAvailablePane = localAvailablePanes.value[i];
const currentOriginalIndex = layoutStore.allPossiblePanes.indexOf(currentAvailablePane); const currentOriginalIndex = layoutStore.allPossiblePanes.indexOf(currentAvailablePane);
if (originalIndex < currentOriginalIndex) { if (originalIndex < currentOriginalIndex) {
localAvailablePanes.value.splice(i, 0, paneName); localAvailablePanes.value.splice(i, 0, 'terminal');
inserted = true; inserted = true;
break; break;
} }
} }
if (!inserted) { if (!inserted) {
localAvailablePanes.value.push(paneName); // Add to end if no suitable spot found localAvailablePanes.value.push('terminal'); // Add to end if no suitable spot found
} }
console.log(`[LayoutConfigurator] Added '${paneName}' back to available panes.`); console.log(`[LayoutConfigurator] Added 'terminal' back to available panes.`);
} }
} }
// Helper to remove 'terminal' from available list
function removePaneFromAvailableList(paneName: PaneName) {
if (paneName === 'terminal') {
const index = localAvailablePanes.value.indexOf('terminal');
if (index > -1) {
localAvailablePanes.value.splice(index, 1);
console.log(`[LayoutConfigurator] Removed 'terminal' from available panes.`);
}
}
}
// --- Computed --- // --- Computed ---
// Panel Labels for display // Panel Labels for display
// Real-time comparison to determine if changes exist
const isModified = computed(() => {
// Compare current local state with the original snapshot
const currentLayoutJson = JSON.stringify(localLayoutTree.value);
const originalLayoutJson = JSON.stringify(originalLayoutTree.value);
const currentSidebarJson = JSON.stringify(localSidebarPanes.value);
const originalSidebarJson = JSON.stringify(originalSidebarPanes.value);
// Return true if either layout or sidebars differ from the original
const modified = currentLayoutJson !== originalLayoutJson || currentSidebarJson !== originalSidebarJson;
// console.log(`[LayoutConfigurator] isModified computed: ${modified}`); // Debug log
return modified;
});
const paneLabels = computed(() => ({ // Assuming labels might depend on i18n const paneLabels = computed(() => ({ // Assuming labels might depend on i18n
connections: t('layout.pane.connections', '连接列表'), connections: t('layout.pane.connections', '连接列表'),
terminal: t('layout.pane.terminal', '终端'), terminal: t('layout.pane.terminal', '终端'),
@@ -168,7 +161,8 @@ const paneLabels = computed(() => ({ // Assuming labels might depend on i18n
// --- Methods --- // --- Methods ---
const closeDialog = () => { const closeDialog = () => {
if (hasChanges.value) { // Use the computed property for the check
if (isModified.value) {
if (confirm(t('layoutConfigurator.confirmClose', '有未保存的更改,确定要关闭吗?'))) { if (confirm(t('layoutConfigurator.confirmClose', '有未保存的更改,确定要关闭吗?'))) {
emit('close'); emit('close');
} }
@@ -177,25 +171,28 @@ const closeDialog = () => {
} }
}; };
const saveLayout = () => { const saveLayout = async () => { // Make async
// Save main layout console.log('[LayoutConfigurator] Attempting to save layout...');
if (localLayoutTree.value) { try {
layoutStore.updateLayoutTree(localLayoutTree.value); // Save main layout and wait for persistence
console.log('[LayoutConfigurator] Main layout saved to Store.'); console.log('[LayoutConfigurator] Updating main layout tree in store...');
} else { await layoutStore.updateLayoutTree(localLayoutTree.value); // Await the async action
// Handle potentially empty layout based on store logic console.log('[LayoutConfigurator] Main layout tree update awaited.');
layoutStore.updateLayoutTree(null); // Assuming null is valid for empty
console.log('[LayoutConfigurator] Main layout is empty, saved null to Store.');
}
// Save sidebar config // Save sidebar config and wait for persistence
const sidebarConfigToSave = JSON.parse(JSON.stringify(localSidebarPanes.value)); const sidebarConfigToSave = JSON.parse(JSON.stringify(localSidebarPanes.value));
console.log('[LayoutConfigurator] Preparing to save sidebar config:', sidebarConfigToSave); // Log before sending console.log('[LayoutConfigurator] Updating sidebar panes in store:', sidebarConfigToSave);
layoutStore.updateSidebarPanes(sidebarConfigToSave); await layoutStore.updateSidebarPanes(sidebarConfigToSave); // Await the async action
console.log('[LayoutConfigurator] Sidebar config sent to Store.'); console.log('[LayoutConfigurator] Sidebar panes update awaited.');
hasChanges.value = false; // isModified will update automatically based on comparison with original state after save
emit('close'); emit('close'); // Close dialog *after* save is complete
console.log('[LayoutConfigurator] Layout saved successfully, dialog closed.');
} catch (error) {
console.error('[LayoutConfigurator] Error saving layout:', error);
// Optionally notify the user about the error
alert(t('layoutConfigurator.saveError', '保存布局时出错,请稍后再试。'));
}
}; };
const resetToDefault = () => { const resetToDefault = () => {
@@ -208,30 +205,20 @@ const resetToDefault = () => {
const defaultSidebarPanes = layoutStore.getSystemDefaultSidebarPanes(); const defaultSidebarPanes = layoutStore.getSystemDefaultSidebarPanes();
localSidebarPanes.value = JSON.parse(JSON.stringify(defaultSidebarPanes)); localSidebarPanes.value = JSON.parse(JSON.stringify(defaultSidebarPanes));
// Reset available panes using the new logic // Reset available panes: Include 'terminal' only if it's not used in the default layout.
const defaultUsed = getAllLocalUsedPaneNames(localLayoutTree.value, localSidebarPanes.value); const defaultUsed = getAllLocalUsedPaneNames(localLayoutTree.value, localSidebarPanes.value);
const nonTerminalPanesDefault = layoutStore.allPossiblePanes.filter(p => p !== 'terminal'); if (defaultUsed.has('terminal')) {
const availableDefault = [...nonTerminalPanesDefault]; localAvailablePanes.value = layoutStore.allPossiblePanes.filter(p => p !== 'terminal');
if (!defaultUsed.has('terminal')) { } else {
const terminalOriginalIndex = layoutStore.allPossiblePanes.indexOf('terminal'); localAvailablePanes.value = [...layoutStore.allPossiblePanes];
let inserted = false;
for (let i = 0; i < availableDefault.length; i++) {
const currentPane = availableDefault[i];
const currentOriginalIndex = layoutStore.allPossiblePanes.indexOf(currentPane);
if (terminalOriginalIndex < currentOriginalIndex) {
availableDefault.splice(i, 0, 'terminal');
inserted = true;
break;
} }
} // Ensure original order
if (!inserted) { localAvailablePanes.value.sort((a, b) =>
availableDefault.push('terminal'); layoutStore.allPossiblePanes.indexOf(a) - layoutStore.allPossiblePanes.indexOf(b)
} );
}
localAvailablePanes.value = availableDefault;
console.log('[LayoutConfigurator] Reset to default layout, sidebar panes, and available panes (non-terminals always present).'); console.log('[LayoutConfigurator] Reset to default layout, sidebar panes, and available panes.');
hasChanges.value = true; // Mark as changed after reset // isModified computed property will detect the change automatically by comparing with original state
} }
}; };
@@ -251,8 +238,8 @@ const handleNodeUpdate = (updatedNode: LayoutNode) => {
console.log('[LayoutConfigurator] Received node update from editor:', updatedNode); console.log('[LayoutConfigurator] Received node update from editor:', updatedNode);
// Assuming the update is for the root node for simplicity // Assuming the update is for the root node for simplicity
// v-model on LayoutNodeEditor might handle this, but explicit update is safer // v-model on LayoutNodeEditor might handle this, but explicit update is safer
// Update the local tree; isModified will react automatically
localLayoutTree.value = updatedNode; localLayoutTree.value = updatedNode;
// No need to set hasChanges here, the watcher on localLayoutTree handles it
}; };
// Handle remove requests from LayoutNodeEditor (for main layout) - CORRECTED VERSION // Handle remove requests from LayoutNodeEditor (for main layout) - CORRECTED VERSION
@@ -265,24 +252,11 @@ function findAndRemoveNode(node: LayoutNode | null, parentNodeId: string | undef
const removedNode = updatedChildren.splice(nodeIndex, 1)[0]; // Remove and get the node const removedNode = updatedChildren.splice(nodeIndex, 1)[0]; // Remove and get the node
console.log(`[LayoutConfigurator] Removing node at index ${nodeIndex} from parent ${parentNodeId}`); console.log(`[LayoutConfigurator] Removing node at index ${nodeIndex} from parent ${parentNodeId}`);
// Add the pane back to available list if it was a pane node // If the removed node was the terminal pane, add it back to available list
if (removedNode.type === 'pane' && removedNode.component) { if (removedNode.type === 'pane' && removedNode.component === 'terminal') {
addPaneToAvailableList(removedNode.component); addPaneToAvailableList('terminal');
}
// If the removed node was a container, recursively add its children back
else if (removedNode.type === 'container') { // Check type directly
function addPanesFromContainer(containerNode: LayoutNode | null) { // Accept null
if (!containerNode || !containerNode.children) return; // Guard against null/undefined children
containerNode.children.forEach(child => {
if (child.type === 'pane' && child.component) {
addPaneToAvailableList(child.component);
} else if (child.type === 'container') {
addPanesFromContainer(child); // Recurse into nested containers
}
});
}
addPanesFromContainer(removedNode);
} }
// No need to handle containers specifically for adding back, only terminal matters.
return { ...node, children: updatedChildren }; return { ...node, children: updatedChildren };
} }
@@ -304,15 +278,16 @@ const handleNodeRemove = (payload: { parentNodeId: string | undefined; nodeIndex
console.log('[LayoutConfigurator] Received node remove request:', payload); console.log('[LayoutConfigurator] Received node remove request:', payload);
if (payload.parentNodeId === undefined && payload.nodeIndex === 0) { if (payload.parentNodeId === undefined && payload.nodeIndex === 0) {
if (confirm(t('layoutConfigurator.confirmClearLayout', '确定要清空整个布局吗?所有面板将返回可用列表。'))) { if (confirm(t('layoutConfigurator.confirmClearLayout', '确定要清空整个布局吗?所有面板将返回可用列表。'))) {
// Add all panes from the tree back to available list before clearing // Add all panes from the tree back to available list before clearing - REMOVED, no longer needed
const usedInTree = getMainLayoutUsedPaneNames(localLayoutTree.value); // Single declaration // const usedInTree = getMainLayoutUsedPaneNames(localLayoutTree.value);
usedInTree.forEach(paneName => addPaneToAvailableList(paneName)); // Correctly call the helper // usedInTree.forEach(paneName => addPaneToAvailableList(paneName));
// Clear the tree // Clear the tree
// Update the local tree; isModified will react automatically
localLayoutTree.value = null; localLayoutTree.value = null;
} }
} else if (payload.parentNodeId) { } else if (payload.parentNodeId) {
// Update the local tree; isModified will react automatically
localLayoutTree.value = findAndRemoveNode(localLayoutTree.value, payload.parentNodeId, payload.nodeIndex); localLayoutTree.value = findAndRemoveNode(localLayoutTree.value, payload.parentNodeId, payload.nodeIndex);
// Watcher on localLayoutTree handles hasChanges
} else { } else {
console.warn('[LayoutConfigurator] Invalid remove payload:', payload); console.warn('[LayoutConfigurator] Invalid remove payload:', payload);
} }
@@ -323,10 +298,12 @@ const removeSidebarPane = (side: 'left' | 'right', index: number) => {
const removedPane = localSidebarPanes.value[side].splice(index, 1)[0]; // Remove and get pane name const removedPane = localSidebarPanes.value[side].splice(index, 1)[0]; // Remove and get pane name
if (removedPane) { if (removedPane) {
console.log(`[LayoutConfigurator] Removed pane '${removedPane}' from ${side} sidebar at index ${index}.`); console.log(`[LayoutConfigurator] Removed pane '${removedPane}' from ${side} sidebar at index ${index}.`);
addPaneToAvailableList(removedPane); // Correctly call the helper // If the removed pane was 'terminal', add it back to available list
if (removedPane === 'terminal') {
addPaneToAvailableList('terminal');
} }
// Explicitly set hasChanges flag (watcher might not catch splice reliably?) }
hasChanges.value = true; // isModified will react automatically
}; };
// Handler for vuedraggable end event to ensure changes flag is set and handle added items // Handler for vuedraggable end event to ensure changes flag is set and handle added items
@@ -351,44 +328,21 @@ const onDraggableChange = (event: any, side: 'left' | 'right') => { // Add side
console.log(`[LayoutConfigurator] Item moved or removed within/from ${side} sidebar.`); console.log(`[LayoutConfigurator] Item moved or removed within/from ${side} sidebar.`);
} }
// Ensure changes flag is set for any modification (add, remove, move) // isModified will react automatically
hasChanges.value = true;
}; };
// Handle drag end from the available panes list // Handle drag end from the available panes list
const handleAvailablePaneDragEnd = (event: any) => { const handleAvailablePaneDragEnd = (event: any) => {
// Check if the item was dropped into a different list (main layout or sidebars) // Check if the item was dropped into a different list
if (event.to !== event.from) { if (event.to !== event.from) {
// Find the component (Draggable) associated with the source item element const paneName = event.oldIndex !== undefined ? localAvailablePanes.value[event.oldIndex] : null;
// This might rely on internal structure, adjust if needed or find a better way
const draggedItemElement = event.item; // The original element in the source list
let paneName: PaneName | null = null;
// Attempt to get data via Vue's internal context (might be unstable)
// Note: __draggable_component__ might not be reliable across versions. Consider data attributes if this fails.
if ((draggedItemElement as any)?.__draggable_component__?.context?.element) {
paneName = (draggedItemElement as any).__draggable_component__.context.element as PaneName;
} else {
// Fallback: Try getting from the data array using oldIndex if context fails
if (event.oldIndex !== undefined && localAvailablePanes.value[event.oldIndex]) {
paneName = localAvailablePanes.value[event.oldIndex];
console.warn("[LayoutConfigurator] Using index fallback to get pane name in drag end.");
}
}
// If 'terminal' was dragged out, remove it from the available list
if (paneName === 'terminal') { if (paneName === 'terminal') {
console.log('[LayoutConfigurator] "terminal" pane dropped elsewhere. Removing from available list.'); removePaneFromAvailableList('terminal');
// Find the precise index in the *current* state of localAvailablePanes, as it might have shifted
const currentIndex = localAvailablePanes.value.indexOf('terminal');
if (currentIndex > -1) {
localAvailablePanes.value.splice(currentIndex, 1);
} else {
console.warn('[LayoutConfigurator] Could not find "terminal" in available list to remove after drag.');
}
} else if (paneName) { } else if (paneName) {
console.log(`[LayoutConfigurator] Non-terminal pane "${paneName}" dropped elsewhere. Kept in available list (clone).`); console.log(`[LayoutConfigurator] Non-terminal pane "${paneName}" dropped elsewhere (clone).`);
// Do nothing, item remains in localAvailablePanes // Other panes are clones, do nothing to the available list
} else { } else {
console.error('[LayoutConfigurator] Could not determine dragged pane name in handleAvailablePaneDragEnd.'); console.error('[LayoutConfigurator] Could not determine dragged pane name in handleAvailablePaneDragEnd.');
} }
@@ -404,7 +358,7 @@ const handleAvailablePaneDragEnd = (event: any) => {
<div ref="dialogRef" class="layout-configurator-dialog"> <div ref="dialogRef" class="layout-configurator-dialog">
<header class="dialog-header"> <header class="dialog-header">
<h2>{{ t('layoutConfigurator.title', '配置工作区布局') }}</h2> <h2>{{ t('layoutConfigurator.title', '布局管理器') }}</h2>
<button class="close-button" @click="closeDialog" :title="t('common.close', '关闭')">&times;</button> <button class="close-button" @click="closeDialog" :title="t('common.close', '关闭')">&times;</button>
</header> </header>
@@ -526,8 +480,8 @@ const handleAvailablePaneDragEnd = (event: any) => {
<footer class="dialog-footer"> <footer class="dialog-footer">
<button @click="closeDialog" class="button-secondary">{{ t('common.cancel', '取消') }}</button> <button @click="closeDialog" class="button-secondary">{{ t('common.cancel', '取消') }}</button>
<button @click="saveLayout" class="button-primary" :disabled="!hasChanges"> <button @click="saveLayout" class="button-primary" :disabled="!isModified">
{{ t('common.save', '保存') }} {{ hasChanges ? '*' : '' }} {{ t('common.save', '保存') }}{{ isModified ? '*' : '' }}
</button> </button>
</footer> </footer>
</div> </div>
@@ -546,9 +546,9 @@ const getIconClasses = (paneName: PaneName): string[] => {
<div :class="['sidebar-panel', 'left-sidebar-panel', { active: !!activeLeftSidebarPane }]"> <div :class="['sidebar-panel', 'left-sidebar-panel', { active: !!activeLeftSidebarPane }]">
<button class="close-sidebar-btn" @click="closeSidebars" title="Close Sidebar">&times;</button> <button class="close-sidebar-btn" @click="closeSidebars" title="Close Sidebar">&times;</button>
<component <component
v-if="currentLeftSidebarComponent && (activeLeftSidebarPane !== 'fileManager' || activeSession)" v-if="currentLeftSidebarComponent && (!['fileManager', 'statusMonitor'].includes(activeLeftSidebarPane) || activeSession)"
:is="currentLeftSidebarComponent" :is="currentLeftSidebarComponent"
:key="`left-panel-${activeLeftSidebarPane}`" :key="`left-panel-${activeLeftSidebarPane ?? 'null'}`"
v-bind="sidebarProps(activeLeftSidebarPane)" v-bind="sidebarProps(activeLeftSidebarPane)"
/> />
<!-- Placeholder if FileManager is selected but no active session --> <!-- Placeholder if FileManager is selected but no active session -->
@@ -559,16 +559,23 @@ const getIconClasses = (paneName: PaneName): string[] => {
<div class="empty-session-tip">文件管理器需要活动会话</div> <div class="empty-session-tip">文件管理器需要活动会话</div>
</div> </div>
</div> </div>
<!-- Placeholder if StatusMonitor is selected but no active session -->
<div v-else-if="activeLeftSidebarPane === 'statusMonitor' && !activeSession" class="sidebar-pane-content pane-placeholder empty-session">
<div class="empty-session-content">
<i class="fas fa-plug"></i>
<span>无活动会话</span>
<div class="empty-session-tip">状态监视器需要活动会话</div>
</div>
</div>
</div> </div>
<!-- Right Sidebar Panel --> <!-- Right Sidebar Panel -->
<div :class="['sidebar-panel', 'right-sidebar-panel', { active: !!activeRightSidebarPane }]"> <div :class="['sidebar-panel', 'right-sidebar-panel', { active: !!activeRightSidebarPane }]">
<button class="close-sidebar-btn" @click="closeSidebars" title="Close Sidebar">&times;</button> <button class="close-sidebar-btn" @click="closeSidebars" title="Close Sidebar">&times;</button>
<component <component
v-if="currentRightSidebarComponent && (activeRightSidebarPane !== 'fileManager' || activeSession)" v-if="currentRightSidebarComponent && (!['fileManager', 'statusMonitor'].includes(activeRightSidebarPane) || activeSession)"
:is="currentRightSidebarComponent" :is="currentRightSidebarComponent"
:key="`right-panel-${activeRightSidebarPane}`" :key="`right-panel-${activeRightSidebarPane ?? 'null'}`"
v-bind="sidebarProps(activeRightSidebarPane)"
/> />
<!-- Placeholder if FileManager is selected but no active session --> <!-- Placeholder if FileManager is selected but no active session -->
<div v-else-if="activeRightSidebarPane === 'fileManager' && !activeSession" class="sidebar-pane-content pane-placeholder empty-session"> <div v-else-if="activeRightSidebarPane === 'fileManager' && !activeSession" class="sidebar-pane-content pane-placeholder empty-session">
@@ -578,6 +585,14 @@ const getIconClasses = (paneName: PaneName): string[] => {
<div class="empty-session-tip">文件管理器需要活动会话</div> <div class="empty-session-tip">文件管理器需要活动会话</div>
</div> </div>
</div> </div>
<!-- Placeholder if StatusMonitor is selected but no active session -->
<div v-else-if="activeRightSidebarPane === 'statusMonitor' && !activeSession" class="sidebar-pane-content pane-placeholder empty-session">
<div class="empty-session-content">
<i class="fas fa-plug"></i>
<span>无活动会话</span>
<div class="empty-session-tip">状态监视器需要活动会话</div>
</div>
</div>
</div> </div>
<!-- Right Sidebar Buttons (Only render if root) --> <!-- Right Sidebar Buttons (Only render if root) -->
+1 -1
View File
@@ -675,7 +675,7 @@
"remove": "移除" "remove": "移除"
}, },
"layoutConfigurator": { "layoutConfigurator": {
"title": "配置工作区布局", "title": "布局管理器",
"availablePanes": "可用面板", "availablePanes": "可用面板",
"layoutPreview": "主布局预览(拖拽到此处)", "layoutPreview": "主布局预览(拖拽到此处)",
"resetDefault": "恢复默认", "resetDefault": "恢复默认",
+53 -75
View File
@@ -257,35 +257,54 @@ export const useLayoutStore = defineStore('layout', () => {
} }
} }
// --- Helper for debounced persistence ---
// We still might want debounce if updates happen rapidly outside the configurator (e.g., pane resize)
let persistLayoutDebounceTimer: ReturnType<typeof setTimeout> | null = null;
const debouncedPersistLayout = () => {
if (persistLayoutDebounceTimer) clearTimeout(persistLayoutDebounceTimer);
persistLayoutDebounceTimer = setTimeout(async () => { // Make async
await persistLayoutTree(); // Await the async persist function
}, 1000);
};
// 更新整个布局树(通常由配置器保存时调用) // 更新整个布局树(通常由配置器保存时调用)
function updateLayoutTree(newTree: LayoutNode | null) { // <-- Allow null async function updateLayoutTree(newTree: LayoutNode | null) { // Make async
// 可选:添加验证逻辑 (如果 newTree 不是 null) // 可选:添加验证逻辑
if (newTree) { if (newTree) {
// TODO: Add validation for LayoutNode structure if needed // TODO: Add validation
} }
layoutTree.value = newTree; // Assign null or the new tree // Check if the tree actually changed before updating and persisting
if (JSON.stringify(newTree) !== JSON.stringify(layoutTree.value)) {
layoutTree.value = newTree;
console.log('[Layout Store] 布局树已更新。 New tree:', newTree); console.log('[Layout Store] 布局树已更新。 New tree:', newTree);
// 保存将在 watch 中自动触发 // --- Directly call persist ---
await persistLayoutTree(); // Await persistence directly
} else {
console.log('[Layout Store] updateLayoutTree called but tree is unchanged.');
}
} }
// 新增:更新侧栏配置 // 新增:更新侧栏配置
function updateSidebarPanes(newPanes: { left: PaneName[], right: PaneName[] }) { async function updateSidebarPanes(newPanes: { left: PaneName[], right: PaneName[] }) { // Make async
// --- Add Validation --- // --- Add Validation ---
if (newPanes && if (newPanes &&
isValidPaneNameArray(newPanes.left, allPossiblePanes.value) && isValidPaneNameArray(newPanes.left, allPossiblePanes.value) &&
isValidPaneNameArray(newPanes.right, allPossiblePanes.value)) isValidPaneNameArray(newPanes.right, allPossiblePanes.value))
{ {
// Check if panes actually changed
if (JSON.stringify(newPanes) !== JSON.stringify(sidebarPanes.value)) {
sidebarPanes.value = newPanes as { left: PaneName[], right: PaneName[] }; // Assign validated data sidebarPanes.value = newPanes as { left: PaneName[], right: PaneName[] }; // Assign validated data
// Log the value immediately after update
console.log('[Layout Store] 侧栏配置已通过验证并更新。 New sidebarPanes value:', JSON.parse(JSON.stringify(sidebarPanes.value))); console.log('[Layout Store] 侧栏配置已通过验证并更新。 New sidebarPanes value:', JSON.parse(JSON.stringify(sidebarPanes.value)));
// 保存将在 watch 中自动触发 // --- Directly call persist ---
await persistSidebarPanes(); // Await persistence directly
} else {
console.log('[Layout Store] updateSidebarPanes called but panes are unchanged.');
}
} else { } else {
console.error('[Layout Store] updateSidebarPanes 接收到无效的侧栏配置数据,未更新状态:', newPanes); console.error('[Layout Store] updateSidebarPanes 接收到无效的侧栏配置数据,未更新状态:', newPanes);
// 可选:抛出错误或通知用户 // 可选:抛出错误或通知用户
} }
} }
// 递归查找并更新节点大小 // 递归查找并更新节点大小
function findAndUpdateNodeSize(node: LayoutNode | null, nodeId: string, childrenSizes: { index: number; size: number }[]): LayoutNode | null { function findAndUpdateNodeSize(node: LayoutNode | null, nodeId: string, childrenSizes: { index: number; size: number }[]): LayoutNode | null {
if (!node) return null; if (!node) return null;
@@ -313,18 +332,18 @@ export const useLayoutStore = defineStore('layout', () => {
// 新增 Action: 更新特定容器节点的子节点大小 // 新增 Action: 更新特定容器节点的子节点大小
function updateNodeSizes(nodeId: string, childrenSizes: { index: number; size: number }[]) { function updateNodeSizes(nodeId: string, childrenSizes: { index: number; size: number }[]) {
console.log(`[Layout Store] 请求更新节点 ${nodeId} 的子节点大小:`, childrenSizes); console.log(`[Layout Store] 请求更新节点 ${nodeId} 的子节点大小:`, childrenSizes);
const originalJson = JSON.stringify(layoutTree.value); // Store original state
const updatedTree = findAndUpdateNodeSize(layoutTree.value, nodeId, childrenSizes); const updatedTree = findAndUpdateNodeSize(layoutTree.value, nodeId, childrenSizes);
if (updatedTree && updatedTree !== layoutTree.value) {
// 只有在树实际发生变化时才更新 ref 以触发 watch
layoutTree.value = updatedTree;
console.log(`[Layout Store] 节点 ${nodeId} 的子节点大小已更新。`);
} else if (updatedTree === layoutTree.value) {
console.log(`[Layout Store] 未找到节点 ${nodeId} 或大小未改变。`);
} else {
console.error(`[Layout Store] 更新节点 ${nodeId} 大小后得到无效的树结构。`);
}
}
if (updatedTree && JSON.stringify(updatedTree) !== originalJson) { // Compare with original JSON
layoutTree.value = updatedTree;
console.log(`[Layout Store] 节点 ${nodeId} 的子节点大小已更新,触发防抖保存。`);
// --- Use debounced persist for resize ---
debouncedPersistLayout();
} else {
console.log(`[Layout Store] 未找到节点 ${nodeId} 或大小未改变。`);
}
}
// 新增 Action: 切换布局(Header/Footer)的可见性 // 新增 Action: 切换布局(Header/Footer)的可见性
function toggleLayoutVisibility() { function toggleLayoutVisibility() {
isLayoutVisible.value = !isLayoutVisible.value; isLayoutVisible.value = !isLayoutVisible.value;
@@ -383,27 +402,19 @@ export const useLayoutStore = defineStore('layout', () => {
} }
// 新增 Action: 将当前主布局树持久化到后端和 localStorage // 新增 Action: 将当前主布局树持久化到后端和 localStorage
async function persistLayoutTree() { async function persistLayoutTree() { // Make async
if (!layoutTree.value) { // ... (existing try/catch logic for backend and localStorage) ...
console.warn('[Layout Store] persistLayoutTree: layoutTree is null, cannot persist.'); // Ensure apiClient calls are awaited if they return promises
// TODO: 考虑是否需要删除后端设置或发送空布局
// await apiClient.put('/settings/layout', null); // 发送 null 或空对象
localStorage.removeItem(LAYOUT_STORAGE_KEY);
return;
}
const layoutToSave = JSON.stringify(layoutTree.value);
// 1. 保存到后端
try { try {
console.log('[Layout Store] Attempting to save main layout to backend...'); console.log('[Layout Store] Attempting to save main layout to backend...');
// Send the layoutTree value directly (which can be null) await apiClient.put('/settings/layout', layoutTree.value); // await
await apiClient.put('/settings/layout', layoutTree.value);
console.log('[Layout Store] 主布局已成功保存到后端 (sent value):', layoutTree.value); console.log('[Layout Store] 主布局已成功保存到后端 (sent value):', layoutTree.value);
} catch (error) { } catch (error) {
console.error('[Layout Store] 保存主布局到后端失败:', error); console.error('[Layout Store] 保存主布局到后端失败:', error);
} }
// 2. 保存到 localStorage // localStorage is synchronous
try { try {
// If layoutTree.value is null, layoutToSave will be 'null' const layoutToSave = JSON.stringify(layoutTree.value);
localStorage.setItem(LAYOUT_STORAGE_KEY, layoutToSave); localStorage.setItem(LAYOUT_STORAGE_KEY, layoutToSave);
console.log('[Layout Store] 主布局已自动保存到 localStorage (saved value):', layoutToSave); console.log('[Layout Store] 主布局已自动保存到 localStorage (saved value):', layoutToSave);
} catch (error) { } catch (error) {
@@ -412,18 +423,18 @@ export const useLayoutStore = defineStore('layout', () => {
} }
// 新增 Action: 将当前侧栏配置持久化到后端和 localStorage // 新增 Action: 将当前侧栏配置持久化到后端和 localStorage
async function persistSidebarPanes() { async function persistSidebarPanes() { // Make async
const sidebarsToSave = JSON.stringify(sidebarPanes.value); // ... (existing try/catch logic for backend and localStorage) ...
// 1. 保存到后端 (假设 API 端点为 /settings/sidebar)
try { try {
console.log('[Layout Store] Attempting to save sidebar config to backend...'); console.log('[Layout Store] Attempting to save sidebar config to backend...');
await apiClient.put('/settings/sidebar', sidebarPanes.value); // 新 API 端点 await apiClient.put('/settings/sidebar', sidebarPanes.value); // await
console.log('[Layout Store] 侧栏配置已成功保存到后端。'); console.log('[Layout Store] 侧栏配置已成功保存到后端。');
} catch (error) { } catch (error) {
console.error('[Layout Store] 保存侧栏配置到后端失败:', error); console.error('[Layout Store] 保存侧栏配置到后端失败:', error);
} }
// 2. 保存到 localStorage // localStorage is synchronous
try { try {
const sidebarsToSave = JSON.stringify(sidebarPanes.value);
localStorage.setItem(SIDEBAR_STORAGE_KEY, sidebarsToSave); localStorage.setItem(SIDEBAR_STORAGE_KEY, sidebarsToSave);
console.log('[Layout Store] 侧栏配置已自动保存到 localStorage。'); console.log('[Layout Store] 侧栏配置已自动保存到 localStorage。');
} catch (error) { } catch (error) {
@@ -432,42 +443,9 @@ export const useLayoutStore = defineStore('layout', () => {
} }
// --- 持久化 Watchers --- // --- REMOVE the old watchers that called persist ---
let layoutDebounceTimer: ReturnType<typeof setTimeout> | null = null; // watch(layoutTree, ...); // REMOVE THIS
let sidebarDebounceTimer: ReturnType<typeof setTimeout> | null = null; // watch(sidebarPanes, ...); // REMOVE THIS
// 监听主布局树变化
watch(
layoutTree,
(newTree, oldTree) => {
if (oldTree === undefined) return; // 避免初始化触发
if (JSON.stringify(newTree) !== JSON.stringify(oldTree)) {
console.log('[Layout Store] Main layout tree changed, scheduling persistence...');
if (layoutDebounceTimer) clearTimeout(layoutDebounceTimer);
layoutDebounceTimer = setTimeout(() => {
persistLayoutTree();
}, 1000); // 1秒防抖
}
},
{ deep: true }
);
// 监听侧栏配置变化
watch(
sidebarPanes,
(newPanes, oldPanes) => {
if (oldPanes === undefined) return; // 避免初始化触发
if (JSON.stringify(newPanes) !== JSON.stringify(oldPanes)) {
console.log('[Layout Store] Sidebar panes changed, scheduling persistence...');
if (sidebarDebounceTimer) clearTimeout(sidebarDebounceTimer);
sidebarDebounceTimer = setTimeout(() => {
persistSidebarPanes();
}, 1000); // 1秒防抖
}
},
{ deep: true }
);
// --- 初始化 --- // --- 初始化 ---
// Store 创建时自动初始化布局和侧栏 // Store 创建时自动初始化布局和侧栏
initializeLayout(); initializeLayout();