feat: 文件树支持右键菜单,以及增删节点操作
This commit is contained in:
parent
e4b5fab2fa
commit
3f693be1bd
@ -1,5 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<Tree ref="tree" :data="treeRenderData" :render="renderContent" expand-node></Tree>
|
<Tree ref="tree" :data="treeRenderData" :render="renderContent" @on-contextmenu="handleContextMenu" expand-node>
|
||||||
|
<template #contextMenu>
|
||||||
|
<DropdownItem v-for="item in currentMenuNode.contextMenuData" :key="item.name"
|
||||||
|
@click="item.handler(currentMenuNode)">
|
||||||
|
{{ item.name }}
|
||||||
|
</DropdownItem>
|
||||||
|
</template>
|
||||||
|
</Tree>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@ -20,6 +27,16 @@ export default {
|
|||||||
suffix: "txt",
|
suffix: "txt",
|
||||||
directory: false,
|
directory: false,
|
||||||
}],
|
}],
|
||||||
|
// 如果定义了 contextMenuData,则开启右键菜单
|
||||||
|
// 需要定义 name 和 handler,分别对应显示的文本和点击事件
|
||||||
|
contextMenuData: [
|
||||||
|
{
|
||||||
|
name: "新建目录",
|
||||||
|
handler: (nodeRenderData) => {
|
||||||
|
console.log(nodeRenderData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
selectedNode: {
|
selectedNode: {
|
||||||
@ -46,7 +63,9 @@ export default {
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
root: null,
|
||||||
treeRenderData: [],
|
treeRenderData: [],
|
||||||
|
currentMenuNode: {},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
@ -58,12 +77,12 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
// 以 treeData 为源数据,渲染文件树
|
// 以 treeData 为源数据,生成完整的文件树数据 treeRenderData
|
||||||
renderTreeByTreeData(currentTreeData) {
|
renderTreeByTreeData(currentTreeData) {
|
||||||
const renderNode = this.renderNodeByNodeData(currentTreeData, 0)
|
const renderNode = this.renderNodeByNodeData(currentTreeData, 0)
|
||||||
this.treeRenderData = renderNode == null ? [] : [renderNode]
|
this.treeRenderData = renderNode == null ? [] : [renderNode]
|
||||||
},
|
},
|
||||||
// 渲染文件树节点。level 表示 nodeData 在文件树的层级(从 0 开始计数)
|
// 生成文件树节点数据。level 表示 nodeData 在文件树的层级(从 0 开始计数)
|
||||||
renderNodeByNodeData(nodeData, level) {
|
renderNodeByNodeData(nodeData, level) {
|
||||||
// 判断必须包含的字段,没有则返回 null
|
// 判断必须包含的字段,没有则返回 null
|
||||||
if (nodeData == null || nodeData == undefined) {
|
if (nodeData == null || nodeData == undefined) {
|
||||||
@ -71,8 +90,7 @@ export default {
|
|||||||
}
|
}
|
||||||
if (nodeData.path == undefined ||
|
if (nodeData.path == undefined ||
|
||||||
nodeData.name == undefined ||
|
nodeData.name == undefined ||
|
||||||
nodeData.directory == undefined ||
|
nodeData.directory == undefined
|
||||||
(nodeData.directory == true && nodeData.nodes == undefined)
|
|
||||||
) {
|
) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@ -97,9 +115,19 @@ export default {
|
|||||||
// 附带数据,不直接参与渲染
|
// 附带数据,不直接参与渲染
|
||||||
path: nodeData.path,
|
path: nodeData.path,
|
||||||
level: level,
|
level: level,
|
||||||
|
sourceData: nodeData, // 保留源数据。重新渲染节点时需要使用
|
||||||
|
}
|
||||||
|
if (nodeData.contextMenuData && nodeData.contextMenuData.length > 0) {
|
||||||
|
// contextmenu = true 用于开启右键菜单
|
||||||
|
nodeRenderData.contextmenu = true
|
||||||
|
// contextMenuData 是渲染右键菜单的源数据
|
||||||
|
nodeRenderData.contextMenuData = nodeData.contextMenuData
|
||||||
}
|
}
|
||||||
if (nodeData.directory) {
|
if (nodeData.directory) {
|
||||||
let childrenRenderData = []
|
if (nodeData.nodes === undefined) {
|
||||||
|
nodeData.nodes = [];
|
||||||
|
}
|
||||||
|
let childrenRenderData = [];
|
||||||
nodeData.nodes.forEach(node => {
|
nodeData.nodes.forEach(node => {
|
||||||
const renderNode = this.renderNodeByNodeData(node, level + 1)
|
const renderNode = this.renderNodeByNodeData(node, level + 1)
|
||||||
if (renderNode != null) {
|
if (renderNode != null) {
|
||||||
@ -111,9 +139,62 @@ export default {
|
|||||||
});
|
});
|
||||||
nodeRenderData.children = childrenRenderData
|
nodeRenderData.children = childrenRenderData
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果 input 为 true,保留源数据,然后渲染成 input 框,并绑定输入完成事件
|
||||||
|
if (nodeData.input === true) {
|
||||||
|
// 标识渲染为 input 框
|
||||||
|
nodeRenderData.input = true;
|
||||||
|
// 当输入完成后,触发的完成事件
|
||||||
|
nodeRenderData.completer = nodeData.completer ?
|
||||||
|
nodeData.completer : (data, value) => {
|
||||||
|
this.$Message.error("Input Node have no completer.");
|
||||||
|
};
|
||||||
|
// 源数据中的 input 标识需要删除。当输入取消时,用源数据进行恢复节点
|
||||||
|
delete nodeData.input;
|
||||||
|
delete nodeData.completer;
|
||||||
|
}
|
||||||
return nodeRenderData
|
return nodeRenderData
|
||||||
},
|
},
|
||||||
|
// 负责文件树每个节点的渲染,决策每个节点的样式
|
||||||
renderContent(h, { root, node, data }) {
|
renderContent(h, { root, node, data }) {
|
||||||
|
// 将 root 树存储起来,便于后续操作文件树增删节点
|
||||||
|
this.root = root;
|
||||||
|
// 给 data 绑定 nodeKey 和 parentKey,便于向上索引
|
||||||
|
data.nodeKey = node.nodeKey;
|
||||||
|
data.parentKey = node.parent;
|
||||||
|
// 将节点渲染为输入框,并绑定输入完成事件
|
||||||
|
if (data.input) {
|
||||||
|
return h('span', {
|
||||||
|
style: {
|
||||||
|
display: 'inline-block',
|
||||||
|
width: '100%'
|
||||||
|
},
|
||||||
|
}, [
|
||||||
|
h('span', [
|
||||||
|
// h(resolveComponent('Input'), {
|
||||||
|
// 使用 resolveComponent 时 blur 事件失效,改用原生 input 框
|
||||||
|
h('Input', {
|
||||||
|
type: 'text',
|
||||||
|
autofocus: true,
|
||||||
|
autoCorrect: "off",
|
||||||
|
spellCheck: "false",
|
||||||
|
class: 'ivu-input ivu-input-default',
|
||||||
|
style: { marginRight: '8px' },
|
||||||
|
value: data.title,
|
||||||
|
// 当 input 框触发 enter 或 blur 事件时,执行 completer 回调
|
||||||
|
onKeyup: (event) => {
|
||||||
|
if (event.key === "Enter") {
|
||||||
|
data.completer(data, event.target.value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onBlur: (event) => {
|
||||||
|
data.completer(data, event.target.value);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
// 其他正常节点渲染事件
|
||||||
return h('span', {
|
return h('span', {
|
||||||
style: {
|
style: {
|
||||||
display: 'inline-block',
|
display: 'inline-block',
|
||||||
@ -132,9 +213,10 @@ export default {
|
|||||||
])
|
])
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
selectNode(nodeData) {
|
// 当点击某个节点时,设置选中状态,并触发事件
|
||||||
|
selectNode(nodeRenderData) {
|
||||||
// 文件夹不做任何动作
|
// 文件夹不做任何动作
|
||||||
if (nodeData.directory) {
|
if (nodeRenderData.directory) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -143,10 +225,67 @@ export default {
|
|||||||
selectedNodes.forEach(element => {
|
selectedNodes.forEach(element => {
|
||||||
element.selected = false
|
element.selected = false
|
||||||
});
|
});
|
||||||
nodeData.selected = true
|
nodeRenderData.selected = true
|
||||||
|
|
||||||
// 向父组件暴露当前选中的文件
|
// 向父组件暴露当前选中的文件
|
||||||
this.$emit('file-selected', nodeData);
|
this.$emit('file-selected', nodeRenderData);
|
||||||
|
},
|
||||||
|
// 在父节点插入子节点
|
||||||
|
insertTreeNode(parentRenderData, nodeData, index) {
|
||||||
|
const renderNode = this.renderNodeByNodeData(nodeData, parentRenderData.level + 1);
|
||||||
|
// 由于文件树有过滤规则,此处 renderNode 可能为 null
|
||||||
|
if (renderNode == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (index === -1) {
|
||||||
|
parentRenderData.children.push(renderNode);
|
||||||
|
} else {
|
||||||
|
parentRenderData.children.splice(index, 0, renderNode);
|
||||||
|
}
|
||||||
|
return renderNode;
|
||||||
|
},
|
||||||
|
// 为一个子节点寻找父节点。返回值:父节点,以及子节点在父节点中的位置
|
||||||
|
findNodeParent(nodeRenderData) {
|
||||||
|
try {
|
||||||
|
const parentKey = this.root.find(el => el.nodeKey === nodeRenderData.nodeKey).parent;
|
||||||
|
const parent = this.root.find(el => el.nodeKey === parentKey).node;
|
||||||
|
const index = parent.children.indexOf(nodeRenderData);
|
||||||
|
return [parent, index];
|
||||||
|
} catch (err) {
|
||||||
|
console.log("find parentNode failed.");
|
||||||
|
return [null, -1];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 给父节点增加子节点
|
||||||
|
appendTreeNode(parentRenderData, nodeData) {
|
||||||
|
return this.insertTreeNode(parentRenderData, nodeData, -1);
|
||||||
|
},
|
||||||
|
// 删除文件树中某个子节点。返回值:是否成功,父节点
|
||||||
|
removeTreeNode(nodeRenderData) {
|
||||||
|
const [parent, index] = this.findNodeParent(nodeRenderData);
|
||||||
|
if (parent === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
parent.children.splice(index, 1);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
// 让一个子节点在父节点下重新渲染
|
||||||
|
reRenderTreeNode(nodeRenderData) {
|
||||||
|
// 先找到子节点的父节点
|
||||||
|
const [parentRenderData, indexOfParent] = this.findNodeParent(nodeRenderData);
|
||||||
|
if (parentRenderData === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// 将节点从文件树上删去
|
||||||
|
if (this.removeTreeNode(nodeRenderData) === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// 然后使用新的 nodeData 挂载到父节点下,以重新渲染该节点
|
||||||
|
return this.insertTreeNode(parentRenderData, nodeRenderData.sourceData, indexOfParent);
|
||||||
|
},
|
||||||
|
// 对某个节点进行右键操作时,给 currentMenuNode 赋值,便于渲染右键菜单
|
||||||
|
handleContextMenu(nodeRenderData) {
|
||||||
|
this.currentMenuNode = nodeRenderData;
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user