为了在现有的嵌套拖拽组件基础上,实现拖拽结束时组件自动缩放以撑满网格或槽位,您需要对以下部分进行修改和增加:
- 定义网格单元格尺寸
- 调整
getNodeStyle
方法 - 修改 CSS 以确保组件撑满容器
- 确保槽位内的组件自动填充
以下是具体的实现步骤和代码片段,仅包含需要增加或修改的部分。
1. 定义网格单元格尺寸
首先,明确网格单元格的宽度和高度,以便在拖拽结束时调整组件的尺寸和位置。
// 在 DraggableTree.vue 中定义网格尺寸
const GRID_SIZE = 100; // 每个网格单元格的宽度和高度,例如 100px
2. 调整 getNodeStyle
方法
修改 getNodeStyle
方法,使顶层组件在拖拽结束后自动缩放并填满网格单元格,子组件在槽位中自动填满其父容器。
// DraggableTree.vue
<script>
import draggable from 'vuedraggable';
import { v4 as uuidv4 } from 'uuid';
const GRID_SIZE = 100; // 定义网格大小
export default {
name: 'DraggableTree',
components: {
draggable,
DraggableTree: () => import('./DraggableTree.vue')
},
props: {
nodes: {
type: Array,
required: true
},
isTopLevel: {
type: Boolean,
default: false
}
},
data() {
return {
localNodes: JSON.parse(JSON.stringify(this.nodes)) // 深拷贝
};
},
watch: {
localNodes: {
handler(newVal) {
this.$emit('update', newVal);
},
deep: true
},
nodes: {
handler(newVal) {
this.localNodes = JSON.parse(JSON.stringify(newVal));
},
deep: true
}
},
methods: {
getNodeStyle(node) {
if (this.isTopLevel) {
return {
position: 'absolute',
left: node.position.x + 'px',
top: node.position.y + 'px',
width: `${GRID_SIZE}px`, // 设置宽度为网格大小
height: `${GRID_SIZE}px` // 设置高度为网格大小
};
}
// 对于非顶层组件,填满其父容器
return {
width: '100%',
height: '100%'
};
},
onDragEnd(evt) {
if (this.isTopLevel) {
const gridSize = GRID_SIZE;
const x = Math.round(evt.event.x / gridSize) * gridSize;
const y = Math.round(evt.event.y / gridSize) * gridSize;
// 更新组件的位置并确保其尺寸
this.localNodes[evt.newIndex].position = { x, y };
this.localNodes[evt.newIndex].width = gridSize;
this.localNodes[evt.newIndex].height = gridSize;
}
this.$emit('update', this.localNodes);
},
// 其他方法保持不变
addChild(node) {
if (!node.slots) {
// 如果节点没有预定义槽位,则不允许添加子组件
return;
}
const emptySlot = node.slots.find(slot => slot.children.length === 0);
if (emptySlot) {
const newNode = {
id: uuidv4(),
type: 'Item', // 或其他类型
props: { content: '新项目' },
children: []
};
emptySlot.children.push(newNode);
} else {
alert('所有槽位均已被占用');
}
},
removeNode(node) {
const removeRecursively = (nodes, target) => {
const index = nodes.findIndex(n => n.id === target.id);
if (index !== -1) {
nodes.splice(index, 1);
return true;
}
for (let n of nodes) {
if (n.children && n.children.length) {
if (removeRecursively(n.children, target)) {
return true;
}
}
// 检查槽位
if (n.slots && n.slots.length) {
for (let slot of n.slots) {
if (slot.children && slot.children.length) {
if (removeRecursively(slot.children, target)) {
return true;
}
}
}
}
}
return false;
};
removeRecursively(this.localNodes, node);
},
updateChildren(parentNode, updatedChildren) {
parentNode.children = updatedChildren;
},
editNode(node) {
if (node.type === 'Container') {
const newTitle = prompt('修改标题', node.props.title);
if (newTitle !== null) {
node.props.title = newTitle;
}
} else if (node.type === 'Item') {
const newContent = prompt('修改内容', node.props.content);
if (newContent !== null) {
node.props.content = newContent;
}
}
},
onSlotAdd(slot, event) {
// 确保槽位只能接受特定类型的组件
const draggedItem = event.item.__vue__.node;
if (!slot.accepts.includes(draggedItem.type)) {
// 如果类型不匹配,移除该组件
slot.children.pop();
alert('该槽位不接受此类型的组件');
}
},
getComponentType(type) {
const map = {
Container: 'ContainerComponent',
Item: 'ItemComponent'
// 添加其他类型映射
};
return map[type] || 'div';
}
}
};
</script>
3. 修改 CSS 以确保组件撑满容器
更新 CSS 样式,使顶层组件和槽位内的子组件能够自动填满其容器。
/* DraggableTree.vue 的样式部分 */
<style>
.grid-container {
position: relative; /* 绝对定位的基础 */
width: 800px; /* 根据需要调整 */
height: 600px;
background: #f0f0f0;
border: 1px solid #ccc;
}
.node {
border: 1px solid #ddd;
padding: 10px;
margin: 0; /* 去除 margin 以确保填满网格 */
background-color: #fff;
box-sizing: border-box; /* 包含 padding 和 border 在内 */
display: flex;
flex-direction: column;
justify-content: space-between;
}
.node-header {
display: flex;
justify-content: space-between;
align-items: center;
flex-grow: 1;
}
.slots {
margin-top: 10px;
display: flex;
gap: 10px;
flex-grow: 1;
}
.slot {
width: 100%; /* 填满父容器 */
height: 100%; /* 填满父容器 */
border: 1px dashed #aaa;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.slot-placeholder {
color: #aaa;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.slot div {
width: 100%;
height: 100%;
}
.node-children {
margin-left: 20px;
flex-grow: 1;
}
button {
margin-left: 5px;
}
.slot > div > .node {
width: 100%; /* 确保子组件填满槽位 */
height: 100%;
}
</style>
4. 确保槽位内的组件自动填充
确保槽位内的子组件在渲染时自动填充其父容器。可以通过在子组件中设置 width
和 height
为 100%
来实现。
<!-- 例如,ItemComponent.vue -->
<template>
<div class="item-component">
<!-- 组件内容 -->
{{ props.content }}
</div>
</template>
<script>
export default {
name: 'ItemComponent',
props: {
props: {
type: Object,
required: true
}
}
};
</script>
<style scoped>
.item-component {
width: 100%;
height: 100%;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
background-color: #e0f7fa;
border: 1px solid #00796b;
}
</style>
同样地,确保其他类型的组件(如 ContainerComponent.vue
)也具有相似的样式以填满其父容器。
5. 更新槽位组件(可选)
为了更好地管理槽位内的组件,可以创建一个独立的槽位组件,如 SlotComponent.vue
,并在其中处理自动填充和尺寸调整。
<!-- SlotComponent.vue -->
<template>
<div class="slot">
<draggable
v-model="children"
:group="{ name: 'slots', pull: false, put: accepts }"
:disabled="children.length > 0"
@add="onAdd"
>
<div class="slot-placeholder" v-if="children.length === 0">
放置组件
</div>
<div v-else>
<component
:is="getComponentType(children[0].type)"
:props="children[0].props"
class="filler-component"
/>
</div>
</draggable>
</div>
</template>
<script>
export default {
name: 'SlotComponent',
props: {
slot: {
type: Object,
required: true
}
},
computed: {
children: {
get() {
return this.slot.children;
},
set(value) {
this.$emit('update:children', value);
}
},
accepts() {
return this.slot.accepts;
}
},
methods: {
onAdd(event) {
const draggedItem = event.item.__vue__.node;
if (!this.accepts.includes(draggedItem.type)) {
// 如果类型不匹配,移除该组件
this.children.pop();
alert('该槽位不接受此类型的组件');
}
},
getComponentType(type) {
const map = {
Container: 'ContainerComponent',
Item: 'ItemComponent'
// 添加其他类型映射
};
return map[type] || 'div';
}
}
};
</script>
<style scoped>
.slot {
width: 100%;
height: 100%;
border: 1px dashed #aaa;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.slot-placeholder {
color: #aaa;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.filler-component {
width: 100%;
height: 100%;
}
</style>
然后在 DraggableTree.vue
中使用 SlotComponent
:
<!-- DraggableTree.vue 中的槽位部分 -->
<template>
<draggable
v-model="localNodes"
:group="{ name: 'nodes', pull: true, put: true }"
:grid="[GRID_SIZE, GRID_SIZE]" <!-- 设置网格大小 -->
@end="onDragEnd"
:sort="false" <!-- 使用绝对定位时禁用排序 -->
class="grid-container"
>
<div
v-for="node in localNodes"
:key="node.id"
class="node"
:style="getNodeStyle(node)"
>
<div class="node-header">
<span>{{ node.type }} - {{ node.props.title || node.props.content }}</span>
<div>
<button @click="editNode(node)">编辑</button>
<button @click="addChild(node)">添加子组件</button>
<button @click="removeNode(node)">删除</button>
</div>
</div>
<!-- 使用 SlotComponent 渲染槽位 -->
<div class="slots" v-if="node.slots && node.slots.length">
<SlotComponent
v-for="slot in node.slots"
:key="slot.id"
:slot="slot"
@update:children="(newChildren) => updateSlotChildren(node, slot, newChildren)"
/>
</div>
<!-- 递归渲染子节点 -->
<div class="node-children" v-if="node.children && node.children.length">
<DraggableTree
:nodes="node.children"
@update="updateChildren(node, $event)"
:isTopLevel="false"
/>
</div>
</div>
</draggable>
</template>
<script>
import draggable from 'vuedraggable';
import { v4 as uuidv4 } from 'uuid';
import SlotComponent from './SlotComponent.vue'; // 引入 SlotComponent
const GRID_SIZE = 100; // 定义网格大小
export default {
name: 'DraggableTree',
components: {
draggable,
DraggableTree: () => import('./DraggableTree.vue'),
SlotComponent
},
props: {
nodes: {
type: Array,
required: true
},
isTopLevel: {
type: Boolean,
default: false
}
},
data() {
return {
localNodes: JSON.parse(JSON.stringify(this.nodes)) // 深拷贝
};
},
watch: {
localNodes: {
handler(newVal) {
this.$emit('update', newVal);
},
deep: true
},
nodes: {
handler(newVal) {
this.localNodes = JSON.parse(JSON.stringify(newVal));
},
deep: true
}
},
methods: {
getNodeStyle(node) {
if (this.isTopLevel) {
return {
position: 'absolute',
left: node.position.x + 'px',
top: node.position.y + 'px',
width: `${GRID_SIZE}px`, // 设置宽度为网格大小
height: `${GRID_SIZE}px` // 设置高度为网格大小
};
}
// 对于非顶层组件,填满其父容器
return {
width: '100%',
height: '100%'
};
},
onDragEnd(evt) {
if (this.isTopLevel) {
const gridSize = GRID_SIZE;
const x = Math.round(evt.event.x / gridSize) * gridSize;
const y = Math.round(evt.event.y / gridSize) * gridSize;
// 更新组件的位置并确保其尺寸
this.localNodes[evt.newIndex].position = { x, y };
this.localNodes[evt.newIndex].width = gridSize;
this.localNodes[evt.newIndex].height = gridSize;
}
this.$emit('update', this.localNodes);
},
addChild(node) {
if (!node.slots) {
// 如果节点没有预定义槽位,则不允许添加子组件
return;
}
const emptySlot = node.slots.find(slot => slot.children.length === 0);
if (emptySlot) {
const newNode = {
id: uuidv4(),
type: 'Item', // 或其他类型
props: { content: '新项目' },
children: []
};
emptySlot.children.push(newNode);
} else {
alert('所有槽位均已被占用');
}
},
removeNode(node) {
const removeRecursively = (nodes, target) => {
const index = nodes.findIndex(n => n.id === target.id);
if (index !== -1) {
nodes.splice(index, 1);
return true;
}
for (let n of nodes) {
if (n.children && n.children.length) {
if (removeRecursively(n.children, target)) {
return true;
}
}
// 检查槽位
if (n.slots && n.slots.length) {
for (let slot of n.slots) {
if (slot.children && slot.children.length) {
if (removeRecursively(slot.children, target)) {
return true;
}
}
}
}
}
return false;
};
removeRecursively(this.localNodes, node);
},
updateChildren(parentNode, updatedChildren) {
parentNode.children = updatedChildren;
},
editNode(node) {
if (node.type === 'Container') {
const newTitle = prompt('修改标题', node.props.title);
if (newTitle !== null) {
node.props.title = newTitle;
}
} else if (node.type === 'Item') {
const newContent = prompt('修改内容', node.props.content);
if (newContent !== null) {
node.props.content = newContent;
}
}
},
updateSlotChildren(parentNode, slot, newChildren) {
const targetSlot = parentNode.slots.find(s => s.id === slot.id);
if (targetSlot) {
targetSlot.children = newChildren;
}
}
}
};
</script>
6. 确保 JSON 数据包含尺寸信息
为了在从 JSON 恢复组件时保持尺寸信息,确保在保存和加载时包含 width
和 height
属性。
// App.vue 中的保存和加载方法
<script>
import DraggableTree from './components/DraggableTree.vue';
export default {
name: 'App',
components: { DraggableTree },
data() {
return {
treeData: [
{
id: '1',
type: 'Container',
props: { title: '默认容器' },
position: { x: 0, y: 0 },
width: 100, // 初始宽度
height: 100, // 初始高度
slots: [
{ id: 'slot1', accepts: ['Item'], children: [] },
{ id: 'slot2', accepts: ['Item'], children: [] }
],
children: []
}
]
};
},
methods: {
saveToJSON() {
const json = JSON.stringify(this.treeData, null, 2);
localStorage.setItem('treeData', json);
alert('已保存为 JSON');
},
loadFromJSON() {
const json = localStorage.getItem('treeData');
if (json) {
this.treeData = JSON.parse(json);
alert('已从 JSON 恢复');
} else {
alert('没有找到保存的 JSON 数据');
}
},
updateTreeData(newData) {
this.treeData = newData;
}
}
};
</script>
7. 测试和调整
完成上述修改后,确保测试以下场景:
- 顶层组件拖拽到网格:组件应自动调整大小并填满拖拽到的网格单元格。
- 子组件拖拽到槽位:子组件应自动填满槽位,并且槽位在被占用后不允许再次放置组件。
- 从 JSON 恢复:加载的 JSON 数据应正确恢复组件的层级、位置和尺寸。
8. 进一步优化(可选)
使用动态计算尺寸
如果需要更灵活的网格布局,可以根据容器大小动态计算网格单元格的尺寸。
Comments | NOTHING