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">
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 { useLayoutStore, type LayoutNode, type PaneName } from '../stores/layout.store';
import draggable from 'vuedraggable';
@@ -22,9 +22,14 @@ const layoutStore = useLayoutStore();
// --- State ---
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 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 ---
const dialogRef = ref<HTMLElement | null>(null);
@@ -32,80 +37,44 @@ const dialogRef = ref<HTMLElement | null>(null);
// --- Watchers ---
watch(() => props.isVisible, (newValue) => {
if (newValue) {
// Load main layout
if (layoutStore.layoutTree) {
localLayoutTree.value = JSON.parse(JSON.stringify(layoutStore.layoutTree));
} else {
localLayoutTree.value = null; // Ensure it's null if store is null
}
// 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;
// --- Load initial data and create original copies ---
// Main layout
const initialLayout = layoutStore.layoutTree ? JSON.parse(JSON.stringify(layoutStore.layoutTree)) : null;
localLayoutTree.value = initialLayout;
originalLayoutTree.value = JSON.parse(JSON.stringify(initialLayout)); // Deep copy for original
// Sidebar config
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 {
// Terminal is not used, available list includes all
localAvailablePanes.value = [...layoutStore.allPossiblePanes];
}
// 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.');
hasChanges.value = false; // Reset changes flag on open
console.log('[LayoutConfigurator] Dialog opened, initialized available panes (non-terminals always present).');
} else {
localLayoutTree.value = null; // Clear main layout
localSidebarPanes.value = { left: [], right: [] }; // Clear sidebars
localAvailablePanes.value = []; // Clear available panes
console.log('[LayoutConfigurator] Dialog closed.');
// --- 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 ---
function getMainLayoutUsedPaneNames(node: LayoutNode | null): Set<PaneName> {
const usedNames = new Set<PaneName>();
@@ -128,32 +97,56 @@ function getAllLocalUsedPaneNames(mainNode: LayoutNode | null, sidebars: { left:
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) {
if (!localAvailablePanes.value.includes(paneName) && layoutStore.allPossiblePanes.includes(paneName)) {
// Maintain original order if possible, otherwise just add
// Find the original index in allPossiblePanes
const originalIndex = layoutStore.allPossiblePanes.indexOf(paneName);
// Only act if the pane is 'terminal' and it's not already available
if (paneName === 'terminal' && !localAvailablePanes.value.includes('terminal')) {
// Maintain original order if possible
const originalIndex = layoutStore.allPossiblePanes.indexOf('terminal');
let inserted = false;
// Try to insert based on original order relative to existing available panes
for (let i = 0; i < localAvailablePanes.value.length; i++) {
const currentAvailablePane = localAvailablePanes.value[i];
const currentOriginalIndex = layoutStore.allPossiblePanes.indexOf(currentAvailablePane);
if (originalIndex < currentOriginalIndex) {
localAvailablePanes.value.splice(i, 0, paneName);
localAvailablePanes.value.splice(i, 0, 'terminal');
inserted = true;
break;
}
}
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 ---
// 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
connections: t('layout.pane.connections', '连接列表'),
terminal: t('layout.pane.terminal', '终端'),
@@ -168,7 +161,8 @@ const paneLabels = computed(() => ({ // Assuming labels might depend on i18n
// --- Methods ---
const closeDialog = () => {
if (hasChanges.value) {
// Use the computed property for the check
if (isModified.value) {
if (confirm(t('layoutConfigurator.confirmClose', '有未保存的更改,确定要关闭吗?'))) {
emit('close');
}
@@ -177,25 +171,28 @@ const closeDialog = () => {
}
};
const saveLayout = () => {
// Save main layout
if (localLayoutTree.value) {
layoutStore.updateLayoutTree(localLayoutTree.value);
console.log('[LayoutConfigurator] Main layout saved to Store.');
} else {
// Handle potentially empty layout based on store logic
layoutStore.updateLayoutTree(null); // Assuming null is valid for empty
console.log('[LayoutConfigurator] Main layout is empty, saved null to Store.');
const saveLayout = async () => { // Make async
console.log('[LayoutConfigurator] Attempting to save layout...');
try {
// Save main layout and wait for persistence
console.log('[LayoutConfigurator] Updating main layout tree in store...');
await layoutStore.updateLayoutTree(localLayoutTree.value); // Await the async action
console.log('[LayoutConfigurator] Main layout tree update awaited.');
// Save sidebar config and wait for persistence
const sidebarConfigToSave = JSON.parse(JSON.stringify(localSidebarPanes.value));
console.log('[LayoutConfigurator] Updating sidebar panes in store:', sidebarConfigToSave);
await layoutStore.updateSidebarPanes(sidebarConfigToSave); // Await the async action
console.log('[LayoutConfigurator] Sidebar panes update awaited.');
// isModified will update automatically based on comparison with original state after save
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', '保存布局时出错,请稍后再试。'));
}
// Save sidebar config
const sidebarConfigToSave = JSON.parse(JSON.stringify(localSidebarPanes.value));
console.log('[LayoutConfigurator] Preparing to save sidebar config:', sidebarConfigToSave); // Log before sending
layoutStore.updateSidebarPanes(sidebarConfigToSave);
console.log('[LayoutConfigurator] Sidebar config sent to Store.');
hasChanges.value = false;
emit('close');
};
const resetToDefault = () => {
@@ -208,30 +205,20 @@ const resetToDefault = () => {
const defaultSidebarPanes = layoutStore.getSystemDefaultSidebarPanes();
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 nonTerminalPanesDefault = layoutStore.allPossiblePanes.filter(p => p !== 'terminal');
const availableDefault = [...nonTerminalPanesDefault];
if (!defaultUsed.has('terminal')) {
const terminalOriginalIndex = layoutStore.allPossiblePanes.indexOf('terminal');
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;
}
}
if (!inserted) {
availableDefault.push('terminal');
}
if (defaultUsed.has('terminal')) {
localAvailablePanes.value = layoutStore.allPossiblePanes.filter(p => p !== 'terminal');
} else {
localAvailablePanes.value = [...layoutStore.allPossiblePanes];
}
localAvailablePanes.value = availableDefault;
// Ensure original order
localAvailablePanes.value.sort((a, b) =>
layoutStore.allPossiblePanes.indexOf(a) - layoutStore.allPossiblePanes.indexOf(b)
);
console.log('[LayoutConfigurator] Reset to default layout, sidebar panes, and available panes (non-terminals always present).');
hasChanges.value = true; // Mark as changed after reset
console.log('[LayoutConfigurator] Reset to default layout, sidebar panes, and available panes.');
// 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);
// Assuming the update is for the root node for simplicity
// v-model on LayoutNodeEditor might handle this, but explicit update is safer
// Update the local tree; isModified will react automatically
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
@@ -265,24 +252,11 @@ function findAndRemoveNode(node: LayoutNode | null, parentNodeId: string | undef
const removedNode = updatedChildren.splice(nodeIndex, 1)[0]; // Remove and get the node
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 (removedNode.type === 'pane' && removedNode.component) {
addPaneToAvailableList(removedNode.component);
}
// 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);
// If the removed node was the terminal pane, add it back to available list
if (removedNode.type === 'pane' && removedNode.component === 'terminal') {
addPaneToAvailableList('terminal');
}
// No need to handle containers specifically for adding back, only terminal matters.
return { ...node, children: updatedChildren };
}
@@ -304,15 +278,16 @@ const handleNodeRemove = (payload: { parentNodeId: string | undefined; nodeIndex
console.log('[LayoutConfigurator] Received node remove request:', payload);
if (payload.parentNodeId === undefined && payload.nodeIndex === 0) {
if (confirm(t('layoutConfigurator.confirmClearLayout', '确定要清空整个布局吗?所有面板将返回可用列表。'))) {
// Add all panes from the tree back to available list before clearing
const usedInTree = getMainLayoutUsedPaneNames(localLayoutTree.value); // Single declaration
usedInTree.forEach(paneName => addPaneToAvailableList(paneName)); // Correctly call the helper
// Add all panes from the tree back to available list before clearing - REMOVED, no longer needed
// const usedInTree = getMainLayoutUsedPaneNames(localLayoutTree.value);
// usedInTree.forEach(paneName => addPaneToAvailableList(paneName));
// Clear the tree
// Update the local tree; isModified will react automatically
localLayoutTree.value = null;
}
} else if (payload.parentNodeId) {
// Update the local tree; isModified will react automatically
localLayoutTree.value = findAndRemoveNode(localLayoutTree.value, payload.parentNodeId, payload.nodeIndex);
// Watcher on localLayoutTree handles hasChanges
} else {
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
if (removedPane) {
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
@@ -351,50 +328,27 @@ const onDraggableChange = (event: any, side: 'left' | 'right') => { // Add side
console.log(`[LayoutConfigurator] Item moved or removed within/from ${side} sidebar.`);
}
// Ensure changes flag is set for any modification (add, remove, move)
hasChanges.value = true;
// isModified will react automatically
};
// Handle drag end from the available panes list
const handleAvailablePaneDragEnd = (event: any) => {
// Check if the item was dropped into a different list (main layout or sidebars)
if (event.to !== event.from) {
// Find the component (Draggable) associated with the source item element
// 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;
// Check if the item was dropped into a different list
if (event.to !== event.from) {
const paneName = event.oldIndex !== undefined ? localAvailablePanes.value[event.oldIndex] : 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 (paneName === 'terminal') {
console.log('[LayoutConfigurator] "terminal" pane dropped elsewhere. Removing from available list.');
// 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) {
console.log(`[LayoutConfigurator] Non-terminal pane "${paneName}" dropped elsewhere. Kept in available list (clone).`);
// Do nothing, item remains in localAvailablePanes
} else {
console.error('[LayoutConfigurator] Could not determine dragged pane name in handleAvailablePaneDragEnd.');
}
} else {
console.log('[LayoutConfigurator] Item dropped back into available list or drag cancelled.');
}
// If 'terminal' was dragged out, remove it from the available list
if (paneName === 'terminal') {
removePaneFromAvailableList('terminal');
} else if (paneName) {
console.log(`[LayoutConfigurator] Non-terminal pane "${paneName}" dropped elsewhere (clone).`);
// Other panes are clones, do nothing to the available list
} else {
console.error('[LayoutConfigurator] Could not determine dragged pane name in handleAvailablePaneDragEnd.');
}
} else {
console.log('[LayoutConfigurator] Item dropped back into available list or drag cancelled.');
}
};
</script>
@@ -404,7 +358,7 @@ const handleAvailablePaneDragEnd = (event: any) => {
<div ref="dialogRef" class="layout-configurator-dialog">
<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>
</header>
@@ -526,8 +480,8 @@ const handleAvailablePaneDragEnd = (event: any) => {
<footer class="dialog-footer">
<button @click="closeDialog" class="button-secondary">{{ t('common.cancel', '取消') }}</button>
<button @click="saveLayout" class="button-primary" :disabled="!hasChanges">
{{ t('common.save', '保存') }} {{ hasChanges ? '*' : '' }}
<button @click="saveLayout" class="button-primary" :disabled="!isModified">
{{ t('common.save', '保存') }}{{ isModified ? '*' : '' }}
</button>
</footer>
</div>
@@ -546,9 +546,9 @@ const getIconClasses = (paneName: PaneName): string[] => {
<div :class="['sidebar-panel', 'left-sidebar-panel', { active: !!activeLeftSidebarPane }]">
<button class="close-sidebar-btn" @click="closeSidebars" title="Close Sidebar">&times;</button>
<component
v-if="currentLeftSidebarComponent && (activeLeftSidebarPane !== 'fileManager' || activeSession)"
v-if="currentLeftSidebarComponent && (!['fileManager', 'statusMonitor'].includes(activeLeftSidebarPane) || activeSession)"
:is="currentLeftSidebarComponent"
:key="`left-panel-${activeLeftSidebarPane}`"
:key="`left-panel-${activeLeftSidebarPane ?? 'null'}`"
v-bind="sidebarProps(activeLeftSidebarPane)"
/>
<!-- 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>
</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>
<!-- Right Sidebar Panel -->
<div :class="['sidebar-panel', 'right-sidebar-panel', { active: !!activeRightSidebarPane }]">
<button class="close-sidebar-btn" @click="closeSidebars" title="Close Sidebar">&times;</button>
<component
v-if="currentRightSidebarComponent && (activeRightSidebarPane !== 'fileManager' || activeSession)"
v-if="currentRightSidebarComponent && (!['fileManager', 'statusMonitor'].includes(activeRightSidebarPane) || activeSession)"
:is="currentRightSidebarComponent"
:key="`right-panel-${activeRightSidebarPane}`"
v-bind="sidebarProps(activeRightSidebarPane)"
:key="`right-panel-${activeRightSidebarPane ?? 'null'}`"
/>
<!-- Placeholder if FileManager is selected but no active 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>
</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>
<!-- Right Sidebar Buttons (Only render if root) -->
+1 -1
View File
@@ -675,7 +675,7 @@
"remove": "移除"
},
"layoutConfigurator": {
"title": "配置工作区布局",
"title": "布局管理器",
"availablePanes": "可用面板",
"layoutPreview": "主布局预览(拖拽到此处)",
"resetDefault": "恢复默认",
+54 -76
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
// 可选:添加验证逻辑 (如果 newTree 不是 null)
async function updateLayoutTree(newTree: LayoutNode | null) { // Make async
// 可选:添加验证逻辑
if (newTree) {
// TODO: Add validation for LayoutNode structure if needed
// TODO: Add validation
}
// 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);
// --- Directly call persist ---
await persistLayoutTree(); // Await persistence directly
} else {
console.log('[Layout Store] updateLayoutTree called but tree is unchanged.');
}
layoutTree.value = newTree; // Assign null or the new tree
console.log('[Layout Store] 布局树已更新。 New tree:', newTree);
// 保存将在 watch 中自动触发
}
// 新增:更新侧栏配置
function updateSidebarPanes(newPanes: { left: PaneName[], right: PaneName[] }) {
async function updateSidebarPanes(newPanes: { left: PaneName[], right: PaneName[] }) { // Make async
// --- Add Validation ---
if (newPanes &&
isValidPaneNameArray(newPanes.left, allPossiblePanes.value) &&
isValidPaneNameArray(newPanes.right, allPossiblePanes.value))
{
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)));
// 保存将在 watch 中自动触发
// Check if panes actually changed
if (JSON.stringify(newPanes) !== JSON.stringify(sidebarPanes.value)) {
sidebarPanes.value = newPanes as { left: PaneName[], right: PaneName[] }; // Assign validated data
console.log('[Layout Store] 侧栏配置已通过验证并更新。 New sidebarPanes value:', JSON.parse(JSON.stringify(sidebarPanes.value)));
// --- Directly call persist ---
await persistSidebarPanes(); // Await persistence directly
} else {
console.log('[Layout Store] updateSidebarPanes called but panes are unchanged.');
}
} else {
console.error('[Layout Store] updateSidebarPanes 接收到无效的侧栏配置数据,未更新状态:', newPanes);
// 可选:抛出错误或通知用户
}
}
// 递归查找并更新节点大小
function findAndUpdateNodeSize(node: LayoutNode | null, nodeId: string, childrenSizes: { index: number; size: number }[]): LayoutNode | null {
if (!node) return null;
@@ -313,18 +332,18 @@ export const useLayoutStore = defineStore('layout', () => {
// 新增 Action: 更新特定容器节点的子节点大小
function updateNodeSizes(nodeId: string, childrenSizes: { index: number; size: number }[]) {
console.log(`[Layout Store] 请求更新节点 ${nodeId} 的子节点大小:`, childrenSizes);
const originalJson = JSON.stringify(layoutTree.value); // Store original state
const updatedTree = findAndUpdateNodeSize(layoutTree.value, nodeId, childrenSizes);
if (updatedTree && updatedTree !== layoutTree.value) {
// 只有在树实际发生变化时才更新 ref 以触发 watch
if (updatedTree && JSON.stringify(updatedTree) !== originalJson) { // Compare with original JSON
layoutTree.value = updatedTree;
console.log(`[Layout Store] 节点 ${nodeId} 的子节点大小已更新。`);
} else if (updatedTree === layoutTree.value) {
console.log(`[Layout Store] 未找到节点 ${nodeId} 或大小未改变。`);
console.log(`[Layout Store] 节点 ${nodeId} 的子节点大小已更新,触发防抖保存`);
// --- Use debounced persist for resize ---
debouncedPersistLayout();
} else {
console.error(`[Layout Store] 更新节点 ${nodeId} 大小后得到无效的树结构`);
console.log(`[Layout Store] 未找到节点 ${nodeId} 大小未改变`);
}
}
// 新增 Action: 切换布局(Header/Footer)的可见性
function toggleLayoutVisibility() {
isLayoutVisible.value = !isLayoutVisible.value;
@@ -383,27 +402,19 @@ export const useLayoutStore = defineStore('layout', () => {
}
// 新增 Action: 将当前主布局树持久化到后端和 localStorage
async function persistLayoutTree() {
if (!layoutTree.value) {
console.warn('[Layout Store] persistLayoutTree: layoutTree is null, cannot persist.');
// TODO: 考虑是否需要删除后端设置或发送空布局
// await apiClient.put('/settings/layout', null); // 发送 null 或空对象
localStorage.removeItem(LAYOUT_STORAGE_KEY);
return;
}
const layoutToSave = JSON.stringify(layoutTree.value);
// 1. 保存到后端
async function persistLayoutTree() { // Make async
// ... (existing try/catch logic for backend and localStorage) ...
// Ensure apiClient calls are awaited if they return promises
try {
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 apiClient.put('/settings/layout', layoutTree.value); // await
console.log('[Layout Store] 主布局已成功保存到后端 (sent value):', layoutTree.value);
} catch (error) {
console.error('[Layout Store] 保存主布局到后端失败:', error);
}
// 2. 保存到 localStorage
// localStorage is synchronous
try {
// If layoutTree.value is null, layoutToSave will be 'null'
const layoutToSave = JSON.stringify(layoutTree.value);
localStorage.setItem(LAYOUT_STORAGE_KEY, layoutToSave);
console.log('[Layout Store] 主布局已自动保存到 localStorage (saved value):', layoutToSave);
} catch (error) {
@@ -412,18 +423,18 @@ export const useLayoutStore = defineStore('layout', () => {
}
// 新增 Action: 将当前侧栏配置持久化到后端和 localStorage
async function persistSidebarPanes() {
const sidebarsToSave = JSON.stringify(sidebarPanes.value);
// 1. 保存到后端 (假设 API 端点为 /settings/sidebar)
try {
async function persistSidebarPanes() { // Make async
// ... (existing try/catch logic for backend and localStorage) ...
try {
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] 侧栏配置已成功保存到后端。');
} catch (error) {
console.error('[Layout Store] 保存侧栏配置到后端失败:', error);
}
// 2. 保存到 localStorage
// localStorage is synchronous
try {
const sidebarsToSave = JSON.stringify(sidebarPanes.value);
localStorage.setItem(SIDEBAR_STORAGE_KEY, sidebarsToSave);
console.log('[Layout Store] 侧栏配置已自动保存到 localStorage。');
} catch (error) {
@@ -432,42 +443,9 @@ export const useLayoutStore = defineStore('layout', () => {
}
// --- 持久化 Watchers ---
let layoutDebounceTimer: ReturnType<typeof setTimeout> | null = null;
let sidebarDebounceTimer: ReturnType<typeof setTimeout> | null = null;
// 监听主布局树变化
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 }
);
// --- REMOVE the old watchers that called persist ---
// watch(layoutTree, ...); // REMOVE THIS
// watch(sidebarPanes, ...); // REMOVE THIS
// --- 初始化 ---
// Store 创建时自动初始化布局和侧栏
initializeLayout();