为了在现有的嵌套拖拽组件基础上,添加顶层按网格吸附的父布局,并在子组件内部动态载入预先放置的空位供子组件吸附,同时确保数据能够通过 JSON 恢复,您可以按照以下步骤进行优化。这里只提供需要增加或修改的部分代码。
1. 修改数据结构
增加位置属性和预定义槽位
在顶层组件和需要预定义子组件放置位置的组件中,增加 position
属性和 slots
数组,以便管理网格位置和子组件的放置槽位。
// 示例节点结构
{
id: '1',
type: 'Container',
props: { title: '容器 1' },
position: { x: 0, y: 0 }, // 新增位置属性
slots: [ // 新增槽位属性
{
id: 'slot1',
accepts: ['Item'], // 可以接受的组件类型
children: [] // 当前槽位的子组件
},
{
id: 'slot2',
accepts: ['Item', 'AnotherType'],
children: []
}
],
children: []
}
2. 修改 App.vue
确保顶层布局使用网格,并传递 isTopLevel
属性给 DraggableTree
组件。
<template>
<div id="app">
<h1>嵌套拖拽组件示例</h1>
<button @click="saveToJSON">保存为 JSON</button>
<button @click="loadFromJSON">从 JSON 恢复</button>
<div class="grid-container">
<DraggableTree :nodes="treeData" @update="updateTreeData" :isTopLevel="true" />
</div>
</div>
</template>
<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 },
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>
<style>
#app {
font-family: Arial, sans-serif;
padding: 20px;
}
.grid-container {
position: relative;
width: 800px; /* 根据需要调整 */
height: 600px;
background: #f0f0f0;
border: 1px solid #ccc;
}
</style>
3. 修改 DraggableTree.vue
3.1. 传递 isTopLevel
属性
在递归调用 DraggableTree
组件时,传递 isTopLevel
属性以区分顶层和子层。
<template>
<draggable
v-model="localNodes"
:group="{ name: 'nodes', pull: true, put: true }"
:grid="[20, 20]" <!-- 设置网格大小 -->
@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>
<!-- 渲染预定义槽位 -->
<div class="slots" v-if="node.slots && node.slots.length">
<div
v-for="slot in node.slots"
:key="slot.id"
class="slot"
>
<draggable
v-model="slot.children"
:group="{ name: 'slots', pull: false, put: slot.accepts }"
:disabled="slot.children.length > 0" <!-- 已占用则禁用 -->
@add="onSlotAdd(slot, $event)"
>
<div class="slot-placeholder" v-if="slot.children.length === 0">
放置组件
</div>
<div v-else>
<component
:is="getComponentType(slot.children[0].type)"
:props="slot.children[0].props"
/>
</div>
</draggable>
</div>
</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';
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'
};
}
return {};
},
onDragEnd(evt) {
if (this.isTopLevel) {
const gridSize = 20;
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.$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>
<style>
.grid-container {
position: relative; /* 绝对定位的基础 */
width: 800px; /* 根据需要调整 */
height: 600px;
background: #f0f0f0;
border: 1px solid #ccc;
}
.node {
border: 1px solid #ddd;
padding: 10px;
margin: 5px;
background-color: #fff;
}
.node-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.slots {
margin-top: 10px;
display: flex;
gap: 10px;
}
.slot {
width: 150px;
height: 100px;
border: 1px dashed #aaa;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.slot-placeholder {
color: #aaa;
}
.slot div {
width: 100%;
height: 100%;
}
.node-children {
margin-left: 20px;
}
button {
margin-left: 5px;
}
</style>
3.2. 动态载入子组件占位符
确保每个预定义槽位只能接受特定类型的组件,并且在槽位被占用后禁用放置。上述代码中的 slots
部分已经实现了这一功能。
4. 序列化和反序列化 JSON
确保在保存和加载 JSON 时包含位置和槽位信息。
修改保存和加载方法
在 App.vue
中,位置和槽位信息已经包含在 treeData
中,因此无需额外修改。确保保存和加载时完整保存 treeData
。
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;
}
}
5. 优化拖拽逻辑以支持网格吸附
设置 Vue.Draggable 的网格选项
在 DraggableTree.vue
中,已经通过 :grid="[20, 20]"
实现了网格吸附。您可以根据需要调整网格大小。
确保组件在拖拽结束时对齐到网格
在 onDragEnd
方法中,已经根据拖拽事件的位置对组件进行了网格对齐。
6. 防止在占用槽位后继续放置
上述 DraggableTree.vue
中,通过 :disabled="slot.children.length > 0"
禁用了已被占用的槽位,从而防止了在槽位被占用后继续放置其他组件。
7. 动态载入组件类型
确保根据 type
动态渲染不同类型的组件。上述代码中的 getComponentType
方法已经实现了这一功能。确保您有相应的组件(如 ContainerComponent.vue
和 ItemComponent.vue
)并在父组件中注册。
methods: {
getComponentType(type) {
const map = {
Container: 'ContainerComponent',
Item: 'ItemComponent'
// 添加其他类型映射
};
return map[type] || 'div';
}
}
8. 示例槽位组件
如果需要,可以创建预定义的槽位组件来进一步增强功能。例如,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" />
</div>
</draggable>
</div>
</template>
<script>
export default {
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>
.slot {
width: 150px;
height: 100px;
border: 1px dashed #aaa;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.slot-placeholder {
color: #aaa;
}
</style>
然后在 DraggableTree.vue
中使用 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>
并在 methods
中添加 updateSlotChildren
方法:
methods: {
// 其他方法...
updateSlotChildren(parentNode, slot, newChildren) {
const targetSlot = parentNode.slots.find(s => s.id === slot.id);
if (targetSlot) {
targetSlot.children = newChildren;
}
}
}
结论
通过以上增加和修改的代码,您可以实现顶层组件的网格吸附布局,同时在子组件内部动态加载预定义的槽位,并确保槽位的唯一性和类型限制。这样的优化不仅提升了用户体验,还保证了数据的完整性和一致性。确保在实际应用中根据需求调整网格大小、组件类型映射以及样式等细节。
Comments | NOTHING