update
This commit is contained in:
Generated
+20
-1
@@ -12,7 +12,8 @@
|
|||||||
"packages/*"
|
"packages/*"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"pinia-plugin-persistedstate": "^4.2.0"
|
"pinia-plugin-persistedstate": "^4.2.0",
|
||||||
|
"vuedraggable": "^4.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/qrcode": "^1.5.5",
|
"@types/qrcode": "^1.5.5",
|
||||||
@@ -5154,6 +5155,12 @@
|
|||||||
"node": ">= 10"
|
"node": ">= 10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/sortablejs": {
|
||||||
|
"version": "1.14.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.14.0.tgz",
|
||||||
|
"integrity": "sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/source-map": {
|
"node_modules/source-map": {
|
||||||
"version": "0.6.1",
|
"version": "0.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||||
@@ -6092,6 +6099,18 @@
|
|||||||
"typescript": "*"
|
"typescript": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vuedraggable": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"sortablejs": "1.14.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"vue": "^3.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/webidl-conversions": {
|
"node_modules/webidl-conversions": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||||
|
|||||||
+2
-1
@@ -25,7 +25,8 @@
|
|||||||
"homepage": "https://github.com/Heavrnl/nexus-terminal#readme",
|
"homepage": "https://github.com/Heavrnl/nexus-terminal#readme",
|
||||||
"description": "",
|
"description": "",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"pinia-plugin-persistedstate": "^4.2.0"
|
"pinia-plugin-persistedstate": "^4.2.0",
|
||||||
|
"vuedraggable": "^4.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/qrcode": "^1.5.5",
|
"@types/qrcode": "^1.5.5",
|
||||||
|
|||||||
@@ -195,6 +195,10 @@ export const initializeWebSocket = (server: http.Server, sessionParser: RequestH
|
|||||||
|
|
||||||
// 5. 设置 Shell 事件转发
|
// 5. 设置 Shell 事件转发
|
||||||
shellStream.on('data', (data: Buffer) => {
|
shellStream.on('data', (data: Buffer) => {
|
||||||
|
// --- 添加日志:打印收到的原始数据 ---
|
||||||
|
console.log(`SSH Data (会话: ${newSessionId}, 原始): `, data.toString()); // 添加原始数据日志 (尝试 utf8)
|
||||||
|
console.log(`SSH Data (会话: ${newSessionId}, Hex): `, data.toString('hex')); // 添加 Hex 日志
|
||||||
|
// ------------------------------------
|
||||||
if (ws.readyState === WebSocket.OPEN) {
|
if (ws.readyState === WebSocket.OPEN) {
|
||||||
ws.send(JSON.stringify({ type: 'ssh:output', payload: data.toString('base64'), encoding: 'base64' }));
|
ws.send(JSON.stringify({ type: 'ssh:output', payload: data.toString('base64'), encoding: 'base64' }));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,465 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, watch, type Ref } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useLayoutStore, type LayoutNode, type PaneName } from '../stores/layout.store';
|
||||||
|
import draggable from 'vuedraggable';
|
||||||
|
import LayoutNodeEditor from './LayoutNodeEditor.vue'; // *** 导入节点编辑器 ***
|
||||||
|
|
||||||
|
// --- Props ---
|
||||||
|
const props = defineProps({
|
||||||
|
isVisible: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Emits ---
|
||||||
|
const emit = defineEmits(['close']);
|
||||||
|
|
||||||
|
// --- Setup ---
|
||||||
|
const { t } = useI18n();
|
||||||
|
const layoutStore = useLayoutStore();
|
||||||
|
|
||||||
|
// --- State ---
|
||||||
|
// 创建布局树的本地副本,以便在不直接修改 store 的情况下进行编辑
|
||||||
|
const localLayoutTree: Ref<LayoutNode | null> = ref(null);
|
||||||
|
// 标记是否有更改未保存
|
||||||
|
const hasChanges = ref(false);
|
||||||
|
|
||||||
|
// --- Watchers ---
|
||||||
|
// 当弹窗可见时,从 store 加载当前布局到本地副本
|
||||||
|
watch(() => props.isVisible, (newValue) => {
|
||||||
|
if (newValue && layoutStore.layoutTree) {
|
||||||
|
// 深拷贝以避免直接修改 store 状态
|
||||||
|
localLayoutTree.value = JSON.parse(JSON.stringify(layoutStore.layoutTree));
|
||||||
|
hasChanges.value = false; // 重置更改状态
|
||||||
|
console.log('[LayoutConfigurator] 弹窗打开,已加载当前布局到本地副本。');
|
||||||
|
} else {
|
||||||
|
localLayoutTree.value = null; // 关闭时清空本地副本
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听本地布局树的变化,标记有未保存更改
|
||||||
|
watch(localLayoutTree, (newValue, oldValue) => {
|
||||||
|
// 确保不是初始化加载触发的 watch
|
||||||
|
if (oldValue !== null && props.isVisible) {
|
||||||
|
hasChanges.value = true;
|
||||||
|
console.log('[LayoutConfigurator] 本地布局已更改。');
|
||||||
|
}
|
||||||
|
}, { deep: true });
|
||||||
|
|
||||||
|
// --- Helper Function for Local Tree ---
|
||||||
|
// 递归查找本地布局树中所有使用的面板组件名称
|
||||||
|
function getLocalUsedPaneNames(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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// --- Computed ---
|
||||||
|
// const availablePanes = computed(() => layoutStore.availablePanes); // 旧的,基于 store
|
||||||
|
const allPossiblePanes = computed(() => layoutStore.allPossiblePanes); // 获取所有可能的面板
|
||||||
|
|
||||||
|
// *** 新增:计算当前配置器预览中可用的面板 ***
|
||||||
|
const configuratorAvailablePanes = computed(() => {
|
||||||
|
const localUsed = getLocalUsedPaneNames(localLayoutTree.value);
|
||||||
|
return allPossiblePanes.value.filter(pane => !localUsed.has(pane));
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// 将 PaneName 映射到用户友好的中文标签
|
||||||
|
const paneLabels = computed(() => ({
|
||||||
|
connections: t('layout.pane.connections', '连接列表'),
|
||||||
|
terminal: t('layout.pane.terminal', '终端'),
|
||||||
|
commandBar: t('layout.pane.commandBar', '命令栏'),
|
||||||
|
fileManager: t('layout.pane.fileManager', '文件管理器'),
|
||||||
|
editor: t('layout.pane.editor', '编辑器'),
|
||||||
|
statusMonitor: t('layout.pane.statusMonitor', '状态监视器'),
|
||||||
|
commandHistory: t('layout.pane.commandHistory', '命令历史'),
|
||||||
|
quickCommands: t('layout.pane.quickCommands', '快捷指令'),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// --- Methods ---
|
||||||
|
const closeDialog = () => {
|
||||||
|
if (hasChanges.value) {
|
||||||
|
if (confirm(t('layoutConfigurator.confirmClose', '有未保存的更改,确定要关闭吗?'))) {
|
||||||
|
emit('close');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
emit('close');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveLayout = () => {
|
||||||
|
if (localLayoutTree.value) {
|
||||||
|
layoutStore.updateLayoutTree(localLayoutTree.value);
|
||||||
|
hasChanges.value = false;
|
||||||
|
console.log('[LayoutConfigurator] 布局已保存到 Store。');
|
||||||
|
emit('close'); // 保存后关闭
|
||||||
|
} else {
|
||||||
|
console.error('[LayoutConfigurator] 无法保存,本地布局树为空。');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetToDefault = () => {
|
||||||
|
if (confirm(t('layoutConfigurator.confirmReset', '确定要恢复默认布局吗?当前更改将丢失。'))) {
|
||||||
|
// 重新调用 store 的初始化方法来获取默认布局
|
||||||
|
layoutStore.initializeLayout();
|
||||||
|
// 重新加载到本地副本
|
||||||
|
if (layoutStore.layoutTree) {
|
||||||
|
localLayoutTree.value = JSON.parse(JSON.stringify(layoutStore.layoutTree));
|
||||||
|
hasChanges.value = true; // 标记为有更改,因为是重置操作
|
||||||
|
console.log('[LayoutConfigurator] 已重置为默认布局。');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Drag & Drop Methods ---
|
||||||
|
// 克隆函数:当从可用列表拖拽时,创建新的 LayoutNode 对象
|
||||||
|
const clonePane = (paneName: PaneName): LayoutNode => {
|
||||||
|
console.log(`[LayoutConfigurator] 克隆面板: ${paneName}`);
|
||||||
|
return {
|
||||||
|
id: layoutStore.generateId(), // 使用 store 中的函数生成新 ID
|
||||||
|
type: 'pane',
|
||||||
|
component: paneName,
|
||||||
|
size: 50, // 默认大小,可以后续调整
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// 移除旧的 handleDragStart
|
||||||
|
// const handleDragStart = (event: DragEvent, paneName: PaneName) => { ... }
|
||||||
|
|
||||||
|
// 移除旧的预览区域 drop/dragover 处理,由 LayoutNodeEditor 内部处理
|
||||||
|
// const handleDropOnPreview = (event: DragEvent) => { ... };
|
||||||
|
// const handleDragOverPreview = (event: DragEvent) => { ... };
|
||||||
|
|
||||||
|
// *** 新增:处理来自 LayoutNodeEditor 的更新事件 ***
|
||||||
|
const handleNodeUpdate = (updatedNode: LayoutNode) => {
|
||||||
|
// 因为 LayoutNodeEditor 是直接操作 localLayoutTree 的副本,
|
||||||
|
// 理论上 v-model 绑定应该能处理更新。
|
||||||
|
// 但为了明确和处理可能的深层更新问题,我们直接替换根节点。
|
||||||
|
// 注意:这假设 LayoutNodeEditor 只会 emit 根节点的更新事件,
|
||||||
|
// 或者我们需要一个更复杂的查找和替换逻辑。
|
||||||
|
// 简单的做法是,只要有更新,就认为整个 localLayoutTree 可能变了。
|
||||||
|
// (vuedraggable 的 v-model 应该能处理大部分情况)
|
||||||
|
// 暂时只打印日志,依赖 v-model 的更新
|
||||||
|
console.log('[LayoutConfigurator] Received node update:', updatedNode);
|
||||||
|
// 如果 v-model 更新不完全,可能需要手动更新:
|
||||||
|
localLayoutTree.value = updatedNode; // 强制更新整个树
|
||||||
|
};
|
||||||
|
|
||||||
|
// *** 新增:处理来自 LayoutNodeEditor 的移除事件 ***
|
||||||
|
// 递归查找并移除指定索引的节点
|
||||||
|
function findAndRemoveNode(node: LayoutNode | null, parentNodeId: string | undefined, nodeIndex: number): LayoutNode | null {
|
||||||
|
if (!node) return null;
|
||||||
|
|
||||||
|
// 如果当前节点是目标节点的父节点
|
||||||
|
if (node.id === parentNodeId && node.type === 'container' && node.children && node.children[nodeIndex]) {
|
||||||
|
const updatedChildren = [...node.children];
|
||||||
|
updatedChildren.splice(nodeIndex, 1);
|
||||||
|
console.log(`[LayoutConfigurator] Removed node at index ${nodeIndex} from parent ${parentNodeId}`);
|
||||||
|
// 如果移除后容器为空,可以选择移除容器自身,这里暂时保留空容器
|
||||||
|
return { ...node, children: updatedChildren };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 递归查找子节点
|
||||||
|
if (node.type === 'container' && node.children) {
|
||||||
|
const updatedChildren = node.children.map(child => findAndRemoveNode(child, parentNodeId, nodeIndex));
|
||||||
|
// 检查是否有子节点被更新(即目标节点在更深层被找到并移除)
|
||||||
|
if (updatedChildren.some((child, index) => child !== node.children![index])) {
|
||||||
|
return { ...node, children: updatedChildren.filter(Boolean) as LayoutNode[] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return node; // 未找到或未修改,返回原节点
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleNodeRemove = (payload: { parentNodeId: string | undefined; nodeIndex: number }) => {
|
||||||
|
console.log('[LayoutConfigurator] Received node remove request:', payload);
|
||||||
|
if (payload.parentNodeId === undefined && payload.nodeIndex === 0) {
|
||||||
|
// 尝试移除根节点,不允许或清空布局
|
||||||
|
if (confirm('确定要清空整个布局吗?')) {
|
||||||
|
localLayoutTree.value = null; // 或者设置为空容器
|
||||||
|
}
|
||||||
|
} else if (payload.parentNodeId) {
|
||||||
|
localLayoutTree.value = findAndRemoveNode(localLayoutTree.value, payload.parentNodeId, payload.nodeIndex);
|
||||||
|
} else {
|
||||||
|
console.warn('[LayoutConfigurator] Invalid remove payload:', payload);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="isVisible" class="layout-configurator-overlay" @click.self="closeDialog">
|
||||||
|
<div class="layout-configurator-dialog">
|
||||||
|
<header class="dialog-header">
|
||||||
|
<h2>{{ t('layoutConfigurator.title', '配置工作区布局') }}</h2>
|
||||||
|
<button class="close-button" @click="closeDialog" :title="t('common.close', '关闭')">×</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="dialog-content">
|
||||||
|
<section class="available-panes-section">
|
||||||
|
<h3>{{ t('layoutConfigurator.availablePanes', '可用面板') }}</h3>
|
||||||
|
<!-- *** 使用 draggable 包裹列表 *** -->
|
||||||
|
<draggable
|
||||||
|
:list="configuratorAvailablePanes"
|
||||||
|
tag="ul"
|
||||||
|
class="available-panes-list"
|
||||||
|
:item-key="(element: PaneName) => element"
|
||||||
|
:group="{ name: 'layout-items', pull: 'clone', put: false }"
|
||||||
|
:sort="false"
|
||||||
|
:clone="clonePane"
|
||||||
|
>
|
||||||
|
<template #item="{ element }: { element: PaneName }">
|
||||||
|
<li
|
||||||
|
class="available-pane-item"
|
||||||
|
>
|
||||||
|
<i class="fas fa-grip-vertical drag-handle"></i>
|
||||||
|
{{ paneLabels[element] || element }}
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<li v-if="configuratorAvailablePanes.length === 0" class="no-available-panes"> <!-- *** 使用新的计算属性 *** -->
|
||||||
|
{{ t('layoutConfigurator.noAvailablePanes', '所有面板都已在布局中') }}
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
</draggable>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section
|
||||||
|
class="layout-preview-section"
|
||||||
|
>
|
||||||
|
<h3>{{ t('layoutConfigurator.layoutPreview', '布局预览(拖拽到此处)') }}</h3>
|
||||||
|
<div class="preview-area">
|
||||||
|
<!-- *** 使用 LayoutNodeEditor 渲染预览 *** -->
|
||||||
|
<LayoutNodeEditor
|
||||||
|
v-if="localLayoutTree"
|
||||||
|
:node="localLayoutTree"
|
||||||
|
:parent-node="null"
|
||||||
|
:node-index="0"
|
||||||
|
@update:node="handleNodeUpdate"
|
||||||
|
@removeNode="handleNodeRemove"
|
||||||
|
/>
|
||||||
|
<p v-else style="text-align: center; color: #aaa; margin-top: 50px;">
|
||||||
|
{{ t('layoutConfigurator.emptyLayout', '布局为空,请从左侧拖拽面板或添加容器。') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="preview-actions">
|
||||||
|
<button @click="resetToDefault" class="button-secondary">
|
||||||
|
{{ t('layoutConfigurator.resetDefault', '恢复默认') }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.layout-configurator-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.6);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 1000; /* 低于 TerminalTabBar 的弹出窗口?可能需要调整 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-configurator-dialog {
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.3);
|
||||||
|
width: 80vw;
|
||||||
|
max-width: 900px; /* 增加最大宽度 */
|
||||||
|
max-height: 85vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden; /* 防止内容溢出 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #aaa;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.close-button:hover {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-content {
|
||||||
|
flex-grow: 1;
|
||||||
|
padding: 1.5rem;
|
||||||
|
overflow-y: auto; /* 允许内容区滚动 */
|
||||||
|
display: flex; /* 左右布局 */
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.available-panes-section {
|
||||||
|
flex: 1; /* 占据一部分空间 */
|
||||||
|
min-width: 200px;
|
||||||
|
border-right: 1px solid #eee;
|
||||||
|
padding-right: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-preview-section {
|
||||||
|
flex: 2; /* 占据更多空间 */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
.available-panes-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.available-pane-item {
|
||||||
|
padding: 0.6rem 0.8rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: grab;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
.available-pane-item:hover {
|
||||||
|
background-color: #e9ecef;
|
||||||
|
}
|
||||||
|
.available-pane-item:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
background-color: #ced4da;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-handle {
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
color: #adb5bd;
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
.available-pane-item:active .drag-handle {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-available-panes {
|
||||||
|
color: #6c757d;
|
||||||
|
font-style: italic;
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-area {
|
||||||
|
flex-grow: 1;
|
||||||
|
border: 2px dashed #ced4da;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 1rem;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
min-height: 300px; /* 保证预览区有一定高度 */
|
||||||
|
display: flex; /* 用于内部占位符居中 */
|
||||||
|
flex-direction: column;
|
||||||
|
/* justify-content: center; */ /* 移除,让内容从顶部开始 */
|
||||||
|
/* align-items: center; */ /* 移除 */
|
||||||
|
overflow: auto; /* 如果预览内容复杂,允许滚动 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-actions {
|
||||||
|
margin-top: 1rem;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-footer {
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 0.8rem;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 通用按钮样式 */
|
||||||
|
.button-primary,
|
||||||
|
.button-secondary {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
transition: background-color 0.2s ease, opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-primary {
|
||||||
|
background-color: #007bff;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.button-primary:hover {
|
||||||
|
background-color: #0056b3;
|
||||||
|
}
|
||||||
|
.button-primary:disabled {
|
||||||
|
background-color: #6c757d;
|
||||||
|
opacity: 0.7;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-secondary {
|
||||||
|
background-color: #e9ecef;
|
||||||
|
color: #343a40;
|
||||||
|
border: 1px solid #ced4da;
|
||||||
|
}
|
||||||
|
.button-secondary:hover {
|
||||||
|
background-color: #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,353 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, type PropType } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import draggable from 'vuedraggable';
|
||||||
|
import { useLayoutStore, type LayoutNode, type PaneName } from '../stores/layout.store';
|
||||||
|
|
||||||
|
// --- Props ---
|
||||||
|
const props = defineProps({
|
||||||
|
node: {
|
||||||
|
type: Object as PropType<LayoutNode>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
// 接收父节点信息,用于删除等操作
|
||||||
|
parentNode: {
|
||||||
|
type: Object as PropType<LayoutNode | null>,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
// 接收当前节点在父节点 children 中的索引
|
||||||
|
nodeIndex: {
|
||||||
|
type: Number,
|
||||||
|
default: -1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Emits ---
|
||||||
|
// 定义需要向上层(LayoutConfigurator)传递的事件
|
||||||
|
const emit = defineEmits(['update:node', 'removeNode']);
|
||||||
|
|
||||||
|
// --- Setup ---
|
||||||
|
const { t } = useI18n();
|
||||||
|
const layoutStore = useLayoutStore();
|
||||||
|
|
||||||
|
// --- Computed ---
|
||||||
|
// 获取面板标签
|
||||||
|
const paneLabels = computed(() => ({
|
||||||
|
connections: t('layout.pane.connections', '连接列表'),
|
||||||
|
terminal: t('layout.pane.terminal', '终端'),
|
||||||
|
commandBar: t('layout.pane.commandBar', '命令栏'),
|
||||||
|
fileManager: t('layout.pane.fileManager', '文件管理器'),
|
||||||
|
editor: t('layout.pane.editor', '编辑器'),
|
||||||
|
statusMonitor: t('layout.pane.statusMonitor', '状态监视器'),
|
||||||
|
commandHistory: t('layout.pane.commandHistory', '命令历史'),
|
||||||
|
quickCommands: t('layout.pane.quickCommands', '快捷指令'),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 计算当前节点的子节点列表(用于 v-model)
|
||||||
|
// 注意:直接修改 props 是不允许的,vuedraggable 需要一个可写的 list
|
||||||
|
// 我们通过 emit 事件来通知父组件更新
|
||||||
|
const childrenList = computed({
|
||||||
|
get: () => props.node.children || [],
|
||||||
|
set: (newChildren) => {
|
||||||
|
// 当 vuedraggable 修改列表时,它应该直接修改绑定的列表 (props.node.children)
|
||||||
|
// 移除下面的 emit 调用,因为它导致了事件风暴
|
||||||
|
// emit('update:node', { ...props.node, children: newChildren });
|
||||||
|
// 添加日志以确认 setter 被调用,并依赖 vuedraggable 的直接修改
|
||||||
|
console.log('[LayoutNodeEditor] childrenList setter called, relying on v-model/vuedraggable mutation.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Methods ---
|
||||||
|
// 添加水平分割容器
|
||||||
|
const addHorizontalContainer = () => {
|
||||||
|
const newNode: LayoutNode = {
|
||||||
|
id: layoutStore.generateId(),
|
||||||
|
type: 'container',
|
||||||
|
direction: 'horizontal',
|
||||||
|
children: [], // 新容器初始为空
|
||||||
|
size: 50, // 默认大小
|
||||||
|
};
|
||||||
|
const updatedChildren = [...(props.node.children || []), newNode];
|
||||||
|
emit('update:node', { ...props.node, children: updatedChildren });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加垂直分割容器
|
||||||
|
const addVerticalContainer = () => {
|
||||||
|
const newNode: LayoutNode = {
|
||||||
|
id: layoutStore.generateId(),
|
||||||
|
type: 'container',
|
||||||
|
direction: 'vertical',
|
||||||
|
children: [],
|
||||||
|
size: 50,
|
||||||
|
};
|
||||||
|
const updatedChildren = [...(props.node.children || []), newNode];
|
||||||
|
emit('update:node', { ...props.node, children: updatedChildren });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 移除当前节点
|
||||||
|
const removeSelf = () => {
|
||||||
|
emit('removeNode', { parentNodeId: props.parentNode?.id, nodeIndex: props.nodeIndex });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 切换容器方向
|
||||||
|
const toggleDirection = () => {
|
||||||
|
if (props.node.type === 'container') {
|
||||||
|
const newDirection = props.node.direction === 'horizontal' ? 'vertical' : 'horizontal';
|
||||||
|
emit('update:node', { ...props.node, direction: newDirection });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理子节点更新事件(由递归调用发出)
|
||||||
|
const handleChildUpdate = (updatedChildNode: LayoutNode, index: number) => {
|
||||||
|
if (props.node.children) {
|
||||||
|
const newChildren = [...props.node.children];
|
||||||
|
newChildren[index] = updatedChildNode;
|
||||||
|
emit('update:node', { ...props.node, children: newChildren });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理子节点移除事件
|
||||||
|
const handleChildRemove = (payload: { parentNodeId: string | undefined; nodeIndex: number }) => {
|
||||||
|
// 总是将移除事件向上传递,让顶层 LayoutConfigurator 处理
|
||||||
|
console.log(`[LayoutNodeEditor ${props.node.id}] Relaying removeNode event upwards:`, payload); // 添加日志
|
||||||
|
emit('removeNode', payload);
|
||||||
|
|
||||||
|
/* 移除旧逻辑:
|
||||||
|
// 如果移除的是当前节点的直接子节点
|
||||||
|
if (payload.parentNodeId === props.node.id && props.node.children) {
|
||||||
|
const newChildren = [...props.node.children];
|
||||||
|
newChildren.splice(payload.nodeIndex, 1);
|
||||||
|
// 如果容器变空,可以选择移除容器自身或保留空容器
|
||||||
|
// 这里选择保留空容器,让用户手动删除
|
||||||
|
// 问题:这里 emit update:node,但实际移除逻辑在 LayoutConfigurator
|
||||||
|
emit('update:node', { ...props.node, children: newChildren });
|
||||||
|
} else {
|
||||||
|
// 如果不是直接子节点,继续向上传递事件
|
||||||
|
emit('removeNode', payload);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
};
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="layout-node-editor"
|
||||||
|
:class="[`node-type-${node.type}`, node.direction ? `direction-${node.direction}` : '']"
|
||||||
|
:data-node-id="node.id"
|
||||||
|
>
|
||||||
|
<!-- 节点控制栏 -->
|
||||||
|
<div class="node-controls">
|
||||||
|
<span class="node-info">
|
||||||
|
{{ node.type === 'pane' ? (paneLabels[node.component!] || node.component) : `容器 (${node.direction === 'horizontal' ? '水平' : '垂直'})` }}
|
||||||
|
</span>
|
||||||
|
<div class="node-actions">
|
||||||
|
<button v-if="node.type === 'container'" @click="toggleDirection" title="切换方向" class="action-button">
|
||||||
|
<i class="fas fa-sync-alt"></i>
|
||||||
|
</button>
|
||||||
|
<button v-if="node.type === 'container'" @click="addHorizontalContainer" title="添加水平容器" class="action-button">
|
||||||
|
<i class="fas fa-columns"></i> H
|
||||||
|
</button>
|
||||||
|
<button v-if="node.type === 'container'" @click="addVerticalContainer" title="添加垂直容器" class="action-button">
|
||||||
|
<i class="fas fa-bars"></i> V
|
||||||
|
</button>
|
||||||
|
<button @click="removeSelf" title="移除此节点" class="action-button remove-button">
|
||||||
|
<i class="fas fa-trash-alt"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 如果是容器节点,使用 draggable 渲染子节点 -->
|
||||||
|
<draggable
|
||||||
|
v-if="node.type === 'container'"
|
||||||
|
:list="childrenList"
|
||||||
|
@update:list="childrenList = $event"
|
||||||
|
tag="div"
|
||||||
|
class="node-children-container"
|
||||||
|
:class="[`children-direction-${node.direction}`]"
|
||||||
|
item-key="id"
|
||||||
|
group="layout-items"
|
||||||
|
handle=".drag-handle-node"
|
||||||
|
|
||||||
|
>
|
||||||
|
<template #item="{ element: childNode, index }">
|
||||||
|
<div class="child-node-wrapper" :key="childNode.id">
|
||||||
|
<i class="fas fa-grip-vertical drag-handle-node" title="拖拽调整顺序或移动"></i>
|
||||||
|
|
||||||
|
<LayoutNodeEditor
|
||||||
|
:node="childNode"
|
||||||
|
:parent-node="node"
|
||||||
|
:node-index="index"
|
||||||
|
@update:node="handleChildUpdate($event, index)"
|
||||||
|
@removeNode="handleChildRemove"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<!-- 容器为空时的占位符 -->
|
||||||
|
<template #footer>
|
||||||
|
<div v-if="!childrenList || childrenList.length === 0" class="empty-container-placeholder">
|
||||||
|
将面板或容器拖拽到此处
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</draggable>
|
||||||
|
|
||||||
|
<!-- 如果是面板节点,只显示信息(因为内容在主视图渲染) -->
|
||||||
|
<div v-else class="pane-node-content">
|
||||||
|
<!-- 面板节点在配置器中通常不需要显示内容 -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.layout-node-editor {
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
margin: 5px;
|
||||||
|
padding: 5px;
|
||||||
|
position: relative;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
min-height: 60px; /* 保证有最小高度以便拖放 */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-type-container {
|
||||||
|
background-color: #eef; /* 容器用淡蓝色背景 */
|
||||||
|
}
|
||||||
|
.node-type-pane {
|
||||||
|
background-color: #efe; /* 面板用淡绿色背景 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-controls {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #eee;
|
||||||
|
padding: 3px 5px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-size: 0.8em;
|
||||||
|
min-height: 24px; /* 确保控制栏有高度 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-info {
|
||||||
|
font-weight: bold;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button {
|
||||||
|
background: none;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 1px 4px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.action-button:hover {
|
||||||
|
background-color: #ddd;
|
||||||
|
}
|
||||||
|
.remove-button {
|
||||||
|
color: #dc3545;
|
||||||
|
border-color: #dc3545;
|
||||||
|
}
|
||||||
|
.remove-button:hover {
|
||||||
|
background-color: #dc3545;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-children-container {
|
||||||
|
flex-grow: 1;
|
||||||
|
padding: 5px;
|
||||||
|
border: 1px dashed #bbb; /* 容器内部边框 */
|
||||||
|
min-height: 40px; /* 容器拖放区域最小高度 */
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.children-direction-horizontal {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
.children-direction-vertical {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.child-node-wrapper {
|
||||||
|
border: 1px solid transparent; /* 占位,防止抖动 */
|
||||||
|
position: relative; /* 用于定位拖拽句柄 */
|
||||||
|
display: flex; /* 让句柄和内容并排 */
|
||||||
|
align-items: stretch; /* 让子项高度一致 */
|
||||||
|
}
|
||||||
|
/* 根据方向调整子项的 flex 属性 */
|
||||||
|
.children-direction-horizontal > .child-node-wrapper {
|
||||||
|
flex: 1 1 auto; /* 水平方向允许伸缩 */
|
||||||
|
flex-direction: column; /* 内部还是列方向 */
|
||||||
|
}
|
||||||
|
.children-direction-vertical > .child-node-wrapper {
|
||||||
|
width: 100%; /* 垂直方向占满宽度 */
|
||||||
|
flex-direction: row; /* 内部行方向 */
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.drag-handle-node {
|
||||||
|
cursor: grab;
|
||||||
|
color: #aaa;
|
||||||
|
padding: 5px 3px; /* 增加点击区域 */
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
border-right: 1px solid #ddd; /* 垂直句柄 */
|
||||||
|
}
|
||||||
|
.children-direction-vertical > .child-node-wrapper > .drag-handle-node {
|
||||||
|
border-right: none;
|
||||||
|
border-bottom: 1px solid #ddd; /* 水平句柄 */
|
||||||
|
writing-mode: vertical-rl; /* 可选:旋转图标 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.child-node-wrapper > .layout-node-editor {
|
||||||
|
flex-grow: 1; /* 让递归组件填充剩余空间 */
|
||||||
|
margin: 0; /* 移除递归组件的外边距 */
|
||||||
|
border: none; /* 移除递归组件的边框 */
|
||||||
|
padding: 0; /* 移除递归组件的内边距 */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.pane-node-content {
|
||||||
|
/* 面板节点在配置器中通常是空的 */
|
||||||
|
min-height: 30px; /* 给面板一个最小高度 */
|
||||||
|
text-align: center;
|
||||||
|
color: #aaa;
|
||||||
|
font-size: 0.8em;
|
||||||
|
padding-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-container-placeholder {
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
color: #ccc;
|
||||||
|
font-size: 0.9em;
|
||||||
|
min-height: 30px;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px dashed #ddd;
|
||||||
|
margin: 5px; /* 与 layout-node-editor 的 margin 匹配 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* vuedraggable 拖拽时的样式 */
|
||||||
|
.sortable-ghost {
|
||||||
|
opacity: 0.4;
|
||||||
|
background-color: #cceeff !important; /* 拖拽占位符样式 */
|
||||||
|
border: 1px dashed #007bff;
|
||||||
|
}
|
||||||
|
.sortable-chosen {
|
||||||
|
/* 被选中的元素样式 */
|
||||||
|
}
|
||||||
|
.sortable-drag {
|
||||||
|
/* 正在拖拽的元素样式 */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,350 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, defineAsyncComponent, type PropType, type Component } from 'vue';
|
||||||
|
import { Splitpanes, Pane } from 'splitpanes';
|
||||||
|
import 'splitpanes/dist/splitpanes.css'; // 引入 splitpanes 样式
|
||||||
|
import { useLayoutStore, type LayoutNode, type PaneName } from '../stores/layout.store';
|
||||||
|
import { useSessionStore } from '../stores/session.store';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
import { defineEmits } from 'vue'; // *** 新增:导入 defineEmits ***
|
||||||
|
|
||||||
|
// --- Props ---
|
||||||
|
const props = defineProps({
|
||||||
|
layoutNode: {
|
||||||
|
type: Object as PropType<LayoutNode>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
// 传递必要的上下文数据,避免在递归中重复获取
|
||||||
|
activeSessionId: {
|
||||||
|
type: String as PropType<string | null>,
|
||||||
|
required: false, // 改为非必需
|
||||||
|
default: null, // 提供默认值 null
|
||||||
|
},
|
||||||
|
// *** 新增:接收编辑器相关 props ***
|
||||||
|
editorTabs: {
|
||||||
|
type: Array as PropType<any[]>, // 使用 any[] 简化,或导入具体类型
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
activeEditorTabId: {
|
||||||
|
type: String as PropType<string | null>,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Emits ---
|
||||||
|
// *** 新增:声明所有需要转发的事件 (使用对象语法) ***
|
||||||
|
const emit = defineEmits({
|
||||||
|
'sendCommand': null, // (command: string) - No validation needed here for now
|
||||||
|
'terminalInput': null, // (payload: { sessionId: string; data: string })
|
||||||
|
'terminalResize': null, // (payload: { sessionId: string; dims: { cols: number; rows: number } })
|
||||||
|
'closeEditorTab': null, // (tabId: string)
|
||||||
|
'activateEditorTab': null, // (tabId: string)
|
||||||
|
'updateEditorContent': null, // (payload: { tabId: string; content: string })
|
||||||
|
'saveEditorTab': null, // (tabId: string)
|
||||||
|
'connect-request': null, // (id: number)
|
||||||
|
'open-new-session': null, // (id: number)
|
||||||
|
'request-add-connection': null, // ()
|
||||||
|
'request-edit-connection': null, // (conn: any)
|
||||||
|
// *** 修正:更新 terminal-ready 事件的 payload 类型 ***
|
||||||
|
'terminal-ready': (payload: { sessionId: string; terminal: any }) => // 使用 any 简化类型检查,或导入 Terminal
|
||||||
|
typeof payload === 'object' && typeof payload.sessionId === 'string' && typeof payload.terminal === 'object'
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Setup ---
|
||||||
|
const layoutStore = useLayoutStore();
|
||||||
|
const sessionStore = useSessionStore();
|
||||||
|
const { activeSession } = storeToRefs(sessionStore);
|
||||||
|
|
||||||
|
// --- Component Mapping ---
|
||||||
|
// 使用 defineAsyncComponent 优化加载,并映射 PaneName 到实际组件
|
||||||
|
const componentMap: Record<PaneName, Component> = {
|
||||||
|
connections: defineAsyncComponent(() => import('./WorkspaceConnectionList.vue')),
|
||||||
|
terminal: defineAsyncComponent(() => import('./Terminal.vue')),
|
||||||
|
commandBar: defineAsyncComponent(() => import('./CommandInputBar.vue')),
|
||||||
|
fileManager: defineAsyncComponent(() => import('./FileManager.vue')),
|
||||||
|
editor: defineAsyncComponent(() => import('./FileEditorContainer.vue')),
|
||||||
|
statusMonitor: defineAsyncComponent(() => import('./StatusMonitor.vue')),
|
||||||
|
commandHistory: defineAsyncComponent(() => import('../views/CommandHistoryView.vue')),
|
||||||
|
quickCommands: defineAsyncComponent(() => import('../views/QuickCommandsView.vue')),
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Computed ---
|
||||||
|
// 获取当前节点对应的组件实例
|
||||||
|
const currentComponent = computed(() => {
|
||||||
|
if (props.layoutNode.type === 'pane' && props.layoutNode.component) {
|
||||||
|
return componentMap[props.layoutNode.component] || null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 为特定组件计算需要传递的 Props
|
||||||
|
// 注意:这是一个简化示例,实际可能需要更复杂的逻辑来传递正确的 props
|
||||||
|
// 例如,Terminal, FileManager, StatusMonitor 需要当前 activeSession 的数据
|
||||||
|
// Editor 需要根据共享模式决定数据来源
|
||||||
|
const componentProps = computed(() => {
|
||||||
|
const componentName = props.layoutNode.component;
|
||||||
|
const currentActiveSession = activeSession.value; // 获取当前活动会话
|
||||||
|
|
||||||
|
if (!componentName) return {};
|
||||||
|
|
||||||
|
switch (componentName) {
|
||||||
|
// --- 为需要转发事件的组件添加事件绑定 ---
|
||||||
|
case 'terminal':
|
||||||
|
// Terminal 需要 sessionId, isActive, 并转发 ready, data, resize 事件
|
||||||
|
// 确保 sessionId 始终为字符串
|
||||||
|
return {
|
||||||
|
sessionId: props.activeSessionId ?? '', // 如果 activeSessionId 为 null,则传递空字符串
|
||||||
|
isActive: true,
|
||||||
|
// *** 添加日志并修正事件处理 ***
|
||||||
|
onReady: (payload: { sessionId: string; terminal: any }) => {
|
||||||
|
console.log(`[LayoutRenderer ${props.activeSessionId}] 收到内部 Terminal 的 'ready' 事件:`, payload); // 添加日志
|
||||||
|
emit('terminal-ready', payload); // 直接转发收到的 payload
|
||||||
|
},
|
||||||
|
onData: (data: string) => emit('terminalInput', { sessionId: props.activeSessionId ?? '', data }), // 包装成 payload,确保 sessionId 不为 null
|
||||||
|
onResize: (dims: { cols: number; rows: number }) => emit('terminalResize', { sessionId: props.activeSessionId ?? '', dims }), // 包装成 payload,确保 sessionId 不为 null
|
||||||
|
};
|
||||||
|
// --- 添加日志:确认 onReady 是否在 props 中 ---
|
||||||
|
console.log(`[LayoutRenderer ${props.activeSessionId}] Terminal componentProps 计算完成,包含 onReady。`);
|
||||||
|
// -----------------------------------------
|
||||||
|
case 'fileManager':
|
||||||
|
// 仅当有活动会话时才返回实际 props,否则返回空对象
|
||||||
|
if (!currentActiveSession) return {};
|
||||||
|
return {
|
||||||
|
sessionId: props.activeSessionId ?? '', // 确保 sessionId 不为 null
|
||||||
|
dbConnectionId: currentActiveSession.connectionId,
|
||||||
|
sftpManager: currentActiveSession.sftpManager, // 此时 currentActiveSession 必不为 null
|
||||||
|
wsDeps: { // 确保传递 wsDeps
|
||||||
|
sendMessage: currentActiveSession.wsManager.sendMessage,
|
||||||
|
onMessage: currentActiveSession.wsManager.onMessage,
|
||||||
|
isConnected: currentActiveSession.wsManager.isConnected,
|
||||||
|
isSftpReady: currentActiveSession.wsManager.isSftpReady
|
||||||
|
},
|
||||||
|
class: 'pane-content', // class 可以保留,或者在模板中处理
|
||||||
|
// FileManager 可能也需要转发事件,例如文件操作相关的,暂时省略
|
||||||
|
};
|
||||||
|
case 'statusMonitor':
|
||||||
|
// 仅当有活动会话时才返回实际 props,否则返回空对象
|
||||||
|
if (!currentActiveSession) return {};
|
||||||
|
return {
|
||||||
|
sessionId: props.activeSessionId ?? '', // 确保 sessionId 不为 null
|
||||||
|
serverStatus: currentActiveSession.statusMonitorManager.serverStatus.value, // 此时 currentActiveSession 必不为 null
|
||||||
|
statusError: currentActiveSession.statusMonitorManager.statusError.value, // 此时 currentActiveSession 必不为 null
|
||||||
|
class: 'pane-content',
|
||||||
|
};
|
||||||
|
case 'editor':
|
||||||
|
// FileEditorContainer 需要 tabs, activeTabId, sessionId, 并转发事件
|
||||||
|
return {
|
||||||
|
tabs: props.editorTabs, // 从 WorkspaceView 传入
|
||||||
|
activeTabId: props.activeEditorTabId, // 从 WorkspaceView 传入
|
||||||
|
sessionId: props.activeSessionId,
|
||||||
|
class: 'pane-content',
|
||||||
|
// 绑定内部处理器以转发事件 (恢复正确的编辑器事件)
|
||||||
|
onCloseTab: (tabId: string) => emit('closeEditorTab', tabId),
|
||||||
|
onActivateTab: (tabId: string) => emit('activateEditorTab', tabId),
|
||||||
|
'onUpdate:content': (payload: { tabId: string; content: string }) => emit('updateEditorContent', payload), // 注意事件名
|
||||||
|
onRequestSave: (tabId: string) => emit('saveEditorTab', tabId),
|
||||||
|
};
|
||||||
|
case 'commandBar':
|
||||||
|
// CommandInputBar 需要转发 send-command 事件
|
||||||
|
return {
|
||||||
|
class: 'pane-content',
|
||||||
|
onSendCommand: (command: string) => emit('sendCommand', command),
|
||||||
|
};
|
||||||
|
case 'connections':
|
||||||
|
// WorkspaceConnectionList 需要转发 connect-request 等事件
|
||||||
|
return {
|
||||||
|
class: 'pane-content',
|
||||||
|
// 绑定内部处理器以转发事件 (修正为 kebab-case)
|
||||||
|
onConnectRequest: (id: number) => emit('connect-request', id),
|
||||||
|
onOpenNewSession: (id: number) => emit('open-new-session', id),
|
||||||
|
onRequestAddConnection: () => emit('request-add-connection'),
|
||||||
|
onRequestEditConnection: (conn: any) => emit('request-edit-connection', conn), // 使用 any 避免类型问题
|
||||||
|
};
|
||||||
|
case 'commandHistory':
|
||||||
|
case 'quickCommands':
|
||||||
|
// 这两个视图需要转发 execute-command 事件
|
||||||
|
return {
|
||||||
|
class: 'pane-content',
|
||||||
|
onExecuteCommand: (command: string) => emit('sendCommand', command), // 复用 sendCommand 事件
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return { class: 'pane-content' };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Methods ---
|
||||||
|
// 处理 Splitpanes 大小调整事件,更新 layoutStore 中的节点 size
|
||||||
|
// @resized 事件参数是一个包含 panes 数组的对象
|
||||||
|
const handlePaneResize = (eventData: { panes: Array<{ size: number; [key: string]: any }> }) => {
|
||||||
|
console.log('Splitpanes resized event object:', eventData); // 打印整个事件对象
|
||||||
|
const paneSizes = eventData.panes; // 从事件对象中提取 panes 数组
|
||||||
|
|
||||||
|
console.log('Extracted paneSizes:', paneSizes); // 打印提取出的数组
|
||||||
|
|
||||||
|
if (props.layoutNode.type === 'container' && props.layoutNode.children) {
|
||||||
|
// 确保 paneSizes 是一个数组
|
||||||
|
if (!Array.isArray(paneSizes)) {
|
||||||
|
console.error('[LayoutRenderer] handlePaneResize: 从事件对象提取的 panes 不是数组:', paneSizes);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 构建传递给 store action 的数据结构
|
||||||
|
const childrenSizes = paneSizes.map((paneInfo, index) => ({
|
||||||
|
index: index,
|
||||||
|
size: paneInfo.size
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 调用 store action 来更新节点大小
|
||||||
|
layoutStore.updateNodeSizes(props.layoutNode.id, childrenSizes);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="layout-renderer" :data-node-id="layoutNode.id">
|
||||||
|
<!-- 如果是容器节点 -->
|
||||||
|
<template v-if="layoutNode.type === 'container' && layoutNode.children && layoutNode.children.length > 0">
|
||||||
|
<splitpanes
|
||||||
|
:horizontal="layoutNode.direction === 'vertical'"
|
||||||
|
class="default-theme"
|
||||||
|
style="height: 100%; width: 100%;"
|
||||||
|
@resized="handlePaneResize"
|
||||||
|
>
|
||||||
|
<pane
|
||||||
|
v-for="childNode in layoutNode.children"
|
||||||
|
:key="childNode.id"
|
||||||
|
:size="childNode.size ?? (100 / layoutNode.children.length)"
|
||||||
|
:min-size="5"
|
||||||
|
class="layout-pane-wrapper"
|
||||||
|
>
|
||||||
|
<!-- 递归调用自身来渲染子节点,并转发所有必要的事件 -->
|
||||||
|
<LayoutRenderer
|
||||||
|
:layout-node="childNode"
|
||||||
|
:active-session-id="activeSessionId"
|
||||||
|
:editor-tabs="editorTabs"
|
||||||
|
:active-editor-tab-id="activeEditorTabId"
|
||||||
|
@send-command="emit('sendCommand', $event)"
|
||||||
|
@terminal-input="emit('terminalInput', $event)"
|
||||||
|
@terminal-resize="emit('terminalResize', $event)"
|
||||||
|
@terminal-ready="emit('terminal-ready', $event)"
|
||||||
|
@close-editor-tab="emit('closeEditorTab', $event)"
|
||||||
|
@activate-editor-tab="emit('activateEditorTab', $event)"
|
||||||
|
@update-editor-content="emit('updateEditorContent', $event)"
|
||||||
|
@save-editor-tab="emit('saveEditorTab', $event)"
|
||||||
|
@connect-request="emit('connect-request', $event)"
|
||||||
|
@open-new-session="emit('open-new-session', $event)"
|
||||||
|
@request-add-connection="emit('request-add-connection')"
|
||||||
|
@request-edit-connection="emit('request-edit-connection', $event)"
|
||||||
|
/>
|
||||||
|
</pane>
|
||||||
|
</splitpanes>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 如果是面板节点 -->
|
||||||
|
<template v-else-if="layoutNode.type === 'pane'">
|
||||||
|
<!-- Terminal 需要 keep-alive 处理 -->
|
||||||
|
<template v-if="layoutNode.component === 'terminal'">
|
||||||
|
<keep-alive>
|
||||||
|
<component
|
||||||
|
v-if="activeSession"
|
||||||
|
:is="currentComponent"
|
||||||
|
:key="activeSessionId"
|
||||||
|
v-bind="componentProps"
|
||||||
|
/>
|
||||||
|
</keep-alive>
|
||||||
|
<div v-if="!activeSession" class="pane-placeholder">无活动会话</div> <!-- 处理无活动会话的情况 -->
|
||||||
|
</template>
|
||||||
|
<!-- FileManager 需要 keep-alive 处理 -->
|
||||||
|
<template v-else-if="layoutNode.component === 'fileManager'">
|
||||||
|
<keep-alive>
|
||||||
|
<component
|
||||||
|
v-if="activeSession"
|
||||||
|
:is="currentComponent"
|
||||||
|
:key="activeSessionId"
|
||||||
|
v-bind="componentProps"
|
||||||
|
/>
|
||||||
|
</keep-alive>
|
||||||
|
<div v-if="!activeSession" class="pane-placeholder">无活动会话</div>
|
||||||
|
</template>
|
||||||
|
<!-- StatusMonitor 仅在有活动会话时渲染,并添加 key (无 keep-alive) -->
|
||||||
|
<template v-else-if="layoutNode.component === 'statusMonitor'">
|
||||||
|
<component
|
||||||
|
v-if="activeSession"
|
||||||
|
:is="currentComponent"
|
||||||
|
:key="activeSessionId"
|
||||||
|
v-bind="componentProps"
|
||||||
|
/>
|
||||||
|
<div v-else class="pane-placeholder">无活动会话</div>
|
||||||
|
</template>
|
||||||
|
<!-- 其他面板正常渲染 (不依赖 activeSession 的) -->
|
||||||
|
<template v-else-if="currentComponent">
|
||||||
|
<component :is="currentComponent" v-bind="componentProps" />
|
||||||
|
</template>
|
||||||
|
<!-- 如果找不到组件 -->
|
||||||
|
<div v-else class="pane-placeholder error">
|
||||||
|
无效面板组件: {{ layoutNode.component || '未指定' }} (ID: {{ layoutNode.id }})
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 如果节点类型未知或无效 -->
|
||||||
|
<template v-else>
|
||||||
|
<div class="pane-placeholder error">
|
||||||
|
无效布局节点 (ID: {{ layoutNode.id }})
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.layout-renderer {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden; /* 防止内部内容溢出渲染器边界 */
|
||||||
|
display: flex; /* 确保子元素能正确填充 */
|
||||||
|
flex-direction: column; /* 默认列方向 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 为 splitpanes 包裹的 pane 添加样式,确保内容填充 */
|
||||||
|
.layout-pane-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden; /* 隐藏内部滚动条,由子组件处理 */
|
||||||
|
background-color: #f8f9fa; /* 默认背景,可能被子组件覆盖 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 确保动态加载的组件能正确应用 pane-content 样式 */
|
||||||
|
/* 如果组件内部没有根元素应用 pane-content,可能需要在这里强制 */
|
||||||
|
:deep(.layout-pane-wrapper > *) {
|
||||||
|
flex-grow: 1;
|
||||||
|
overflow: auto; /* 或者 hidden */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
/* 特别是对于没有明确设置 class 的组件 */
|
||||||
|
:deep(.layout-pane-wrapper > .pane-placeholder) {
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
color: #adb5bd;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
font-size: 0.9em;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
:deep(.layout-pane-wrapper > .pane-placeholder.error) {
|
||||||
|
color: #dc3545; /* 错误用红色 */
|
||||||
|
background-color: #fdd;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Splitpanes 默认主题样式调整 (如果需要覆盖全局样式) */
|
||||||
|
/* :deep(.splitpanes.default-theme .splitpanes__splitter) { */
|
||||||
|
/* background-color: #ccc; */
|
||||||
|
/* } */
|
||||||
|
/* :deep(.splitpanes--vertical > .splitpanes__splitter) { */
|
||||||
|
/* width: 7px; */
|
||||||
|
/* } */
|
||||||
|
/* :deep(.splitpanes--horizontal > .splitpanes__splitter) { */
|
||||||
|
/* height: 7px; */
|
||||||
|
/* } */
|
||||||
|
</style>
|
||||||
@@ -17,7 +17,7 @@ const props = defineProps<{
|
|||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'data', data: string): void; // 用户输入事件
|
(e: 'data', data: string): void; // 用户输入事件
|
||||||
(e: 'resize', dimensions: { cols: number; rows: number }): void; // 终端大小调整事件
|
(e: 'resize', dimensions: { cols: number; rows: number }): void; // 终端大小调整事件
|
||||||
(e: 'ready', terminal: Terminal): void; // 终端准备就绪事件
|
(e: 'ready', payload: { sessionId: string; terminal: Terminal }): void; // *** 修正:ready 事件传递包含 sessionId 和 terminal 实例的对象 ***
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const terminalRef = ref<HTMLElement | null>(null); // 终端容器的引用
|
const terminalRef = ref<HTMLElement | null>(null); // 终端容器的引用
|
||||||
@@ -185,8 +185,10 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
}, { immediate: true }); // 立即执行一次 watch
|
}, { immediate: true }); // 立即执行一次 watch
|
||||||
|
|
||||||
// 触发 ready 事件
|
// 触发 ready 事件,传递 sessionId 和 terminal 实例
|
||||||
emit('ready', terminal);
|
if (terminal) { // 确保 terminal 实例已创建
|
||||||
|
emit('ready', { sessionId: props.sessionId, terminal: terminal });
|
||||||
|
}
|
||||||
|
|
||||||
// 聚焦终端
|
// 聚焦终端
|
||||||
terminal.focus();
|
terminal.focus();
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, PropType } from 'vue'; // 导入 ref 和 computed
|
import { ref, computed, PropType } from 'vue'; // 导入 ref 和 computed
|
||||||
import { useI18n } from 'vue-i18n'; // 导入 i18n
|
import { useI18n } from 'vue-i18n'; // 导入 i18n
|
||||||
import { storeToRefs } from 'pinia'; // 导入 storeToRefs
|
// import { storeToRefs } from 'pinia'; // 移除 storeToRefs 导入,因为 paneVisibility 不再使用
|
||||||
import WorkspaceConnectionListComponent from './WorkspaceConnectionList.vue'; // 导入连接列表组件
|
import WorkspaceConnectionListComponent from './WorkspaceConnectionList.vue'; // 导入连接列表组件
|
||||||
import { useSessionStore } from '../stores/session.store'; // 导入 session store
|
import { useSessionStore } from '../stores/session.store'; // 导入 session store
|
||||||
import { useLayoutStore, type PaneName } from '../stores/layout.store'; // 导入布局 store 和类型
|
import { useLayoutStore, type PaneName } from '../stores/layout.store'; // 导入布局 store 和类型
|
||||||
@@ -11,7 +11,7 @@ import type { SessionTabInfoWithStatus } from '../stores/session.store'; // 导
|
|||||||
// --- Setup ---
|
// --- Setup ---
|
||||||
const { t } = useI18n(); // 初始化 i18n
|
const { t } = useI18n(); // 初始化 i18n
|
||||||
const layoutStore = useLayoutStore(); // 初始化布局 store
|
const layoutStore = useLayoutStore(); // 初始化布局 store
|
||||||
const { paneVisibility } = storeToRefs(layoutStore); // 修正:使用 storeToRefs 获取响应式状态
|
// const { paneVisibility } = storeToRefs(layoutStore); // 移除:paneVisibility 不再存在
|
||||||
|
|
||||||
// 定义 Props
|
// 定义 Props
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -20,13 +20,14 @@ const props = defineProps({
|
|||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
activeSessionId: {
|
activeSessionId: {
|
||||||
type: String as PropType<string | null>,
|
type: String as PropType<string | null>, // 类型已包含 null
|
||||||
required: true,
|
required: false, // 改为非必需,允许初始为 null
|
||||||
|
default: null, // 提供默认值 null
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// 定义事件
|
// 定义事件
|
||||||
const emit = defineEmits(['activate-session', 'close-session']);
|
const emit = defineEmits(['activate-session', 'close-session', 'open-layout-configurator']); // 添加新事件
|
||||||
|
|
||||||
const activateSession = (sessionId: string) => {
|
const activateSession = (sessionId: string) => {
|
||||||
if (sessionId !== props.activeSessionId) {
|
if (sessionId !== props.activeSessionId) {
|
||||||
@@ -63,25 +64,32 @@ const toggleLayoutMenu = () => {
|
|||||||
console.log('New state:', showLayoutMenu.value); // 添加日志
|
console.log('New state:', showLayoutMenu.value); // 添加日志
|
||||||
};
|
};
|
||||||
|
|
||||||
// 定义面板名称到显示文本的映射 (恢复 commandBar)
|
// 新增:处理打开布局配置器的事件
|
||||||
|
const openLayoutConfigurator = () => {
|
||||||
|
console.log('[TabBar] Emitting open-layout-configurator event');
|
||||||
|
emit('open-layout-configurator'); // 发出事件
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- 旧的布局菜单相关代码 (暂时保留,但功能已失效) ---
|
||||||
|
// 定义面板名称到显示文本的映射 (保留用于旧菜单显示)
|
||||||
const paneLabels: Record<PaneName, string> = {
|
const paneLabels: Record<PaneName, string> = {
|
||||||
connections: t('layout.pane.connections'),
|
connections: t('layout.pane.connections'),
|
||||||
terminal: t('layout.pane.terminal'),
|
terminal: t('layout.pane.terminal'),
|
||||||
commandBar: t('layout.pane.commandBar'), // 恢复
|
commandBar: t('layout.pane.commandBar'),
|
||||||
fileManager: t('layout.pane.fileManager'),
|
fileManager: t('layout.pane.fileManager'),
|
||||||
editor: t('layout.pane.editor'),
|
editor: t('layout.pane.editor'),
|
||||||
statusMonitor: t('layout.pane.statusMonitor'),
|
statusMonitor: t('layout.pane.statusMonitor'),
|
||||||
commandHistory: t('layout.pane.commandHistory', '命令历史'),
|
commandHistory: t('layout.pane.commandHistory', '命令历史'),
|
||||||
quickCommands: t('layout.pane.quickCommands', '快捷指令'), // 添加快捷指令标签
|
quickCommands: t('layout.pane.quickCommands', '快捷指令'),
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取所有可控制的面板名称
|
// 获取所有理论上的面板名称 (用于旧菜单显示)
|
||||||
const availablePanes = computed(() => Object.keys(paneVisibility.value) as PaneName[]); // 修正:使用 .value
|
const allPanesForMenu = computed(() => layoutStore.allPossiblePanes); // 使用新的 allPossiblePanes
|
||||||
|
|
||||||
// 处理菜单项点击
|
// 处理旧菜单项点击 (功能已失效,仅打印日志)
|
||||||
const handleTogglePane = (paneName: PaneName) => {
|
const handleTogglePane = (paneName: PaneName) => {
|
||||||
layoutStore.togglePaneVisibility(paneName);
|
console.warn(`[TabBar] 旧的 handleTogglePane 被调用,但 togglePaneVisibility 已移除。面板: ${paneName}`);
|
||||||
// 可以选择点击后关闭菜单,或者保持打开
|
// layoutStore.togglePaneVisibility(paneName); // 此方法已不存在
|
||||||
// showLayoutMenu.value = false;
|
// showLayoutMenu.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -111,22 +119,30 @@ const handleTogglePane = (paneName: PaneName) => {
|
|||||||
<i class="fas fa-plus"></i>
|
<i class="fas fa-plus"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<!-- 布局菜单按钮容器(推到最右侧) -->
|
<!-- 按钮容器(推到最右侧) -->
|
||||||
<div class="layout-menu-container">
|
<div class="action-buttons-container">
|
||||||
<button class="layout-menu-button" @click="toggleLayoutMenu" title="调整布局">
|
<!-- 新增:布局配置器按钮 -->
|
||||||
<i class="fas fa-bars"></i> <!-- 使用 Font Awesome bars 图标 -->
|
<button class="layout-config-button" @click="openLayoutConfigurator" title="配置工作区布局">
|
||||||
|
<i class="fas fa-th-large"></i> <!-- 网格布局图标 -->
|
||||||
</button>
|
</button>
|
||||||
<!-- 布局菜单下拉列表 (保持不变) -->
|
<!-- 保留旧的布局菜单按钮 -->
|
||||||
<div v-if="showLayoutMenu" class="layout-menu-dropdown">
|
<div class="layout-menu-container">
|
||||||
<ul>
|
<button class="layout-menu-button" @click="toggleLayoutMenu" title="切换面板可见性 (旧)">
|
||||||
<li v-for="pane in availablePanes" :key="pane" @click="handleTogglePane(pane)">
|
<i class="fas fa-bars"></i> <!-- 汉堡菜单图标 -->
|
||||||
<span class="checkmark">{{ paneVisibility[pane] ? '✓' : '' }}</span>
|
</button>
|
||||||
{{ paneLabels[pane] || pane }}
|
<!-- 旧布局菜单下拉列表 (显示所有面板,但勾选状态和点击功能失效) -->
|
||||||
</li>
|
<div v-if="showLayoutMenu" class="layout-menu-dropdown">
|
||||||
</ul>
|
<ul>
|
||||||
|
<!-- 使用 allPanesForMenu 迭代 -->
|
||||||
|
<li v-for="pane in allPanesForMenu" :key="pane" @click="handleTogglePane(pane)">
|
||||||
|
<!-- 移除基于 paneVisibility 的勾选 -->
|
||||||
|
<span class="checkmark"></span> <!-- 占位符保持对齐 -->
|
||||||
|
{{ paneLabels[pane] || pane }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 移除多余的结束标签 -->
|
|
||||||
<!-- 连接列表弹出窗口 (保持不变) -->
|
<!-- 连接列表弹出窗口 (保持不变) -->
|
||||||
<div v-if="showConnectionListPopup" class="connection-list-popup" @click.self="togglePopup">
|
<div v-if="showConnectionListPopup" class="connection-list-popup" @click.self="togglePopup">
|
||||||
<div class="popup-content">
|
<div class="popup-content">
|
||||||
@@ -280,7 +296,37 @@ const handleTogglePane = (paneName: PaneName) => {
|
|||||||
line-height: 1; /* 确保图标垂直居中 */
|
line-height: 1; /* 确保图标垂直居中 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 移除 action-buttons-container 样式 */
|
/* 新增:包裹右侧操作按钮的容器 */
|
||||||
|
.action-buttons-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-left: auto; /* 将整个容器推到右侧 */
|
||||||
|
height: 100%;
|
||||||
|
flex-shrink: 0; /* 防止被压缩 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 新增:布局配置器按钮样式 */
|
||||||
|
.layout-config-button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
border-left: 1px solid #bdbdbd; /* 左侧分隔线 */
|
||||||
|
padding: 0 0.8rem;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1.1em; /* 与其他按钮一致 */
|
||||||
|
color: #616161;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.layout-config-button:hover {
|
||||||
|
background-color: #d0d0d0;
|
||||||
|
}
|
||||||
|
.layout-config-button i {
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* 弹出窗口样式 */
|
/* 弹出窗口样式 */
|
||||||
.connection-list-popup {
|
.connection-list-popup {
|
||||||
@@ -346,17 +392,18 @@ const handleTogglePane = (paneName: PaneName) => {
|
|||||||
padding: 0; /* 保持移除内边距 */
|
padding: 0; /* 保持移除内边距 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 新增:布局菜单样式 */
|
/* 调整:旧布局菜单容器样式 */
|
||||||
.layout-menu-container {
|
.layout-menu-container {
|
||||||
position: relative; /* 用于定位下拉菜单 */
|
position: relative; /* 用于定位下拉菜单 */
|
||||||
display: flex; /* 确保按钮垂直居中 */
|
display: flex; /* 确保按钮垂直居中 */
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin-left: auto; /* 保持:将布局按钮推到最右侧 */
|
/* margin-left: auto; */ /* 移除:由父容器 .action-buttons-container 控制 */
|
||||||
border-left: 1px solid #bdbdbd; /* 确保布局按钮左侧有分隔线 */
|
border-left: 1px solid #bdbdbd; /* 保持左侧分隔线 */
|
||||||
flex-shrink: 0; /* 保持:防止被压缩 */
|
flex-shrink: 0; /* 保持:防止被压缩 */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 调整:旧布局菜单按钮样式 */
|
||||||
.layout-menu-button {
|
.layout-menu-button {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
|
|||||||
@@ -38,8 +38,18 @@ export function createSshTerminalManager(sessionId: string, wsDeps: SshTerminalD
|
|||||||
const handleTerminalReady = (term: Terminal) => {
|
const handleTerminalReady = (term: Terminal) => {
|
||||||
console.log(`[会话 ${sessionId}][SSH终端模块] 终端实例已就绪。`);
|
console.log(`[会话 ${sessionId}][SSH终端模块] 终端实例已就绪。`);
|
||||||
terminalInstance.value = term;
|
terminalInstance.value = term;
|
||||||
|
// --- 添加日志:检查缓冲区处理 ---
|
||||||
|
console.log(`[会话 ${sessionId}][SSH前端] handleTerminalReady: 准备处理缓冲区,缓冲区长度: ${terminalOutputBuffer.value.length}`);
|
||||||
|
if (terminalOutputBuffer.value.length > 0) {
|
||||||
|
console.log(`[会话 ${sessionId}][SSH前端] handleTerminalReady: 缓冲区内容 (前100字符):`, terminalOutputBuffer.value.map(d => d.substring(0, 100)).join(' | '));
|
||||||
|
}
|
||||||
|
// ---------------------------------
|
||||||
// 将缓冲区的输出写入终端
|
// 将缓冲区的输出写入终端
|
||||||
terminalOutputBuffer.value.forEach(data => term.write(data));
|
terminalOutputBuffer.value.forEach(data => {
|
||||||
|
console.log(`[会话 ${sessionId}][SSH前端] handleTerminalReady: 正在写入缓冲数据 (前100字符):`, data.substring(0, 100));
|
||||||
|
term.write(data);
|
||||||
|
});
|
||||||
|
console.log(`[会话 ${sessionId}][SSH前端] handleTerminalReady: 缓冲区处理完成。`);
|
||||||
terminalOutputBuffer.value = []; // 清空缓冲区
|
terminalOutputBuffer.value = []; // 清空缓冲区
|
||||||
// 可以在这里自动聚焦或执行其他初始化操作
|
// 可以在这里自动聚焦或执行其他初始化操作
|
||||||
// term.focus(); // 也许在 ssh:connected 时聚焦更好
|
// term.focus(); // 也许在 ssh:connected 时聚焦更好
|
||||||
@@ -166,11 +176,18 @@ export function createSshTerminalManager(sessionId: string, wsDeps: SshTerminalD
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- 添加前端日志 ---
|
||||||
|
console.log(`[会话 ${sessionId}][SSH前端] 收到 ssh:output 原始 payload (解码前):`, payload);
|
||||||
|
console.log(`[会话 ${sessionId}][SSH前端] 解码后的数据 (尝试写入):`, outputData);
|
||||||
|
// --------------------
|
||||||
|
|
||||||
if (terminalInstance.value) {
|
if (terminalInstance.value) {
|
||||||
|
console.log(`[会话 ${sessionId}][SSH前端] 终端实例存在,尝试写入...`);
|
||||||
terminalInstance.value.write(outputData);
|
terminalInstance.value.write(outputData);
|
||||||
|
console.log(`[会话 ${sessionId}][SSH前端] 写入完成。`);
|
||||||
} else {
|
} else {
|
||||||
// 如果终端还没准备好,先缓冲输出
|
// 如果终端还没准备好,先缓冲输出
|
||||||
|
console.log(`[会话 ${sessionId}][SSH前端] 终端实例不存在,缓冲数据...`);
|
||||||
terminalOutputBuffer.value.push(outputData);
|
terminalOutputBuffer.value.push(outputData);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -492,7 +492,14 @@
|
|||||||
"disabled": "Disabled",
|
"disabled": "Disabled",
|
||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
"errorOccurred": "An error occurred.",
|
"errorOccurred": "An error occurred.",
|
||||||
"dismiss": "Dismiss"
|
"dismiss": "Dismiss",
|
||||||
|
"close": "Close"
|
||||||
|
},
|
||||||
|
"layoutConfigurator": {
|
||||||
|
"title": "Layout Configurator",
|
||||||
|
"availablePanes": "Available Panes",
|
||||||
|
"layoutPreview": "Layout Preview",
|
||||||
|
"resetDefault": "Reset to Default Layout"
|
||||||
},
|
},
|
||||||
"auditLog": {
|
"auditLog": {
|
||||||
"title": "Audit Logs",
|
"title": "Audit Logs",
|
||||||
|
|||||||
@@ -495,7 +495,14 @@
|
|||||||
"disabled": "已禁用",
|
"disabled": "已禁用",
|
||||||
"settings": "设置",
|
"settings": "设置",
|
||||||
"errorOccurred": "发生错误。",
|
"errorOccurred": "发生错误。",
|
||||||
"dismiss": "关闭"
|
"dismiss": "关闭",
|
||||||
|
"close": "关闭"
|
||||||
|
},
|
||||||
|
"layoutConfigurator": {
|
||||||
|
"title": "布局配置器",
|
||||||
|
"availablePanes": "可用面板",
|
||||||
|
"layoutPreview": "布局预览",
|
||||||
|
"resetDefault": "重置为默认布局"
|
||||||
},
|
},
|
||||||
"auditLog": {
|
"auditLog": {
|
||||||
"title": "审计日志",
|
"title": "审计日志",
|
||||||
@@ -559,7 +566,8 @@
|
|||||||
"fileManager": "文件管理器",
|
"fileManager": "文件管理器",
|
||||||
"editor": "编辑器",
|
"editor": "编辑器",
|
||||||
"statusMonitor": "状态监视器",
|
"statusMonitor": "状态监视器",
|
||||||
"commandHistory": "命令历史"
|
"commandHistory": "命令历史",
|
||||||
|
"quickCommands": "快捷指令"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"commandHistory": {
|
"commandHistory": {
|
||||||
|
|||||||
@@ -1,46 +1,212 @@
|
|||||||
import { defineStore } from 'pinia';
|
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 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
|
// 定义 Store
|
||||||
export const useLayoutStore = defineStore('layout', () => {
|
export const useLayoutStore = defineStore('layout', () => {
|
||||||
// 使用 ref 创建响应式状态,存储每个面板的可见性 (添加 commandHistory)
|
// --- 状态 ---
|
||||||
const paneVisibility = ref<Record<PaneName, boolean>>({
|
// 核心状态:存储当前布局树结构
|
||||||
connections: true,
|
const layoutTree: Ref<LayoutNode | null> = ref(null);
|
||||||
terminal: true,
|
// 存储所有理论上可用的面板名称
|
||||||
commandBar: true,
|
const allPossiblePanes: Ref<PaneName[]> = ref([
|
||||||
fileManager: true,
|
'connections', 'terminal', 'commandBar', 'fileManager',
|
||||||
editor: true,
|
'editor', 'statusMonitor', 'commandHistory', 'quickCommands'
|
||||||
statusMonitor: true,
|
]);
|
||||||
commandHistory: true,
|
|
||||||
quickCommands: true, // 默认可见
|
// --- 计算属性 ---
|
||||||
|
// 计算当前布局中正在使用的面板
|
||||||
|
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: 切换指定面板的可见性
|
// --- Actions ---
|
||||||
function togglePaneVisibility(paneName: PaneName) {
|
// 初始化布局:尝试从 localStorage 加载,否则使用默认布局
|
||||||
if (paneVisibility.value[paneName] !== undefined) {
|
function initializeLayout() {
|
||||||
paneVisibility.value[paneName] = !paneVisibility.value[paneName];
|
try {
|
||||||
console.log(`[Layout Store] Toggled visibility for ${paneName}: ${paneVisibility.value[paneName]}`);
|
const savedLayout = localStorage.getItem(LAYOUT_STORAGE_KEY);
|
||||||
} else {
|
if (savedLayout) {
|
||||||
console.warn(`[Layout Store] Attempted to toggle visibility for unknown pane: ${paneName}`);
|
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) {
|
function updateLayoutTree(newTree: LayoutNode) {
|
||||||
if (paneVisibility.value[paneName] !== undefined) {
|
// 可选:添加验证逻辑
|
||||||
paneVisibility.value[paneName] = isVisible;
|
layoutTree.value = newTree;
|
||||||
console.log(`[Layout Store] Set visibility for ${paneName} to: ${isVisible}`);
|
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 {
|
} 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 {
|
return {
|
||||||
paneVisibility,
|
layoutTree,
|
||||||
togglePaneVisibility,
|
availablePanes, // 供配置器使用
|
||||||
setPaneVisibility,
|
usedPanes, // 可用于调试或内部逻辑
|
||||||
|
updateLayoutTree,
|
||||||
|
initializeLayout, // 允许外部重置或重新加载
|
||||||
|
updateNodeSizes, // *** 新增:暴露更新大小的 action ***
|
||||||
|
// 暴露 generateId 供配置器使用(如果需要)
|
||||||
|
generateId,
|
||||||
|
// 暴露 allPossiblePanes 供配置器显示所有选项
|
||||||
|
allPossiblePanes,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -352,18 +352,25 @@ export const useSessionStore = defineStore('session', () => {
|
|||||||
// 不需要再调用 activateSession,因为它已经是活动的
|
// 不需要再调用 activateSession,因为它已经是活动的
|
||||||
} else {
|
} else {
|
||||||
// 如果状态正常,则无需操作
|
// 如果状态正常,则无需操作
|
||||||
console.log(`[SessionStore] 活动会话 ${existingSessionId} 状态正常,无需操作。`);
|
console.log(`[SessionStore] 活动会话 ${existingSessionId} 状态正常,无需操作。`);
|
||||||
}
|
}
|
||||||
} else {
|
} else if (existingSessionId && existingSession) {
|
||||||
// 点击的不是当前活动标签(可能是非活动标签,或根本不存在),总是新建标签页
|
// 点击的是一个已存在但非活动的会话
|
||||||
if (existingSessionId) {
|
console.log(`[SessionStore] 点击的连接 ${connIdStr} 存在于非活动会话 ${existingSessionId} 中,将激活它。`);
|
||||||
console.log(`[SessionStore] 点击的连接 ${connIdStr} 存在于非活动会话 ${existingSessionId} 中,将打开新会话。`);
|
activateSession(existingSessionId);
|
||||||
} else {
|
// 激活后检查状态并尝试重连 (如果需要)
|
||||||
console.log(`[SessionStore] 未找到 ID 为 ${connIdStr} 的现有会话,将打开新会话。`);
|
const currentStatus = existingSession.wsManager.connectionStatus.value;
|
||||||
}
|
if (currentStatus === 'disconnected' || currentStatus === 'error') {
|
||||||
openNewSession(connIdStr); // 直接调用 openNewSession
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理连接列表的中键点击(总是打开新会话)
|
* 处理连接列表的中键点击(总是打开新会话)
|
||||||
|
|||||||
@@ -2,61 +2,48 @@
|
|||||||
import { onMounted, onBeforeUnmount, computed, ref } from 'vue';
|
import { onMounted, onBeforeUnmount, computed, ref } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import TerminalComponent from '../components/Terminal.vue';
|
// 移除不再直接使用的组件导入
|
||||||
import FileManagerComponent from '../components/FileManager.vue';
|
|
||||||
import StatusMonitorComponent from '../components/StatusMonitor.vue';
|
|
||||||
import WorkspaceConnectionListComponent from '../components/WorkspaceConnectionList.vue';
|
|
||||||
import AddConnectionFormComponent from '../components/AddConnectionForm.vue';
|
import AddConnectionFormComponent from '../components/AddConnectionForm.vue';
|
||||||
import TerminalTabBar from '../components/TerminalTabBar.vue';
|
import TerminalTabBar from '../components/TerminalTabBar.vue';
|
||||||
import CommandInputBar from '../components/CommandInputBar.vue';
|
import LayoutRenderer from '../components/LayoutRenderer.vue'; // *** 导入布局渲染器 ***
|
||||||
import FileEditorContainer from '../components/FileEditorContainer.vue'; // 导入编辑器容器
|
import LayoutConfigurator from '../components/LayoutConfigurator.vue'; // *** 导入布局配置器 ***
|
||||||
import CommandHistoryView from './CommandHistoryView.vue'; // 导入命令历史视图
|
import { useSessionStore, type SessionTabInfoWithStatus, type SshTerminalInstance } from '../stores/session.store';
|
||||||
import QuickCommandsView from './QuickCommandsView.vue'; // 导入快捷指令视图
|
import { useSettingsStore } from '../stores/settings.store';
|
||||||
import PaneTitleBar from '../components/PaneTitleBar.vue'; // 导入标题栏组件
|
import { useFileEditorStore } from '../stores/fileEditor.store';
|
||||||
import { useSessionStore, type SessionTabInfoWithStatus, type SshTerminalInstance } from '../stores/session.store'; // 导入 SshTerminalInstance
|
import { useLayoutStore } from '../stores/layout.store';
|
||||||
import { useSettingsStore } from '../stores/settings.store'; // 导入设置 Store
|
import { useCommandHistoryStore } from '../stores/commandHistory.store';
|
||||||
import { useFileEditorStore } from '../stores/fileEditor.store'; // 导入文件编辑器 Store
|
|
||||||
import { useLayoutStore } from '../stores/layout.store'; // 导入布局 Store
|
|
||||||
import { useCommandHistoryStore } from '../stores/commandHistory.store'; // 导入命令历史 Store
|
|
||||||
import type { ConnectionInfo } from '../stores/connections.store';
|
import type { ConnectionInfo } from '../stores/connections.store';
|
||||||
// 导入 splitpanes 组件
|
import type { Terminal } from 'xterm'; // *** 导入 Terminal 类型 ***
|
||||||
import { Splitpanes, Pane } from 'splitpanes';
|
|
||||||
// 导入管理器实例类型,用于 FileManagerComponent 的 prop 类型断言
|
|
||||||
// import type { SftpManagerInstance } from '../stores/session.store'; // SftpManagerInstance 已在上面导入
|
|
||||||
|
|
||||||
// --- Setup ---
|
// --- Setup ---
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const sessionStore = useSessionStore();
|
const sessionStore = useSessionStore();
|
||||||
const settingsStore = useSettingsStore(); // 初始化设置 Store
|
const settingsStore = useSettingsStore();
|
||||||
const fileEditorStore = useFileEditorStore(); // 初始化文件编辑器 Store (用于共享模式)
|
const fileEditorStore = useFileEditorStore();
|
||||||
const layoutStore = useLayoutStore(); // 初始化布局 Store
|
const layoutStore = useLayoutStore();
|
||||||
const commandHistoryStore = useCommandHistoryStore(); // 初始化命令历史 Store
|
const commandHistoryStore = useCommandHistoryStore();
|
||||||
|
|
||||||
// --- 从 Store 获取响应式状态和 Getters ---
|
// --- 从 Store 获取响应式状态和 Getters ---
|
||||||
const { sessionTabsWithStatus, activeSessionId, activeSession } = storeToRefs(sessionStore);
|
const { sessionTabsWithStatus, activeSessionId, activeSession } = storeToRefs(sessionStore);
|
||||||
const { shareFileEditorTabsBoolean } = storeToRefs(settingsStore); // 获取共享设置
|
const { shareFileEditorTabsBoolean } = storeToRefs(settingsStore);
|
||||||
const { orderedTabs: globalEditorTabs, activeTabId: globalActiveEditorTabId } = storeToRefs(fileEditorStore); // 获取全局编辑器状态
|
const { orderedTabs: globalEditorTabs, activeTabId: globalActiveEditorTabId } = storeToRefs(fileEditorStore);
|
||||||
const { paneVisibility } = storeToRefs(layoutStore); // 获取布局可见性状态
|
const { layoutTree } = storeToRefs(layoutStore); // 获取布局树
|
||||||
|
|
||||||
// --- 计算属性 (用于动态绑定编辑器 Props) ---
|
// --- 计算属性 (用于动态绑定编辑器 Props) ---
|
||||||
// **再次修正:** 确保计算属性在共享模式下严格只依赖全局状态
|
// 这些计算属性现在需要传递给 LayoutRenderer
|
||||||
const editorTabs = computed(() => {
|
const editorTabs = computed(() => {
|
||||||
if (shareFileEditorTabsBoolean.value) {
|
if (shareFileEditorTabsBoolean.value) {
|
||||||
// console.log('[WorkspaceView] Shared Mode: Returning globalEditorTabs');
|
return globalEditorTabs.value;
|
||||||
return globalEditorTabs.value; // 共享模式:只依赖全局 store 的 tabs
|
|
||||||
} else {
|
} else {
|
||||||
// console.log('[WorkspaceView] Independent Mode: Returning activeSession tabs');
|
return activeSession.value?.editorTabs.value ?? [];
|
||||||
return activeSession.value?.editorTabs.value ?? []; // 独立模式:依赖 activeSession
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const activeEditorTabId = computed(() => {
|
const activeEditorTabId = computed(() => {
|
||||||
if (shareFileEditorTabsBoolean.value) {
|
if (shareFileEditorTabsBoolean.value) {
|
||||||
// console.log('[WorkspaceView] Shared Mode: Returning globalActiveEditorTabId');
|
return globalActiveEditorTabId.value;
|
||||||
return globalActiveEditorTabId.value; // 共享模式:只依赖全局 store 的 activeTabId
|
|
||||||
} else {
|
} else {
|
||||||
// console.log('[WorkspaceView] Independent Mode: Returning activeSession activeEditorTabId');
|
return activeSession.value?.activeEditorTabId.value ?? null;
|
||||||
return activeSession.value?.activeEditorTabId.value ?? null; // 独立模式:依赖 activeSession
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -64,10 +51,12 @@ const activeEditorTabId = computed(() => {
|
|||||||
// --- UI 状态 (保持本地) ---
|
// --- UI 状态 (保持本地) ---
|
||||||
const showAddEditForm = ref(false);
|
const showAddEditForm = ref(false);
|
||||||
const connectionToEdit = ref<ConnectionInfo | null>(null);
|
const connectionToEdit = ref<ConnectionInfo | null>(null);
|
||||||
|
const showLayoutConfigurator = ref(false); // 控制布局配置器可见性
|
||||||
|
|
||||||
// --- 生命周期钩子 ---
|
// --- 生命周期钩子 ---
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
console.log('[工作区视图] 组件已挂载。');
|
console.log('[工作区视图] 组件已挂载。');
|
||||||
|
// 确保布局已初始化 (layoutStore 内部会处理)
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
@@ -101,81 +90,92 @@ onBeforeUnmount(() => {
|
|||||||
handleFormClose();
|
handleFormClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理命令发送
|
// 处理打开和关闭布局配置器
|
||||||
|
const handleOpenLayoutConfigurator = () => {
|
||||||
|
showLayoutConfigurator.value = true;
|
||||||
|
};
|
||||||
|
const handleCloseLayoutConfigurator = () => {
|
||||||
|
showLayoutConfigurator.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- 事件处理 (传递给 LayoutRenderer 或直接使用) ---
|
||||||
|
|
||||||
|
// 处理命令发送 (用于 CommandBar, CommandHistory, QuickCommands)
|
||||||
const handleSendCommand = (command: string) => {
|
const handleSendCommand = (command: string) => {
|
||||||
const currentSession = activeSession.value; // 获取当前活动会话
|
const currentSession = activeSession.value;
|
||||||
if (!currentSession) {
|
if (!currentSession) {
|
||||||
console.warn('[WorkspaceView] Cannot send command, no active session.');
|
console.warn('[WorkspaceView] Cannot send command, no active session.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const terminalManager = currentSession.terminalManager as (SshTerminalInstance | undefined);
|
const terminalManager = currentSession.terminalManager as (SshTerminalInstance | undefined);
|
||||||
|
|
||||||
// 检查连接状态和命令内容
|
|
||||||
if (terminalManager?.isSshConnected && !terminalManager.isSshConnected.value && command.trim() === '') {
|
if (terminalManager?.isSshConnected && !terminalManager.isSshConnected.value && command.trim() === '') {
|
||||||
// 如果连接断开且命令为空(仅按回车),则触发重连
|
|
||||||
console.log(`[WorkspaceView] Command bar Enter detected in disconnected session ${currentSession.sessionId}, attempting reconnect...`);
|
console.log(`[WorkspaceView] Command bar Enter detected in disconnected session ${currentSession.sessionId}, attempting reconnect...`);
|
||||||
// 可选:在终端显示提示
|
|
||||||
if (terminalManager.terminalInstance?.value) {
|
if (terminalManager.terminalInstance?.value) {
|
||||||
terminalManager.terminalInstance.value.writeln(`\r\n\x1b[33m${t('workspace.terminal.reconnectingMsg')}\x1b[0m`);
|
terminalManager.terminalInstance.value.writeln(`\r\n\x1b[33m${t('workspace.terminal.reconnectingMsg')}\x1b[0m`);
|
||||||
}
|
}
|
||||||
sessionStore.handleConnectRequest(currentSession.connectionId);
|
sessionStore.handleConnectRequest(currentSession.connectionId);
|
||||||
return; // 阻止发送空命令
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 否则,正常发送命令
|
|
||||||
if (terminalManager && typeof terminalManager.sendData === 'function') {
|
|
||||||
const commandToSend = command.trim(); // 获取去除首尾空格的命令,用于记录历史
|
|
||||||
console.log(`[WorkspaceView] Sending command to active session ${currentSession.sessionId}: ${commandToSend}`);
|
|
||||||
// 发送给终端时,需要添加回车符以模拟执行
|
|
||||||
terminalManager.sendData(command + '\r');
|
|
||||||
|
|
||||||
// 记录非空命令到历史记录
|
if (terminalManager && typeof terminalManager.sendData === 'function') {
|
||||||
|
const commandToSend = command.trim();
|
||||||
|
console.log(`[WorkspaceView] Sending command to active session ${currentSession.sessionId}: ${commandToSend}`);
|
||||||
|
terminalManager.sendData(command + '\r');
|
||||||
if (commandToSend.length > 0) {
|
if (commandToSend.length > 0) {
|
||||||
commandHistoryStore.addCommand(commandToSend);
|
commandHistoryStore.addCommand(commandToSend);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.warn(`[WorkspaceView] Cannot send command for session ${currentSession.sessionId}, terminal manager or sendData method not available.`);
|
console.warn(`[WorkspaceView] Cannot send command for session ${currentSession.sessionId}, terminal manager or sendData method not available.`);
|
||||||
// 可以考虑给用户一个提示
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- 新增:处理终端输入,包含重连逻辑 ---
|
// 处理终端输入 (用于 Terminal)
|
||||||
const handleTerminalInput = (sessionId: string, data: string) => {
|
// 注意:LayoutRenderer 内部的 Terminal 组件需要 emit('terminal-input', sessionId, data)
|
||||||
const session = sessionStore.sessions.get(sessionId); // 获取整个 session 对象
|
const handleTerminalInput = (payload: { sessionId: string; data: string }) => {
|
||||||
const manager = session?.terminalManager as (SshTerminalInstance | undefined); // 获取 terminalManager 并断言类型
|
const { sessionId, data } = payload; // 解构 payload
|
||||||
|
const session = sessionStore.sessions.get(sessionId);
|
||||||
|
const manager = session?.terminalManager as (SshTerminalInstance | undefined);
|
||||||
if (!session || !manager) {
|
if (!session || !manager) {
|
||||||
console.warn(`[WorkspaceView] handleTerminalInput: 未找到会话 ${sessionId} 或其 terminalManager`);
|
console.warn(`[WorkspaceView] handleTerminalInput: 未找到会话 ${sessionId} 或其 terminalManager`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否按下回车且 SSH 未连接
|
|
||||||
// 确保 manager.isSshConnected 存在再访问 .value
|
|
||||||
if (data === '\r' && manager.isSshConnected && !manager.isSshConnected.value) {
|
if (data === '\r' && manager.isSshConnected && !manager.isSshConnected.value) {
|
||||||
console.log(`[WorkspaceView] 检测到在断开的会话 ${sessionId} 中按下回车,尝试重连...`);
|
console.log(`[WorkspaceView] 检测到在断开的会话 ${sessionId} 中按下回车,尝试重连...`);
|
||||||
// 可选:立即在终端显示提示 (需要 manager 暴露 terminalInstance)
|
|
||||||
if (manager.terminalInstance?.value) {
|
if (manager.terminalInstance?.value) {
|
||||||
manager.terminalInstance.value.writeln(`\r\n\x1b[33m${t('workspace.terminal.reconnectingMsg')}\x1b[0m`);
|
manager.terminalInstance.value.writeln(`\r\n\x1b[33m${t('workspace.terminal.reconnectingMsg')}\x1b[0m`);
|
||||||
} else {
|
} else {
|
||||||
console.warn(`[WorkspaceView] 无法写入重连提示,terminalInstance 不可用。`);
|
console.warn(`[WorkspaceView] 无法写入重连提示,terminalInstance 不可用。`);
|
||||||
}
|
}
|
||||||
// 调用 sessionStore 中现有的重连逻辑
|
|
||||||
sessionStore.handleConnectRequest(session.connectionId);
|
sessionStore.handleConnectRequest(session.connectionId);
|
||||||
} else {
|
} else {
|
||||||
// 否则,正常处理输入
|
|
||||||
manager.handleTerminalData(data);
|
manager.handleTerminalData(data);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- 编辑器操作处理 ---
|
// 处理终端大小调整 (用于 Terminal)
|
||||||
|
// 注意:LayoutRenderer 内部的 Terminal 组件需要 emit('terminal-resize', sessionId, dims)
|
||||||
|
const handleTerminalResize = (payload: { sessionId: string; dims: { cols: number; rows: number } }) => {
|
||||||
|
console.log(`[工作区视图 ${payload.sessionId}] 收到 resize 事件:`, payload.dims);
|
||||||
|
sessionStore.sessions.get(payload.sessionId)?.terminalManager.handleTerminalResize(payload.dims);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理终端就绪 (用于 Terminal)
|
||||||
|
// 注意:LayoutRenderer 内部的 Terminal 组件需要 emit('terminal-ready', payload)
|
||||||
|
const handleTerminalReady = (payload: { sessionId: string; terminal: Terminal }) => { // *** 修正:接收包含 sessionId 和 terminal 的 payload ***
|
||||||
|
console.log(`[工作区视图 ${payload.sessionId}] 收到 terminal-ready 事件。`); // 添加日志
|
||||||
|
sessionStore.sessions.get(payload.sessionId)?.terminalManager.handleTerminalReady(payload.terminal); // *** 修正:传递 terminal 实例 ***
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// --- 编辑器操作处理 (用于 FileEditorContainer) ---
|
||||||
const handleCloseEditorTab = (tabId: string) => {
|
const handleCloseEditorTab = (tabId: string) => {
|
||||||
const isShared = shareFileEditorTabsBoolean.value; // 在函数开始时获取模式
|
const isShared = shareFileEditorTabsBoolean.value;
|
||||||
console.log(`[WorkspaceView] handleCloseEditorTab: ${tabId}, Shared mode: ${isShared}`);
|
console.log(`[WorkspaceView] handleCloseEditorTab: ${tabId}, Shared mode: ${isShared}`);
|
||||||
if (isShared) {
|
if (isShared) {
|
||||||
fileEditorStore.closeTab(tabId);
|
fileEditorStore.closeTab(tabId);
|
||||||
} else {
|
} else {
|
||||||
const currentActiveSessionId = activeSessionId.value; // 获取当前的 activeSessionId
|
const currentActiveSessionId = activeSessionId.value;
|
||||||
if (currentActiveSessionId) {
|
if (currentActiveSessionId) {
|
||||||
sessionStore.closeEditorTabInSession(currentActiveSessionId, tabId);
|
sessionStore.closeEditorTabInSession(currentActiveSessionId, tabId);
|
||||||
} else {
|
} else {
|
||||||
@@ -185,12 +185,12 @@ onBeforeUnmount(() => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleActivateEditorTab = (tabId: string) => {
|
const handleActivateEditorTab = (tabId: string) => {
|
||||||
const isShared = shareFileEditorTabsBoolean.value; // 在函数开始时获取模式
|
const isShared = shareFileEditorTabsBoolean.value;
|
||||||
console.log(`[WorkspaceView] handleActivateEditorTab: ${tabId}, Shared mode: ${isShared}`);
|
console.log(`[WorkspaceView] handleActivateEditorTab: ${tabId}, Shared mode: ${isShared}`);
|
||||||
if (isShared) {
|
if (isShared) {
|
||||||
fileEditorStore.setActiveTab(tabId);
|
fileEditorStore.setActiveTab(tabId);
|
||||||
} else {
|
} else {
|
||||||
const currentActiveSessionId = activeSessionId.value; // 获取当前的 activeSessionId
|
const currentActiveSessionId = activeSessionId.value;
|
||||||
if (currentActiveSessionId) {
|
if (currentActiveSessionId) {
|
||||||
sessionStore.setActiveEditorTabInSession(currentActiveSessionId, tabId);
|
sessionStore.setActiveEditorTabInSession(currentActiveSessionId, tabId);
|
||||||
} else {
|
} else {
|
||||||
@@ -199,14 +199,13 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理编辑器内容更新事件
|
|
||||||
const handleUpdateEditorContent = (payload: { tabId: string; content: string }) => {
|
const handleUpdateEditorContent = (payload: { tabId: string; content: string }) => {
|
||||||
const isShared = shareFileEditorTabsBoolean.value; // 在函数开始时获取模式
|
const isShared = shareFileEditorTabsBoolean.value;
|
||||||
console.log(`[WorkspaceView] handleUpdateEditorContent for tab ${payload.tabId}, Shared mode: ${isShared}`);
|
console.log(`[WorkspaceView] handleUpdateEditorContent for tab ${payload.tabId}, Shared mode: ${isShared}`);
|
||||||
if (isShared) {
|
if (isShared) {
|
||||||
fileEditorStore.updateFileContent(payload.tabId, payload.content);
|
fileEditorStore.updateFileContent(payload.tabId, payload.content);
|
||||||
} else {
|
} else {
|
||||||
const currentActiveSessionId = activeSessionId.value; // 获取当前的 activeSessionId
|
const currentActiveSessionId = activeSessionId.value;
|
||||||
if (currentActiveSessionId) {
|
if (currentActiveSessionId) {
|
||||||
sessionStore.updateFileContentInSession(currentActiveSessionId, payload.tabId, payload.content);
|
sessionStore.updateFileContentInSession(currentActiveSessionId, payload.tabId, payload.content);
|
||||||
} else {
|
} else {
|
||||||
@@ -215,14 +214,13 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理编辑器保存请求事件
|
|
||||||
const handleSaveEditorTab = (tabId: string) => {
|
const handleSaveEditorTab = (tabId: string) => {
|
||||||
const isShared = shareFileEditorTabsBoolean.value; // 在函数开始时获取模式
|
const isShared = shareFileEditorTabsBoolean.value;
|
||||||
console.log(`[WorkspaceView] handleSaveEditorTab: ${tabId}, Shared mode: ${isShared}`);
|
console.log(`[WorkspaceView] handleSaveEditorTab: ${tabId}, Shared mode: ${isShared}`);
|
||||||
if (isShared) {
|
if (isShared) {
|
||||||
fileEditorStore.saveFile(tabId);
|
fileEditorStore.saveFile(tabId);
|
||||||
} else {
|
} else {
|
||||||
const currentActiveSessionId = activeSessionId.value; // 获取当前的 activeSessionId
|
const currentActiveSessionId = activeSessionId.value;
|
||||||
if (currentActiveSessionId) {
|
if (currentActiveSessionId) {
|
||||||
sessionStore.saveFileInSession(currentActiveSessionId, tabId);
|
sessionStore.saveFileInSession(currentActiveSessionId, tabId);
|
||||||
} else {
|
} else {
|
||||||
@@ -230,152 +228,56 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// --- 连接列表操作处理 (用于 WorkspaceConnectionList) ---
|
||||||
|
const handleConnectRequest = (id: number) => {
|
||||||
|
console.log(`[WorkspaceView] Received 'connect-request' event for ID: ${id}`);
|
||||||
|
sessionStore.handleConnectRequest(id);
|
||||||
|
};
|
||||||
|
const handleOpenNewSession = (id: number) => {
|
||||||
|
console.log(`[WorkspaceView] Received 'open-new-session' event for ID: ${id}`);
|
||||||
|
sessionStore.handleOpenNewSession(id);
|
||||||
|
};
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="workspace-view">
|
<div class="workspace-view"> <!-- Root element -->
|
||||||
<TerminalTabBar
|
<TerminalTabBar
|
||||||
:sessions="sessionTabsWithStatus"
|
:sessions="sessionTabsWithStatus"
|
||||||
:active-session-id="activeSessionId"
|
:active-session-id="activeSessionId"
|
||||||
@activate-session="sessionStore.activateSession"
|
@activate-session="sessionStore.activateSession"
|
||||||
@close-session="sessionStore.closeSession"
|
@close-session="sessionStore.closeSession"
|
||||||
|
@open-layout-configurator="handleOpenLayoutConfigurator"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="main-content-area">
|
<div class="main-content-area">
|
||||||
<!-- 最外层:左右分割 (连接列表 | 中间区域 | 编辑器 | 状态监视器) -->
|
<LayoutRenderer
|
||||||
<splitpanes class="default-theme" :horizontal="false" style="height: 100%">
|
v-if="layoutTree"
|
||||||
|
:layout-node="layoutTree"
|
||||||
<!-- 1. 左侧边栏 Pane (连接列表) -->
|
:active-session-id="activeSessionId"
|
||||||
<pane v-if="paneVisibility.connections" size="15" min-size="10" class="sidebar-pane"> <!-- 移除 pane-with-title class -->
|
class="layout-renderer-wrapper"
|
||||||
<WorkspaceConnectionListComponent
|
:editor-tabs="editorTabs"
|
||||||
class="pane-content"
|
:active-editor-tab-id="activeEditorTabId"
|
||||||
@connect-request="(id) => { console.log(`[WorkspaceView] Received 'connect-request' event for ID: ${id}`); sessionStore.handleConnectRequest(id); }"
|
@send-command="handleSendCommand"
|
||||||
@open-new-session="(id) => { console.log(`[WorkspaceView] Received 'open-new-session' event for ID: ${id}`); sessionStore.handleOpenNewSession(id); }"
|
@terminal-input="handleTerminalInput"
|
||||||
@request-add-connection="() => { console.log('[WorkspaceView] Received \'request-add-connection\' event'); handleRequestAddConnection(); }"
|
@terminal-resize="handleTerminalResize"
|
||||||
@request-edit-connection="(conn) => { console.log(`[WorkspaceView] Received 'request-edit-connection' event for connection:`, conn); handleRequestEditConnection(conn); }"
|
@terminal-ready="handleTerminalReady"
|
||||||
/>
|
@close-editor-tab="handleCloseEditorTab"
|
||||||
</pane>
|
@activate-editor-tab="handleActivateEditorTab"
|
||||||
|
@update-editor-content="handleUpdateEditorContent"
|
||||||
<!-- 新增:命令历史 Pane -->
|
@save-editor-tab="handleSaveEditorTab"
|
||||||
<pane v-if="paneVisibility.commandHistory" size="15" min-size="10" class="sidebar-pane command-history-pane">
|
@connect-request="handleConnectRequest"
|
||||||
<CommandHistoryView class="pane-content" @execute-command="handleSendCommand" />
|
@open-new-session="handleOpenNewSession"
|
||||||
</pane>
|
@request-add-connection="handleRequestAddConnection"
|
||||||
|
@request-edit-connection="handleRequestEditConnection"
|
||||||
<!-- 新增:快捷指令 Pane -->
|
></LayoutRenderer> <!-- 修正:使用单独的结束标签 -->
|
||||||
<pane v-if="paneVisibility.quickCommands" size="15" min-size="10" class="sidebar-pane quick-commands-pane">
|
<div v-else class="pane-placeholder"> <!-- 确保 v-else 紧随 v-if -->
|
||||||
<QuickCommandsView class="pane-content" @execute-command="handleSendCommand" /> <!-- 监听事件 -->
|
{{ t('layout.loading', '加载布局中...') }}
|
||||||
</pane>
|
</div>
|
||||||
|
|
||||||
<!-- 2. 中间区域 Pane (终端/命令栏/文件管理器) - 这个 Pane 本身通常保持可见,内部 Pane 才切换 -->
|
|
||||||
<pane size="30" min-size="20" class="middle-pane"> <!-- 再次调整中间区域大小 -->
|
|
||||||
<!-- 上下分割 (终端 | 命令栏 | 文件管理器) -->
|
|
||||||
<splitpanes :horizontal="true" style="height: 100%" :dbl-click-splitter="false">
|
|
||||||
<!-- 上方 Pane (终端) -->
|
|
||||||
<pane v-if="paneVisibility.terminal" size="55" min-size="20" class="terminal-pane"> <!-- 移除 pane-with-title class -->
|
|
||||||
<div class="pane-content terminal-content-wrapper"> <!-- 添加包裹 div -->
|
|
||||||
<div
|
|
||||||
v-for="tabInfo in sessionTabsWithStatus"
|
|
||||||
:key="tabInfo.sessionId"
|
|
||||||
v-show="tabInfo.sessionId === activeSessionId"
|
|
||||||
class="terminal-session-wrapper"
|
|
||||||
>
|
|
||||||
<TerminalComponent
|
|
||||||
:key="tabInfo.sessionId"
|
|
||||||
:session-id="tabInfo.sessionId"
|
|
||||||
:is-active="tabInfo.sessionId === activeSessionId"
|
|
||||||
@ready="sessionStore.sessions.get(tabInfo.sessionId)?.terminalManager.handleTerminalReady"
|
|
||||||
@data="(data) => handleTerminalInput(tabInfo.sessionId, data)"
|
|
||||||
@resize="(dims) => { console.log(`[工作区视图 ${tabInfo.sessionId}] 收到 resize 事件:`, dims); sessionStore.sessions.get(tabInfo.sessionId)?.terminalManager.handleTerminalResize(dims); }"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div v-if="!activeSessionId" class="terminal-placeholder">
|
|
||||||
<h2>{{ t('workspace.selectConnectionPrompt') }}</h2>
|
|
||||||
<p>{{ t('workspace.selectConnectionHint') }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</pane> <!-- End Terminal Pane -->
|
|
||||||
|
|
||||||
<!-- 中间 Pane (命令栏) - 移除标题栏,但保留 v-if -->
|
|
||||||
<pane v-if="paneVisibility.commandBar" size="5" min-size="5" class="command-bar-pane">
|
|
||||||
<CommandInputBar
|
|
||||||
v-if="activeSessionId"
|
|
||||||
@send-command="handleSendCommand"
|
|
||||||
/>
|
|
||||||
</pane> <!-- End Command Bar Pane -->
|
|
||||||
|
|
||||||
<!-- 下方 Pane (文件管理器区域 - 包含新的水平分割) -->
|
|
||||||
<pane v-if="paneVisibility.fileManager" size="40" min-size="15" class="file-manager-area-pane"> <!-- 移除 pane-with-title class -->
|
|
||||||
<!-- 新增:内部水平分割,允许未来添加列 -->
|
|
||||||
<splitpanes :horizontal="false" style="height: 100%" :dbl-click-splitter="false" class="pane-content"> <!-- 添加 class -->
|
|
||||||
<!-- 初始的文件管理器 Pane -->
|
|
||||||
<pane class="file-manager-pane"> <!-- 这个内部 pane 不需要 title bar -->
|
|
||||||
<div
|
|
||||||
v-for="tabInfo in sessionTabsWithStatus"
|
|
||||||
:key="tabInfo.sessionId + '-fm-wrapper'"
|
|
||||||
v-show="tabInfo.sessionId === activeSessionId"
|
|
||||||
class="file-manager-wrapper"
|
|
||||||
>
|
|
||||||
<FileManagerComponent
|
|
||||||
v-if="sessionStore.sessions.get(tabInfo.sessionId)"
|
|
||||||
:key="tabInfo.sessionId + '-fm'"
|
|
||||||
:session-id="tabInfo.sessionId"
|
|
||||||
:db-connection-id="sessionStore.sessions.get(tabInfo.sessionId)!.connectionId"
|
|
||||||
:sftp-manager="sessionStore.sessions.get(tabInfo.sessionId)!.sftpManager"
|
|
||||||
:ws-deps="{
|
|
||||||
sendMessage: sessionStore.sessions.get(tabInfo.sessionId)!.wsManager.sendMessage,
|
|
||||||
onMessage: sessionStore.sessions.get(tabInfo.sessionId)!.wsManager.onMessage,
|
|
||||||
isConnected: sessionStore.sessions.get(tabInfo.sessionId)!.wsManager.isConnected,
|
|
||||||
isSftpReady: sessionStore.sessions.get(tabInfo.sessionId)!.wsManager.isSftpReady
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div v-if="!activeSessionId" class="pane-placeholder">{{ t('fileManager.noActiveSession') }}</div>
|
|
||||||
</pane> <!-- End Inner File Manager Pane -->
|
|
||||||
<!-- 这里可以将来添加其他 Pane -->
|
|
||||||
</splitpanes> <!-- End Inner Horizontal Splitpanes -->
|
|
||||||
</pane> <!-- End File Manager Area Pane -->
|
|
||||||
</splitpanes> <!-- End Middle Area Vertical Splitpanes -->
|
|
||||||
</pane> <!-- End Middle Pane -->
|
|
||||||
|
|
||||||
<!-- 3. 右侧区域 1 Pane (文件编辑器) -->
|
|
||||||
<pane v-if="paneVisibility.editor" size="20" min-size="15" class="file-editor-pane"> <!-- 移除 pane-with-title class -->
|
|
||||||
<FileEditorContainer
|
|
||||||
class="pane-content"
|
|
||||||
:tabs="editorTabs"
|
|
||||||
:active-tab-id="activeEditorTabId"
|
|
||||||
:session-id="activeSessionId"
|
|
||||||
@close-tab="handleCloseEditorTab"
|
|
||||||
@activate-tab="handleActivateEditorTab"
|
|
||||||
@update:content="handleUpdateEditorContent"
|
|
||||||
@request-save="handleSaveEditorTab"
|
|
||||||
/>
|
|
||||||
</pane>
|
|
||||||
|
|
||||||
<!-- 4. 右侧区域 2 Pane (状态监视器) -->
|
|
||||||
<pane v-if="paneVisibility.statusMonitor" size="15" min-size="10" class="sidebar-pane status-monitor-pane"> <!-- 移除 pane-with-title class -->
|
|
||||||
<div class="pane-content status-monitor-content-wrapper"> <!-- 添加包裹 div -->
|
|
||||||
<div
|
|
||||||
v-for="tabInfo in sessionTabsWithStatus"
|
|
||||||
:key="tabInfo.sessionId + '-sm-wrapper'"
|
|
||||||
v-show="tabInfo.sessionId === activeSessionId"
|
|
||||||
class="status-monitor-wrapper"
|
|
||||||
>
|
|
||||||
<StatusMonitorComponent
|
|
||||||
v-if="sessionStore.sessions.get(tabInfo.sessionId)"
|
|
||||||
:key="tabInfo.sessionId + '-sm'"
|
|
||||||
:session-id="tabInfo.sessionId"
|
|
||||||
:server-status="sessionStore.sessions.get(tabInfo.sessionId)!.statusMonitorManager.serverStatus.value"
|
|
||||||
:status-error="sessionStore.sessions.get(tabInfo.sessionId)!.statusMonitorManager.statusError.value"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div v-if="!activeSessionId" class="pane-placeholder">{{ t('statusMonitor.noActiveSession') }}</div>
|
|
||||||
</div>
|
|
||||||
</pane>
|
|
||||||
|
|
||||||
</splitpanes>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 添加/编辑连接表单模态框 (保持不变) -->
|
<!-- Modals should be outside the main content flow -->
|
||||||
<AddConnectionFormComponent
|
<AddConnectionFormComponent
|
||||||
v-if="showAddEditForm"
|
v-if="showAddEditForm"
|
||||||
:connection-to-edit="connectionToEdit"
|
:connection-to-edit="connectionToEdit"
|
||||||
@@ -383,19 +285,22 @@ onBeforeUnmount(() => {
|
|||||||
@connection-added="handleConnectionAdded"
|
@connection-added="handleConnectionAdded"
|
||||||
@connection-updated="handleConnectionUpdated"
|
@connection-updated="handleConnectionUpdated"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
<LayoutConfigurator
|
||||||
|
:is-visible="showLayoutConfigurator"
|
||||||
|
@close="handleCloseLayoutConfigurator"
|
||||||
|
/>
|
||||||
|
</div> <!-- End of root element -->
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.workspace-view {
|
.workspace-view {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: calc(100vh - 60px - 30px - 2rem); /* 恢复原始高度计算 */
|
height: calc(100vh - 60px - 30px - 2rem); /* 保持原始高度计算 */
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 移除 fixed-command-bar 样式 */
|
|
||||||
|
|
||||||
.main-content-area {
|
.main-content-area {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -403,157 +308,14 @@ onBeforeUnmount(() => {
|
|||||||
border-top: 1px solid #ccc;
|
border-top: 1px solid #ccc;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 为 Pane 添加一些基本样式 */
|
.layout-renderer-wrapper {
|
||||||
/* .pane-with-title 已移除 */
|
|
||||||
.pane-content { /* 让内容区域填充剩余空间 */
|
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
overflow: auto; /* 或者 hidden,根据需要 */
|
width: 100%;
|
||||||
display: flex; /* 内部可能还需要 flex 布局 */
|
height: 100%;
|
||||||
flex-direction: column; /* 默认列方向 */
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-pane, /* 用于左右侧边栏 */
|
/* 面板占位符样式 (用于加载或错误状态) */
|
||||||
.middle-pane, /* 中间包含终端、命令栏、文件管理器的 Pane */
|
|
||||||
.terminal-pane,
|
|
||||||
.command-bar-pane,
|
|
||||||
.file-editor-pane, /* 编辑器窗格样式 */
|
|
||||||
.file-manager-area-pane, /* 文件管理器区域 Pane */
|
|
||||||
.file-manager-pane, /* 内部文件管理器 Pane */
|
|
||||||
.status-monitor-pane, /* 状态监视器样式 */
|
|
||||||
.command-history-pane, /* 命令历史窗格样式 */
|
|
||||||
.quick-commands-pane { /* 快捷指令窗格样式 */
|
|
||||||
display: flex; /* 确保 flex 布局 */
|
|
||||||
flex-direction: column; /* 确保列方向 */
|
|
||||||
overflow: hidden; /* 默认隐藏溢出 */
|
|
||||||
background-color: #f8f9fa; /* 默认背景色 */
|
|
||||||
}
|
|
||||||
.middle-pane {
|
|
||||||
padding: 0; /* 移除 middle-pane 的内边距 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 命令栏 Pane 特定样式 - 恢复原样 */
|
|
||||||
.command-bar-pane {
|
|
||||||
background-color: #e9ecef; /* 背景色 */
|
|
||||||
/* justify-content: center; /* 垂直居中输入框 - 移除此行 */
|
|
||||||
overflow: hidden; /* 内容不应超出 */
|
|
||||||
display: flex; /* 确保 flex 布局 */
|
|
||||||
align-items: center; /* 垂直居中 */
|
|
||||||
}
|
|
||||||
/* 调整内部 CommandInputBar 样式 - 恢复原样 */
|
|
||||||
.command-bar-pane > .command-input-bar {
|
|
||||||
border: none;
|
|
||||||
background-color: transparent;
|
|
||||||
min-height: auto;
|
|
||||||
padding: 2px 0; /* 移除水平内边距 */
|
|
||||||
flex-grow: 1; /* 让输入框填充 */
|
|
||||||
width: 80%; /* 显式设置宽度为100% */
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal-pane {
|
|
||||||
background-color: #f8f9fa; /* 外层 pane 背景 */
|
|
||||||
/* position: relative; 由内部 wrapper 处理 */
|
|
||||||
}
|
|
||||||
.terminal-content-wrapper {
|
|
||||||
background-color: #1e1e1e; /* 终端实际背景 */
|
|
||||||
position: relative; /* 用于占位符 */
|
|
||||||
flex-grow: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow: hidden; /* <-- 重新添加,隐藏外部滚动条 */
|
|
||||||
}
|
|
||||||
.file-editor-pane {
|
|
||||||
background-color: #f8f9fa; /* 外层 pane 背景 */
|
|
||||||
}
|
|
||||||
/* FileEditorContainer 自身需要 flex-grow: 1 */
|
|
||||||
.file-editor-pane > .pane-content {
|
|
||||||
background-color: #2d2d2d; /* 编辑器容器背景 */
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-manager-area-pane {
|
|
||||||
padding: 0;
|
|
||||||
background-color: #f8f9fa; /* 外层 pane 背景 */
|
|
||||||
}
|
|
||||||
/* 内部的 splitpanes 需要 flex-grow: 1 */
|
|
||||||
.file-manager-area-pane > .pane-content {
|
|
||||||
background-color: #f0f0f0; /* 内部区域背景 */
|
|
||||||
}
|
|
||||||
.file-manager-pane { /* 内部文件管理器 Pane */
|
|
||||||
background-color: #ffffff; /* 文件管理器使用浅色背景 */
|
|
||||||
display: flex; /* 确保内部 flex 布局 */
|
|
||||||
flex-direction: column;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.status-monitor-pane {
|
|
||||||
background-color: #f8f9fa; /* 外层 pane 背景 */
|
|
||||||
/* text-align: center; 由内部 wrapper 处理 */
|
|
||||||
/* padding: 1rem; 由内部 wrapper 处理 */
|
|
||||||
}
|
|
||||||
.command-history-pane {
|
|
||||||
background-color: #f8f9fa; /* 与其他侧边栏一致 */
|
|
||||||
}
|
|
||||||
.quick-commands-pane {
|
|
||||||
background-color: #f8f9fa; /* 与其他侧边栏一致 */
|
|
||||||
}
|
|
||||||
.status-monitor-content-wrapper {
|
|
||||||
text-align: center;
|
|
||||||
padding: 1rem;
|
|
||||||
flex-grow: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow: auto; /* 允许内容滚动 */
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* 终端会话包装器 */
|
|
||||||
.terminal-session-wrapper {
|
|
||||||
flex-grow: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow: hidden; /* <-- 重新添加,隐藏外部滚动条 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 文件管理器包装器 (内部组件应填充) */
|
|
||||||
.file-manager-wrapper {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 状态监视器包装器 (内部组件应填充) */
|
|
||||||
.status-monitor-wrapper {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow: hidden; /* 内部组件自己处理滚动 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 终端占位符 */
|
|
||||||
.terminal-placeholder {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
text-align: center;
|
|
||||||
color: #6c757d;
|
|
||||||
padding: 2rem;
|
|
||||||
background-color: #f8f9fa; /* 与 pane 背景一致 */
|
|
||||||
}
|
|
||||||
.terminal-placeholder h2 {
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
font-weight: 300;
|
|
||||||
color: #495057;
|
|
||||||
}
|
|
||||||
.terminal-placeholder p {
|
|
||||||
font-size: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 面板占位符样式 */
|
|
||||||
.pane-placeholder {
|
.pane-placeholder {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -566,49 +328,6 @@ onBeforeUnmount(() => {
|
|||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Splitpanes 默认主题样式调整 */
|
/* 移除旧的、不再需要的特定面板样式,因为渲染由 LayoutRenderer 处理 */
|
||||||
.splitpanes.default-theme .splitpanes__splitter {
|
|
||||||
background-color: #ccc;
|
|
||||||
box-sizing: border-box;
|
|
||||||
position: relative;
|
|
||||||
flex-shrink: 0;
|
|
||||||
border-left: 1px solid #eee; /* 可选:添加细微边框 */
|
|
||||||
border-right: 1px solid #eee;
|
|
||||||
}
|
|
||||||
.splitpanes--vertical > .splitpanes__splitter {
|
|
||||||
width: 7px; /* 垂直分割线宽度 */
|
|
||||||
cursor: col-resize;
|
|
||||||
}
|
|
||||||
.splitpanes--horizontal > .splitpanes__splitter {
|
|
||||||
height: 7px; /* 水平分割线高度 */
|
|
||||||
cursor: row-resize;
|
|
||||||
}
|
|
||||||
.splitpanes.default-theme .splitpanes__splitter:before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
transition: opacity 0.4s;
|
|
||||||
background-color: rgba(0, 0, 0, 0.15);
|
|
||||||
opacity: 0;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
.splitpanes.default-theme .splitpanes__splitter:hover:before {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
.splitpanes.default-theme.splitpanes--vertical > .splitpanes__splitter:before {
|
|
||||||
left: 2px; /* 调整指示器位置 */
|
|
||||||
right: 2px;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
.splitpanes.default-theme.splitpanes--horizontal > .splitpanes__splitter:before {
|
|
||||||
top: 2px; /* 调整指示器位置 */
|
|
||||||
bottom: 2px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 尝试提高中间区域水平分割线的 z-index */
|
|
||||||
.middle-pane .splitpanes--horizontal > .splitpanes__splitter {
|
|
||||||
z-index: 10; /* 确保分割线在内容之上 */
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user