This commit is contained in:
Baobhan Sith
2025-04-17 16:21:32 +08:00
parent 747c9491c4
commit 54ea8f34e3
14 changed files with 1648 additions and 483 deletions
+194 -28
View File
@@ -1,46 +1,212 @@
import { defineStore } from 'pinia';
import { ref } from 'vue';
import { ref, computed, watch, type Ref, type ComputedRef } from 'vue';
// 定义面板名称的类型,方便管理和引用 (添加 quickCommands)
// 定义所有可用面板名称
export type PaneName = 'connections' | 'terminal' | 'commandBar' | 'fileManager' | 'editor' | 'statusMonitor' | 'commandHistory' | 'quickCommands';
// 定义布局节点接口
export interface LayoutNode {
id: string; // 唯一 ID
type: 'pane' | 'container'; // 节点类型:面板或容器
component?: PaneName; // 如果 type 是 'pane',指定要渲染的组件
direction?: 'horizontal' | 'vertical'; // 如果 type 是 'container',指定分割方向
children?: LayoutNode[]; // 如果 type 是 'container',包含子节点数组
size?: number; // 节点在父容器中的大小比例 (例如 20, 50, 30)
}
// 本地存储的 Key
const LAYOUT_STORAGE_KEY = 'nexus_terminal_layout_config';
// 生成唯一 ID 的辅助函数
function generateId(): string {
// 简单实现,实际项目中可能使用更健壮的库如 uuid
return Math.random().toString(36).substring(2, 15);
}
// 定义默认布局结构
const getDefaultLayout = (): LayoutNode => ({
id: generateId(), // 根容器 ID
type: 'container',
direction: 'horizontal', // 主方向:水平分割
children: [
// 左侧边栏:连接列表
{ id: generateId(), type: 'pane', component: 'connections', size: 15 },
// 中间主区域:垂直分割
{
id: generateId(),
type: 'container',
direction: 'vertical',
size: 60, // 中间区域占比
children: [
// 上方:终端
{ id: generateId(), type: 'pane', component: 'terminal', size: 60 },
// 中下方:命令栏 (固定高度,特殊处理或放在终端内?) - 暂时移除,可在配置器中添加
// { id: generateId(), type: 'pane', component: 'commandBar', size: 5 },
// 下方:文件管理器
{ id: generateId(), type: 'pane', component: 'fileManager', size: 40 },
],
},
// 右侧边栏:垂直分割
{
id: generateId(),
type: 'container',
direction: 'vertical',
size: 25, // 右侧区域占比
children: [
// 上方:编辑器
{ id: generateId(), type: 'pane', component: 'editor', size: 60 },
// 下方:状态监视器
{ id: generateId(), type: 'pane', component: 'statusMonitor', size: 40 },
// 可选:命令历史和快捷指令可以放在这里,或者作为可添加的面板
// { id: generateId(), type: 'pane', component: 'commandHistory', size: 20 },
// { id: generateId(), type: 'pane', component: 'quickCommands', size: 20 },
],
},
],
});
// 递归查找布局树中所有使用的面板组件名称
function getUsedPaneNames(node: LayoutNode | null): Set<PaneName> {
const usedNames = new Set<PaneName>();
if (!node) return usedNames;
function traverse(currentNode: LayoutNode) {
if (currentNode.type === 'pane' && currentNode.component) {
usedNames.add(currentNode.component);
} else if (currentNode.type === 'container' && currentNode.children) {
currentNode.children.forEach(traverse);
}
}
traverse(node);
return usedNames;
}
// 定义 Store
export const useLayoutStore = defineStore('layout', () => {
// 使用 ref 创建响应式状态,存储每个面板的可见性 (添加 commandHistory)
const paneVisibility = ref<Record<PaneName, boolean>>({
connections: true,
terminal: true,
commandBar: true,
fileManager: true,
editor: true,
statusMonitor: true,
commandHistory: true,
quickCommands: true, // 默认可见
// --- 状态 ---
// 核心状态:存储当前布局树结构
const layoutTree: Ref<LayoutNode | null> = ref(null);
// 存储所有理论上可用的面板名称
const allPossiblePanes: Ref<PaneName[]> = ref([
'connections', 'terminal', 'commandBar', 'fileManager',
'editor', 'statusMonitor', 'commandHistory', 'quickCommands'
]);
// --- 计算属性 ---
// 计算当前布局中正在使用的面板
const usedPanes: ComputedRef<Set<PaneName>> = computed(() => getUsedPaneNames(layoutTree.value));
// 计算当前未在布局中使用的面板(可用于配置器中添加)
const availablePanes: ComputedRef<PaneName[]> = computed(() => {
const used = usedPanes.value;
return allPossiblePanes.value.filter(pane => !used.has(pane));
});
// Action: 切换指定面板的可见性
function togglePaneVisibility(paneName: PaneName) {
if (paneVisibility.value[paneName] !== undefined) {
paneVisibility.value[paneName] = !paneVisibility.value[paneName];
console.log(`[Layout Store] Toggled visibility for ${paneName}: ${paneVisibility.value[paneName]}`);
} else {
console.warn(`[Layout Store] Attempted to toggle visibility for unknown pane: ${paneName}`);
// --- Actions ---
// 初始化布局:尝试从 localStorage 加载,否则使用默认布局
function initializeLayout() {
try {
const savedLayout = localStorage.getItem(LAYOUT_STORAGE_KEY);
if (savedLayout) {
const parsedLayout = JSON.parse(savedLayout) as LayoutNode;
// 可选:添加验证逻辑确保加载的布局结构有效
layoutTree.value = parsedLayout;
console.log('[Layout Store] 从 localStorage 加载布局成功。');
} else {
layoutTree.value = getDefaultLayout();
console.log('[Layout Store] 未找到保存的布局,使用默认布局。');
}
} catch (error) {
console.error('[Layout Store] 加载或解析布局失败:', error);
layoutTree.value = getDefaultLayout(); // 出错时回退到默认布局
}
}
// Action: 设置指定面板的可见性
function setPaneVisibility(paneName: PaneName, isVisible: boolean) {
if (paneVisibility.value[paneName] !== undefined) {
paneVisibility.value[paneName] = isVisible;
console.log(`[Layout Store] Set visibility for ${paneName} to: ${isVisible}`);
// 更新整个布局树(通常由配置器保存时调用)
function updateLayoutTree(newTree: LayoutNode) {
// 可选:添加验证逻辑
layoutTree.value = newTree;
console.log('[Layout Store] 布局树已更新。');
// 保存将在 watch 中自动触发
}
// 新增:递归查找并更新节点大小
function findAndUpdateNodeSize(node: LayoutNode | null, nodeId: string, childrenSizes: { index: number; size: number }[]): LayoutNode | null {
if (!node) return null;
if (node.id === nodeId && node.type === 'container' && node.children) {
const updatedChildren = [...node.children];
childrenSizes.forEach(({ index, size }) => {
if (updatedChildren[index]) {
updatedChildren[index] = { ...updatedChildren[index], size: size };
}
});
return { ...node, children: updatedChildren };
}
if (node.type === 'container' && node.children) {
const updatedChildren = node.children.map(child => findAndUpdateNodeSize(child, nodeId, childrenSizes));
// 检查是否有子节点被更新
if (updatedChildren.some((child, index) => child !== node.children![index])) {
return { ...node, children: updatedChildren.filter(Boolean) as LayoutNode[] };
}
}
return node; // 未找到或未更新,返回原节点
}
// 新增 Action: 更新特定容器节点的子节点大小
function updateNodeSizes(nodeId: string, childrenSizes: { index: number; size: number }[]) {
console.log(`[Layout Store] 请求更新节点 ${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.warn(`[Layout Store] Attempted to set visibility for unknown pane: ${paneName}`);
console.error(`[Layout Store] 更新节点 ${nodeId} 大小后得到无效的树结构。`);
}
}
// --- 持久化 ---
// 监听 layoutTree 的变化,并自动保存到 localStorage
watch(
layoutTree,
(newTree) => {
if (newTree) {
try {
localStorage.setItem(LAYOUT_STORAGE_KEY, JSON.stringify(newTree));
console.log('[Layout Store] 布局已自动保存到 localStorage。');
} catch (error) {
console.error('[Layout Store] 保存布局到 localStorage 失败:', error);
}
} else {
// 如果布局被清空,也移除本地存储
localStorage.removeItem(LAYOUT_STORAGE_KEY);
console.log('[Layout Store] 布局为空,已从 localStorage 移除。');
}
},
{ deep: true } // 需要深度监听来捕获嵌套结构的变化
);
// --- 初始化 ---
// Store 创建时自动初始化布局
initializeLayout();
// --- 返回 ---
return {
paneVisibility,
togglePaneVisibility,
setPaneVisibility,
layoutTree,
availablePanes, // 供配置器使用
usedPanes, // 可用于调试或内部逻辑
updateLayoutTree,
initializeLayout, // 允许外部重置或重新加载
updateNodeSizes, // *** 新增:暴露更新大小的 action ***
// 暴露 generateId 供配置器使用(如果需要)
generateId,
// 暴露 allPossiblePanes 供配置器显示所有选项
allPossiblePanes,
};
});
+19 -12
View File
@@ -352,18 +352,25 @@ export const useSessionStore = defineStore('session', () => {
// 不需要再调用 activateSession,因为它已经是活动的
} else {
// 如果状态正常,则无需操作
console.log(`[SessionStore] 活动会话 ${existingSessionId} 状态正常,无需操作。`);
}
} else {
// 点击的不是当前活动标签(可能是非活动标签,或根本不存在),总是新建标签页
if (existingSessionId) {
console.log(`[SessionStore] 点击的连接 ${connIdStr} 存在于非活动会话 ${existingSessionId} 中,将打开新会话。`);
} else {
console.log(`[SessionStore] 未找到 ID 为 ${connIdStr} 的现有会话,将打开新会话。`);
}
openNewSession(connIdStr); // 直接调用 openNewSession
}
};
console.log(`[SessionStore] 活动会话 ${existingSessionId} 状态正常,无需操作。`);
}
} else if (existingSessionId && existingSession) {
// 点击的是一个已存在但非活动的会话
console.log(`[SessionStore] 点击的连接 ${connIdStr} 存在于非活动会话 ${existingSessionId} 中,将激活它。`);
activateSession(existingSessionId);
// 激活后检查状态并尝试重连 (如果需要)
const currentStatus = existingSession.wsManager.connectionStatus.value;
if (currentStatus === 'disconnected' || currentStatus === 'error') {
console.log(`[SessionStore] 激活的会话 ${existingSessionId} 已断开或出错,尝试重连...`);
const wsUrl = `ws://${window.location.hostname}:3001`; // TODO: 从配置获取 URL
existingSession.wsManager.connect(wsUrl);
}
} else {
// 点击的连接没有对应的会话,创建新会话
console.log(`[SessionStore] 未找到 ID 为 ${connIdStr} 的现有会话,将打开新会话。`);
openNewSession(connIdStr);
}
};
/**
* 处理连接列表的中键点击(总是打开新会话)