feat(workspace): add workbench layout and traffic totals
Rework the default /workspace layout into a three-column view with a left-side Workbench, centered terminal, and right-side status monitor. Add a new Workbench pane that groups file manager, command history, and editor into tabs while preserving panel state. Extend server status data to expose cumulative network upload and download totals since boot, and show them in the monitor. Include a lightweight migration for the old default layout and update related locale strings, pane metadata, and knowledge base records.
This commit is contained in:
@@ -163,6 +163,7 @@ const paneLabels = computed(() => ({ // Assuming labels might depend on i18n
|
||||
commandBar: t('layout.pane.commandBar', '命令栏'),
|
||||
fileManager: t('layout.pane.fileManager', '文件管理器'),
|
||||
editor: t('layout.pane.editor', '编辑器'),
|
||||
workbench: t('layout.pane.workbench', '工作台'),
|
||||
statusMonitor: t('layout.pane.statusMonitor', '状态监视器'),
|
||||
commandHistory: t('layout.pane.commandHistory', '命令历史'),
|
||||
quickCommands: t('layout.pane.quickCommands', '快捷指令'),
|
||||
|
||||
@@ -88,6 +88,7 @@ const componentMap: Record<PaneName, Component> = {
|
||||
commandBar: defineAsyncComponent(() => import('./CommandInputBar.vue')),
|
||||
fileManager: defineAsyncComponent(() => import('./FileManager.vue')),
|
||||
editor: defineAsyncComponent(() => import('./FileEditorContainer.vue')),
|
||||
workbench: defineAsyncComponent(() => import('./WorkspaceWorkbench.vue')),
|
||||
statusMonitor: defineAsyncComponent(() => import('./StatusMonitor.vue')),
|
||||
commandHistory: defineAsyncComponent(() => import('../views/CommandHistoryView.vue')),
|
||||
quickCommands: defineAsyncComponent(() => import('../views/QuickCommandsView.vue')),
|
||||
@@ -131,6 +132,7 @@ const paneLabels = computed(() => ({
|
||||
commandBar: t('layout.pane.commandBar', '命令栏'),
|
||||
fileManager: t('layout.pane.fileManager', '文件管理器'),
|
||||
editor: t('layout.pane.editor', '编辑器'),
|
||||
workbench: t('layout.pane.workbench', '工作台'),
|
||||
statusMonitor: t('layout.pane.statusMonitor', '状态监视器'),
|
||||
commandHistory: t('layout.pane.commandHistory', '命令历史'),
|
||||
quickCommands: t('layout.pane.quickCommands', '快捷指令'),
|
||||
@@ -186,6 +188,21 @@ const componentProps = computed(() => {
|
||||
class: 'pane-content',
|
||||
// --- 移除事件转发 ---
|
||||
};
|
||||
case 'workbench':
|
||||
return {
|
||||
tabs: props.editorTabs,
|
||||
activeTabId: props.activeEditorTabId,
|
||||
sessionId: currentActiveSession?.sessionId ?? props.activeSessionId,
|
||||
instanceId: props.layoutNode.id || `workbench-main-${props.activeSessionId ?? 'unknown'}`,
|
||||
dbConnectionId: currentActiveSession?.connectionId ?? null,
|
||||
wsDeps: currentActiveSession ? {
|
||||
sendMessage: currentActiveSession.wsManager.sendMessage,
|
||||
onMessage: currentActiveSession.wsManager.onMessage,
|
||||
isConnected: currentActiveSession.wsManager.isConnected,
|
||||
isSftpReady: currentActiveSession.wsManager.isSftpReady
|
||||
} : null,
|
||||
class: 'flex flex-col flex-grow h-full overflow-hidden',
|
||||
};
|
||||
case 'commandBar':
|
||||
return {
|
||||
class: 'pane-content',
|
||||
@@ -240,6 +257,21 @@ const sidebarProps = computed(() => (paneName: PaneName | null, side: 'left' | '
|
||||
...baseProps,
|
||||
// --- 移除事件转发 ---
|
||||
};
|
||||
case 'workbench':
|
||||
return {
|
||||
...baseProps,
|
||||
tabs: editorTabsFromStore.value,
|
||||
activeTabId: activeEditorTabIdFromStore.value,
|
||||
sessionId: activeSession.value?.sessionId ?? props.activeSessionId,
|
||||
instanceId: side === 'left' ? 'workbench-sidebar-left' : 'workbench-sidebar-right',
|
||||
dbConnectionId: activeSession.value?.connectionId ?? null,
|
||||
wsDeps: activeSession.value ? {
|
||||
sendMessage: activeSession.value.wsManager.sendMessage,
|
||||
onMessage: activeSession.value.wsManager.onMessage,
|
||||
isConnected: activeSession.value.wsManager.isConnected,
|
||||
isSftpReady: activeSession.value.wsManager.isSftpReady
|
||||
} : null,
|
||||
};
|
||||
case 'fileManager':
|
||||
// Only provide props if there's an active session
|
||||
if (activeSession.value) {
|
||||
@@ -357,6 +389,7 @@ const getIconClasses = (paneName: PaneName): string[] => {
|
||||
case 'quickCommands': return ['fas', 'fa-bolt'];
|
||||
case 'dockerManager': return ['fab', 'fa-docker']; // Use 'fab' for Docker
|
||||
case 'editor': return ['fas', 'fa-file-alt'];
|
||||
case 'workbench': return ['fas', 'fa-table-columns'];
|
||||
case 'statusMonitor': return ['fas', 'fa-tachometer-alt'];
|
||||
case 'suspendedSshSessions': return ['fas', 'fa-pause-circle']; // 图标:暂停圈
|
||||
// Add other specific icons here if needed
|
||||
|
||||
@@ -143,6 +143,21 @@
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div v-if="activeSessionId && currentServerStatus" class="status-item grid grid-cols-[auto_1fr] items-start gap-3 mt-2">
|
||||
<label class="font-semibold text-text-secondary text-left whitespace-nowrap">{{ t('statusMonitor.totalTrafficLabel') }}:</label>
|
||||
<div class="flex flex-col gap-1.5 text-xs">
|
||||
<span class="inline-flex items-center gap-2 whitespace-nowrap text-green-500">
|
||||
<i class="fas fa-arrow-down w-3 text-center"></i>
|
||||
<span>{{ t('statusMonitor.downloadLabel') }}</span>
|
||||
<span class="font-mono text-foreground">{{ formatBytes(currentServerStatus?.netRxTotalBytes) }}</span>
|
||||
</span>
|
||||
<span class="inline-flex items-center gap-2 whitespace-nowrap text-orange-500">
|
||||
<i class="fas fa-arrow-up w-3 text-center"></i>
|
||||
<span>{{ t('statusMonitor.uploadLabel') }}</span>
|
||||
<span class="font-mono text-foreground">{{ formatBytes(currentServerStatus?.netTxTotalBytes) }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 图表组件 -->
|
||||
<!-- 仅当有活动会话且有数据时渲染图表 -->
|
||||
<StatusCharts v-if="activeSessionId && currentServerStatus" :server-status="currentServerStatus" :active-session-id="activeSessionId" />
|
||||
@@ -161,6 +176,7 @@ import { storeToRefs } from 'pinia'; // 导入 storeToRefs
|
||||
import { useSettingsStore } from '../stores/settings.store'; // 导入设置 store
|
||||
import { useConnectionsStore } from '../stores/connections.store'; // 导入连接 store
|
||||
import { useUiNotificationsStore } from '../stores/uiNotifications.store'; // + 导入通知 store
|
||||
import type { ServerStatus } from '../types/server.types';
|
||||
|
||||
const { t } = useI18n();
|
||||
const sessionStore = useSessionStore();
|
||||
@@ -173,24 +189,6 @@ const isSwitchingSession = ref(false);
|
||||
|
||||
const formatPercentageText = (percentage: number): string => `${Math.round(percentage)}%`;
|
||||
|
||||
interface ServerStatus {
|
||||
cpuPercent?: number;
|
||||
memPercent?: number;
|
||||
memUsed?: number; // MB
|
||||
memTotal?: number; // MB
|
||||
swapPercent?: number;
|
||||
swapUsed?: number; // MB
|
||||
swapTotal?: number; // MB
|
||||
diskPercent?: number;
|
||||
diskUsed?: number; // KB
|
||||
diskTotal?: number; // KB
|
||||
cpuModel?: string;
|
||||
netRxRate?: number; // 字节/秒
|
||||
netTxRate?: number; // 字节/秒
|
||||
netInterface?: string;
|
||||
osName?: string;
|
||||
}
|
||||
|
||||
// --- Props ---
|
||||
const props = defineProps({
|
||||
activeSessionId: {
|
||||
@@ -276,6 +274,15 @@ const formatBytesPerSecond = (bytes?: number): string => {
|
||||
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} ${t('statusMonitor.gigaBytesPerSecond')}`;
|
||||
};
|
||||
|
||||
const formatBytes = (bytes?: number): string => {
|
||||
if (bytes === undefined || bytes === null || isNaN(bytes)) return t('statusMonitor.notAvailable');
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} ${t('statusMonitor.megaBytes')}`;
|
||||
if (bytes < 1024 * 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} ${t('statusMonitor.gigaBytes')}`;
|
||||
return `${(bytes / (1024 * 1024 * 1024 * 1024)).toFixed(1)} TB`;
|
||||
};
|
||||
|
||||
const formatKbToGb = (kb?: number): string => {
|
||||
if (kb === undefined || kb === null) return t('statusMonitor.notAvailable');
|
||||
if (kb === 0) return `0.0 ${t('statusMonitor.gigaBytes')}`;
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch, type PropType } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import CommandHistoryView from '../views/CommandHistoryView.vue';
|
||||
import FileManager from './FileManager.vue';
|
||||
import FileEditorContainer from './FileEditorContainer.vue';
|
||||
import { useSessionStore } from '../stores/session.store';
|
||||
import type { FileTab } from '../stores/fileEditor.store';
|
||||
import type { WebSocketDependencies } from '../composables/useSftpActions';
|
||||
|
||||
type WorkbenchTab = 'files' | 'history' | 'editor';
|
||||
|
||||
const props = defineProps({
|
||||
tabs: {
|
||||
type: Array as PropType<FileTab[]>,
|
||||
default: () => [],
|
||||
},
|
||||
activeTabId: {
|
||||
type: String as PropType<string | null>,
|
||||
default: null,
|
||||
},
|
||||
sessionId: {
|
||||
type: String as PropType<string | null>,
|
||||
default: null,
|
||||
},
|
||||
instanceId: {
|
||||
type: String as PropType<string | null>,
|
||||
default: null,
|
||||
},
|
||||
dbConnectionId: {
|
||||
type: String as PropType<string | null>,
|
||||
default: null,
|
||||
},
|
||||
wsDeps: {
|
||||
type: Object as PropType<WebSocketDependencies | null>,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
const sessionStore = useSessionStore();
|
||||
const { sessions } = storeToRefs(sessionStore);
|
||||
|
||||
const activeWorkbenchTab = ref<WorkbenchTab>('files');
|
||||
|
||||
const workbenchTabs = computed(() => [
|
||||
{
|
||||
id: 'files' as const,
|
||||
label: t('workspace.workbench.tabs.files', '文件'),
|
||||
icon: 'fas fa-folder-open',
|
||||
},
|
||||
{
|
||||
id: 'history' as const,
|
||||
label: t('workspace.workbench.tabs.history', '历史命令'),
|
||||
icon: 'fas fa-history',
|
||||
},
|
||||
{
|
||||
id: 'editor' as const,
|
||||
label: t('workspace.workbench.tabs.editor', '编辑器'),
|
||||
icon: 'fas fa-pen-to-square',
|
||||
},
|
||||
]);
|
||||
|
||||
const activeSessionName = computed(() => {
|
||||
if (!props.sessionId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return sessions.value.get(props.sessionId)?.connectionName ?? props.sessionId;
|
||||
});
|
||||
|
||||
const hasFileManagerContext = computed(() => {
|
||||
return Boolean(props.sessionId && props.instanceId && props.dbConnectionId && props.wsDeps);
|
||||
});
|
||||
|
||||
const fileManagerSessionId = computed(() => props.sessionId ?? '');
|
||||
const fileManagerInstanceId = computed(() => props.instanceId ?? '');
|
||||
const fileManagerConnectionId = computed(() => props.dbConnectionId ?? '');
|
||||
const fileManagerWsDeps = computed(() => props.wsDeps as WebSocketDependencies);
|
||||
|
||||
watch(
|
||||
() => props.activeTabId,
|
||||
(newActiveTabId) => {
|
||||
if (newActiveTabId) {
|
||||
activeWorkbenchTab.value = 'editor';
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-full min-h-0 flex-col overflow-hidden bg-background">
|
||||
<div class="border-b border-border bg-header px-3 py-3">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold text-foreground">
|
||||
{{ t('workspace.workbench.title', 'Workbench') }}
|
||||
</h3>
|
||||
<p class="mt-1 text-xs text-text-secondary">
|
||||
{{ activeSessionName || t('workspace.workbench.noSession', '未激活会话') }}
|
||||
</p>
|
||||
</div>
|
||||
<span class="rounded-full border border-border bg-background px-2 py-1 text-[11px] font-medium text-text-secondary">
|
||||
{{ t('workspace.workbench.label', '工作台') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-3 grid grid-cols-3 gap-2">
|
||||
<button
|
||||
v-for="tab in workbenchTabs"
|
||||
:key="tab.id"
|
||||
type="button"
|
||||
@click="activeWorkbenchTab = tab.id"
|
||||
:class="[
|
||||
'inline-flex items-center justify-center gap-2 rounded-lg border px-3 py-2 text-xs font-medium transition-colors',
|
||||
activeWorkbenchTab === tab.id
|
||||
? 'border-primary bg-primary text-white shadow-sm'
|
||||
: 'border-border bg-background text-text-secondary hover:border-primary/40 hover:text-foreground'
|
||||
]"
|
||||
>
|
||||
<i :class="tab.icon"></i>
|
||||
<span>{{ tab.label }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="relative flex-1 min-h-0 overflow-hidden bg-background">
|
||||
<div v-show="activeWorkbenchTab === 'files'" class="absolute inset-0 min-h-0">
|
||||
<FileManager
|
||||
v-if="hasFileManagerContext"
|
||||
:session-id="fileManagerSessionId"
|
||||
:instance-id="fileManagerInstanceId"
|
||||
:db-connection-id="fileManagerConnectionId"
|
||||
:ws-deps="fileManagerWsDeps"
|
||||
class="h-full"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="flex h-full flex-col items-center justify-center gap-3 px-6 text-center text-text-secondary"
|
||||
>
|
||||
<i class="fas fa-plug text-3xl"></i>
|
||||
<div class="text-sm font-medium">
|
||||
{{ t('layout.noActiveSession.title', '没有活动的会话') }}
|
||||
</div>
|
||||
<div class="text-xs">
|
||||
{{ t('workspace.workbench.fileManagerHint', '激活一个 SSH 会话后即可浏览远程文件。') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-show="activeWorkbenchTab === 'history'" class="absolute inset-0 min-h-0">
|
||||
<CommandHistoryView />
|
||||
</div>
|
||||
|
||||
<div v-show="activeWorkbenchTab === 'editor'" class="absolute inset-0 min-h-0">
|
||||
<FileEditorContainer
|
||||
:tabs="tabs"
|
||||
:active-tab-id="activeTabId"
|
||||
:session-id="sessionId"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -583,6 +583,9 @@
|
||||
"swapLabel": "Swap:",
|
||||
"diskLabel": "Disk:",
|
||||
"networkLabel": "Network",
|
||||
"totalTrafficLabel": "Traffic Since Boot",
|
||||
"downloadLabel": "Download",
|
||||
"uploadLabel": "Upload",
|
||||
"notAvailable": "N/A",
|
||||
"bytesPerSecond": "B/s",
|
||||
"kiloBytesPerSecond": "KB/s",
|
||||
@@ -1250,10 +1253,11 @@
|
||||
"pane": {
|
||||
"connections": "Connections",
|
||||
"terminal": "Terminal",
|
||||
"commandBar": "Command Bar",
|
||||
"fileManager": "File Manager",
|
||||
"editor": "Editor",
|
||||
"statusMonitor": "Status Monitor",
|
||||
"commandBar": "Command Bar",
|
||||
"fileManager": "File Manager",
|
||||
"editor": "Editor",
|
||||
"workbench": "Workbench",
|
||||
"statusMonitor": "Status Monitor",
|
||||
"commandHistory": "Command History",
|
||||
"quickCommands": "Quick Commands",
|
||||
"dockerManager": "Docker Manager",
|
||||
@@ -1262,14 +1266,28 @@
|
||||
"panes": {
|
||||
"suspendedSshSessions": "Suspended Session Manager"
|
||||
},
|
||||
"noActiveSession": {
|
||||
"title": "No Active Session",
|
||||
"message": "Please connect to a session first",
|
||||
"fileManagerSidebar": "File Manager requires an active session",
|
||||
"statusMonitorSidebar": "Status Monitor requires an active session"
|
||||
}
|
||||
},
|
||||
"header": {
|
||||
"noActiveSession": {
|
||||
"title": "No Active Session",
|
||||
"message": "Please connect to a session first",
|
||||
"fileManagerSidebar": "File Manager requires an active session",
|
||||
"statusMonitorSidebar": "Status Monitor requires an active session"
|
||||
}
|
||||
},
|
||||
"workspace": {
|
||||
"noActiveSession": "No active session",
|
||||
"workbench": {
|
||||
"title": "Workbench",
|
||||
"label": "Workspace",
|
||||
"noSession": "No active session",
|
||||
"fileManagerHint": "Activate an SSH session to browse remote files.",
|
||||
"tabs": {
|
||||
"files": "Files",
|
||||
"history": "History",
|
||||
"editor": "Editor"
|
||||
}
|
||||
}
|
||||
},
|
||||
"header": {
|
||||
"hide": "Hide",
|
||||
"show": "Show Top Navigation"
|
||||
},
|
||||
|
||||
@@ -555,6 +555,7 @@
|
||||
"dockerManager": "Docker マネージャー",
|
||||
"editor": "エディター",
|
||||
"fileManager": "ファイルマネージャー",
|
||||
"workbench": "ワークベンチ",
|
||||
"quickCommands": "クイックコマンド",
|
||||
"statusMonitor": "ステータスモニター",
|
||||
"terminal": "ターミナル",
|
||||
@@ -564,6 +565,20 @@
|
||||
"suspendedSshSessions": "中断されたセッション管理者"
|
||||
}
|
||||
},
|
||||
"workspace": {
|
||||
"noActiveSession": "アクティブなセッションはありません",
|
||||
"workbench": {
|
||||
"title": "Workbench",
|
||||
"label": "ワークスペース",
|
||||
"noSession": "アクティブなセッションはありません",
|
||||
"fileManagerHint": "SSH セッションを有効にするとリモートファイルを参照できます。",
|
||||
"tabs": {
|
||||
"files": "ファイル",
|
||||
"history": "履歴",
|
||||
"editor": "エディター"
|
||||
}
|
||||
}
|
||||
},
|
||||
"layoutConfigurator": {
|
||||
"availablePanes": "利用可能なパネル",
|
||||
"confirmClearLayout": "レイアウト全体をクリアしますか?すべてのパネルが利用可能なリストに戻ります。",
|
||||
@@ -1205,6 +1220,9 @@
|
||||
"megaBytesPerSecond": "MB/秒",
|
||||
"memoryLabel": "メモリ:",
|
||||
"networkLabel": "ネットワーク",
|
||||
"totalTrafficLabel": "起動後の累計トラフィック",
|
||||
"downloadLabel": "受信",
|
||||
"uploadLabel": "送信",
|
||||
"notAvailable": "N/A",
|
||||
"osLabel": "OS:",
|
||||
"swapLabel": "スワップ:",
|
||||
@@ -1612,4 +1630,4 @@
|
||||
"copiedSuccess": "パスがクリップボードにコピーされました",
|
||||
"copiedError": "パスのコピーに失敗しました"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -583,6 +583,9 @@
|
||||
"swapLabel": "Swap:",
|
||||
"diskLabel": "磁盘:",
|
||||
"networkLabel": "网络",
|
||||
"totalTrafficLabel": "开机累计流量",
|
||||
"downloadLabel": "下行",
|
||||
"uploadLabel": "上行",
|
||||
"notAvailable": "N/A",
|
||||
"bytesPerSecond": "B/s",
|
||||
"kiloBytesPerSecond": "KB/s",
|
||||
@@ -1254,10 +1257,11 @@
|
||||
"pane": {
|
||||
"connections": "连接列表",
|
||||
"terminal": "终端",
|
||||
"commandBar": "命令栏",
|
||||
"fileManager": "文件管理器",
|
||||
"editor": "编辑器",
|
||||
"statusMonitor": "状态监视器",
|
||||
"commandBar": "命令栏",
|
||||
"fileManager": "文件管理器",
|
||||
"editor": "编辑器",
|
||||
"workbench": "工作台",
|
||||
"statusMonitor": "状态监视器",
|
||||
"commandHistory": "命令历史",
|
||||
"quickCommands": "快捷指令",
|
||||
"dockerManager": "Docker 管理器",
|
||||
@@ -1266,14 +1270,28 @@
|
||||
"panes": {
|
||||
"suspendedSshSessions": "挂起会话管理器"
|
||||
},
|
||||
"noActiveSession": {
|
||||
"title": "无活动会话",
|
||||
"message": "请先连接一个会话",
|
||||
"fileManagerSidebar": "文件管理器需要活动会话",
|
||||
"statusMonitorSidebar": "状态监视器需要活动会话"
|
||||
}
|
||||
},
|
||||
"header": {
|
||||
"noActiveSession": {
|
||||
"title": "无活动会话",
|
||||
"message": "请先连接一个会话",
|
||||
"fileManagerSidebar": "文件管理器需要活动会话",
|
||||
"statusMonitorSidebar": "状态监视器需要活动会话"
|
||||
}
|
||||
},
|
||||
"workspace": {
|
||||
"noActiveSession": "没有活动的会话",
|
||||
"workbench": {
|
||||
"title": "Workbench",
|
||||
"label": "工作台",
|
||||
"noSession": "未激活会话",
|
||||
"fileManagerHint": "激活一个 SSH 会话后即可浏览远程文件。",
|
||||
"tabs": {
|
||||
"files": "文件",
|
||||
"history": "历史命令",
|
||||
"editor": "编辑器"
|
||||
}
|
||||
}
|
||||
},
|
||||
"header": {
|
||||
"hide": "隐藏",
|
||||
"show": "显示顶部导航"
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ref, computed, watch, type Ref, type ComputedRef } from 'vue';
|
||||
import apiClient from '../utils/apiClient';
|
||||
|
||||
// 定义所有可用面板的名称
|
||||
export type PaneName = 'connections' | 'terminal' | 'commandBar' | 'fileManager' | 'editor' | 'statusMonitor' | 'commandHistory' | 'quickCommands' | 'dockerManager' | 'suspendedSshSessions';
|
||||
export type PaneName = 'connections' | 'terminal' | 'commandBar' | 'fileManager' | 'editor' | 'workbench' | 'statusMonitor' | 'commandHistory' | 'quickCommands' | 'dockerManager' | 'suspendedSshSessions';
|
||||
|
||||
// 定义布局节点接口
|
||||
export interface LayoutNode {
|
||||
@@ -25,7 +25,38 @@ function generateId(): string {
|
||||
return Math.random().toString(36).substring(2, 15);
|
||||
}
|
||||
|
||||
// 定义默认布局结构 (根据用户提供的配置更新,但使用 generateId)
|
||||
function isPaneNode(node: LayoutNode | undefined | null, component: PaneName): boolean {
|
||||
return node?.type === 'pane' && node.component === component;
|
||||
}
|
||||
|
||||
function isLegacyDefaultLayout(node: LayoutNode | null): boolean {
|
||||
if (!node || node.type !== 'container' || node.direction !== 'horizontal' || !node.children || node.children.length !== 3) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const [leftColumn, centerColumn, rightColumn] = node.children;
|
||||
|
||||
return Boolean(
|
||||
leftColumn?.type === 'container' &&
|
||||
leftColumn.direction === 'vertical' &&
|
||||
leftColumn.children?.length === 3 &&
|
||||
isPaneNode(leftColumn.children[0], 'statusMonitor') &&
|
||||
isPaneNode(leftColumn.children[1], 'commandHistory') &&
|
||||
isPaneNode(leftColumn.children[2], 'quickCommands') &&
|
||||
centerColumn?.type === 'container' &&
|
||||
centerColumn.direction === 'vertical' &&
|
||||
centerColumn.children?.length === 3 &&
|
||||
isPaneNode(centerColumn.children[0], 'terminal') &&
|
||||
isPaneNode(centerColumn.children[1], 'commandBar') &&
|
||||
isPaneNode(centerColumn.children[2], 'fileManager') &&
|
||||
rightColumn?.type === 'container' &&
|
||||
rightColumn.direction === 'vertical' &&
|
||||
rightColumn.children?.length === 1 &&
|
||||
isPaneNode(rightColumn.children[0], 'editor')
|
||||
);
|
||||
}
|
||||
|
||||
// 定义默认布局结构
|
||||
const getDefaultLayout = (): LayoutNode => ({
|
||||
id: generateId(), // Generate new ID
|
||||
type: "container",
|
||||
@@ -33,69 +64,35 @@ const getDefaultLayout = (): LayoutNode => ({
|
||||
children: [
|
||||
{
|
||||
id: generateId(), // Generate new ID
|
||||
type: "container",
|
||||
direction: "vertical",
|
||||
children: [
|
||||
{
|
||||
id: generateId(), // Generate new ID
|
||||
type: "pane",
|
||||
component: "statusMonitor",
|
||||
size: 44.56372126372345 // 使用用户提供的 size
|
||||
},
|
||||
{
|
||||
id: generateId(), // Generate new ID
|
||||
type: "pane",
|
||||
component: "commandHistory",
|
||||
size: 26.235651482670775 // 使用用户提供的 size
|
||||
},
|
||||
{
|
||||
id: generateId(), // Generate new ID
|
||||
type: "pane",
|
||||
component: "quickCommands",
|
||||
size: 29.200627253605774 // 使用用户提供的 size
|
||||
}
|
||||
],
|
||||
size: 14.59006012147659 // 使用用户提供的 size
|
||||
type: "pane",
|
||||
component: "workbench",
|
||||
size: 23
|
||||
},
|
||||
{
|
||||
id: generateId(), // Generate new ID
|
||||
type: "container",
|
||||
direction: "vertical",
|
||||
size: 58.02787988626151, // 使用用户提供的 size
|
||||
size: 57,
|
||||
children: [
|
||||
{
|
||||
id: generateId(), // Generate new ID
|
||||
type: "pane",
|
||||
component: "terminal",
|
||||
size: 59.94833664833884 // 使用用户提供的 size
|
||||
size: 94
|
||||
},
|
||||
{
|
||||
id: generateId(), // Generate new ID
|
||||
type: "pane",
|
||||
component: "commandBar",
|
||||
size: 5 // 使用用户提供的 size
|
||||
},
|
||||
{
|
||||
id: generateId(), // Generate new ID
|
||||
type: "pane",
|
||||
component: "fileManager",
|
||||
size: 35.05166335166116 // 使用用户提供的 size
|
||||
size: 6
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: generateId(), // Generate new ID
|
||||
type: "container",
|
||||
direction: "vertical",
|
||||
size: 27.3820599922619, // 使用用户提供的 size
|
||||
children: [
|
||||
{
|
||||
id: generateId(), // Generate new ID
|
||||
type: "pane",
|
||||
component: "editor",
|
||||
size: 100 // 使用用户提供的 size
|
||||
}
|
||||
]
|
||||
type: "pane",
|
||||
component: "statusMonitor",
|
||||
size: 20
|
||||
}
|
||||
]
|
||||
});
|
||||
@@ -165,7 +162,7 @@ export const useLayoutStore = defineStore('layout', () => {
|
||||
// 所有理论上可用的面板名称
|
||||
const allPossiblePanes: Ref<PaneName[]> = ref([
|
||||
'connections', 'terminal', 'commandBar', 'fileManager',
|
||||
'editor', 'statusMonitor', 'commandHistory', 'quickCommands',
|
||||
'editor', 'workbench', 'statusMonitor', 'commandHistory', 'quickCommands',
|
||||
'dockerManager', 'suspendedSshSessions' // <-- 添加新的挂起 SSH 会话视图
|
||||
]);
|
||||
// 控制布局(Header/Footer)可见性的状态
|
||||
@@ -201,6 +198,18 @@ function ensureNodeIds(node: LayoutNode | null): LayoutNode | null {
|
||||
return node;
|
||||
}
|
||||
|
||||
function normalizeLoadedLayout(node: LayoutNode | null): LayoutNode | null {
|
||||
const layoutWithIds = ensureNodeIds(node);
|
||||
if (!layoutWithIds) return null;
|
||||
|
||||
if (isLegacyDefaultLayout(layoutWithIds)) {
|
||||
console.log('[Layout Store] Detected legacy workspace default layout, migrating to workbench layout.');
|
||||
return ensureNodeIds(getDefaultLayout());
|
||||
}
|
||||
|
||||
return layoutWithIds;
|
||||
}
|
||||
|
||||
// --- Actions ---
|
||||
// 初始化布局和侧栏配置
|
||||
async function initializeLayout() {
|
||||
@@ -220,7 +229,7 @@ function ensureNodeIds(node: LayoutNode | null): LayoutNode | null {
|
||||
if (response.data) {
|
||||
console.log('[Layout Store] Step 1: Backend returned data.');
|
||||
// +++ 在赋值前确保 ID 存在 +++
|
||||
loadedLayout = ensureNodeIds(response.data);
|
||||
loadedLayout = normalizeLoadedLayout(response.data);
|
||||
layoutLoadedFromBackend = true;
|
||||
console.log('[Layout Store] Step 1: Layout processed with ensureNodeIds.');
|
||||
// 更新 localStorage (使用处理过的布局)
|
||||
@@ -270,13 +279,13 @@ function ensureNodeIds(node: LayoutNode | null): LayoutNode | null {
|
||||
const parsedLayout = JSON.parse(savedLayout) as LayoutNode;
|
||||
console.log('[Layout Store] Step 3: Parsed layout from localStorage.');
|
||||
// +++ 在赋值前确保 ID 存在 +++
|
||||
loadedLayout = ensureNodeIds(parsedLayout);
|
||||
loadedLayout = normalizeLoadedLayout(parsedLayout);
|
||||
console.log('[Layout Store] Step 3: Layout processed with ensureNodeIds.');
|
||||
} else {
|
||||
// 4. 如果 localStorage 也没有,使用默认主布局
|
||||
console.log('[Layout Store] Step 4: No layout in localStorage. Applying default.');
|
||||
// +++ 确保默认布局也有 ID (虽然 getDefaultLayout 内部会生成) +++
|
||||
loadedLayout = ensureNodeIds(getDefaultLayout());
|
||||
loadedLayout = normalizeLoadedLayout(getDefaultLayout());
|
||||
console.log('[Layout Store] Step 4: Default layout processed with ensureNodeIds.');
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -284,7 +293,7 @@ function ensureNodeIds(node: LayoutNode | null): LayoutNode | null {
|
||||
// Fallback to default if error and loadedLayout is still null
|
||||
if (!loadedLayout) {
|
||||
console.log('[Layout Store] Step 3/4: Applying default layout due to error.');
|
||||
loadedLayout = ensureNodeIds(getDefaultLayout());
|
||||
loadedLayout = normalizeLoadedLayout(getDefaultLayout());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -326,7 +335,7 @@ function ensureNodeIds(node: LayoutNode | null): LayoutNode | null {
|
||||
// Final check (主要是为了调试,可以简化或移除)
|
||||
if (!layoutTree.value) {
|
||||
console.error('[Layout Store] FATAL: layoutTree is STILL null after all attempts! Applying default as last resort.');
|
||||
layoutTree.value = ensureNodeIds(getDefaultLayout());
|
||||
layoutTree.value = normalizeLoadedLayout(getDefaultLayout());
|
||||
}
|
||||
if (!sidebarPanes.value || !Array.isArray(sidebarPanes.value.left) || !Array.isArray(sidebarPanes.value.right)) {
|
||||
console.warn('[Layout Store] Final Check: Sidebar panes invalid. Applying default.');
|
||||
|
||||
@@ -162,7 +162,7 @@ export const useSettingsStore = defineStore('settings', () => {
|
||||
// Load and parse sidebar pane widths
|
||||
const defaultPaneWidth = '350px';
|
||||
// +++ Ensure PaneName type is available or define it here +++
|
||||
const knownPanes: PaneName[] = ['connections', 'fileManager', 'editor', 'statusMonitor', 'commandHistory', 'quickCommands', 'dockerManager']; // Add all possible sidebar panes
|
||||
const knownPanes: PaneName[] = ['connections', 'fileManager', 'editor', 'workbench', 'statusMonitor', 'commandHistory', 'quickCommands', 'dockerManager']; // Add all possible sidebar panes
|
||||
let loadedWidths: Record<string, string> = {};
|
||||
try {
|
||||
if (settings.value.sidebarPaneWidths) {
|
||||
|
||||
@@ -13,6 +13,8 @@ export interface ServerStatus {
|
||||
swapTotal?: number; // MB
|
||||
netRxRate?: number; // Bytes/sec
|
||||
netTxRate?: number; // Bytes/sec
|
||||
netRxTotalBytes?: number; // Bytes since boot
|
||||
netTxTotalBytes?: number; // Bytes since boot
|
||||
netInterface?: string;
|
||||
osName?: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user