354 lines
11 KiB
Vue
354 lines
11 KiB
Vue
<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>
|