前言
这是一个非常基础的 组织树 前端项目,不需要搭建环境,只需要引入一个 vue 库即可,复制粘贴代码块到 TXT 文本另存为 html 双击打开就能用。
我是没想到这次考试内容居然是在 A4 纸上用水性笔手写出来一棵树并带有增删改查功能。1个半小时的考试时间,提示可以用任意框架,我就是个傻X非要钻牛角尖偏不使用成熟的框架、或者内置写好的组件,导致代码只写完了一部分,最后喜提50分,就差10分及格气死我了,所以考完试复盘写下了这个 Demo 顺便复习一下 CSS、JS。
功能
1.展示默认树内容;
2.鼠标悬浮选中树节点右键弹出菜单;
3.添加节点、删除节点、修改节点。
代码
先说个 BUG ,直接点 CSDN 复制代码会导致鼠标悬浮的 onmouseover Unicode 开头字符 o
变成 ο
,然后悬浮效果获取节点 ID 功能就失效了,点击复制后,务必手动替换一下 onmouseover,文章下方附有图片解释这个问题。
展示效果图放在最后面了,先上代码:
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>menu</title>
<script src="./vue.min.js"></script>
</head>
<body>
<div id="app">
<p>选择节点ID:{{hoverId}} 下一个ID:{{lastId}}</p>
<div class="box" @click.right="fnRightClick($event)" @click.left="fnLeftClick($event)">
<div class="tree" v-html="treeHtml"></div>
</div>
<div class="menu" v-if="menuVisibleState" :style="{left: menuLeft,top: menuTop}">
<div class="btn" @click="fnOpenCreate()"><span>新增</span></div>
<div class="btn" @click="fnOpenModify()"><span>修改</span></div>
<div class="btn" @click="fnDelete()"><span>删除</span></div>
</div>
<div class="wrapper" v-if="formVisibleState">
<div class="dialog">
<div class="form">
<div>
<label for="name">组织名称:</label>
<input type="text" id="name" v-model="name">
</div>
<div>
<label for="desc">组织描述:</label>
<textarea rows="10" id="desc" v-model="desc"></textarea>
</div>
<div class="btn-group">
<button @click="fnCommit()">提交</button>
<button @click="fnCloseCreate()">取消</button>
</div>
</div>
</div>
</div>
</div>
</body>
<script>
const app = new Vue({
el: '#app',
component: {},
data: {
id: '',
name: '',
desc: '',
treeHtml: '',
hoverId: '',
lastId: 12,
detail: {},
menuLeft: 0,
menuTop: 0,
menuVisibleState: false,
formVisibleState: false,
orgDataList: [
{
id: 1, label: '1', desc: '',
children: [
{
id: 4, label: '1-1', desc: '',
children: [
{id: 5, label: '1-1-1', desc: '', children: []}
]
},
{
id: 10, label: '1-2', desc: '',
children: [
{id: 11, label: '1-2-1', desc: '', children: []}
]
}
]
},
{
id: 2, label: '2', desc: '',
children: [
{
id: 6, label: '2-1', desc: '',
children: [
{id: 7, label: '2-1-1', desc: '', children: []}
]
}
]
},
{
id: 3, label: '3', desc: '',
children: [
{
id: 8, label: '3-1', desc: '',
children: [
{id: 9, label: '3-1-1', desc: '', children: []}
]
}
]
}
],
},
mounted() {
},
method: {}
});
// 打开创建窗口
function fnOpenCreate() {
app.menuVisibleState = false
app.formVisibleState = true
}
// 打开修改窗口
function fnOpenModify() {
app.menuVisibleState = false
app.formVisibleState = true
fnOperateNode(app.hoverId, [], app.orgDataList, 'detail')
app.id = app.detail.id
app.name = app.detail.label
app.desc = app.detail.desc
app.children = app.detail.children
}
// 删除
function fnDelete() {
fnOperateNode(app.hoverId, [], app.orgDataList, 'delete')
app.menuVisibleState = false
}
// 关闭创建窗口
function fnCloseCreate() {
app.formVisibleState = false
}
// 左键事件
function fnLeftClick() {
app.menuVisibleState = false
}
// 右键事件
function fnRightClick(event) {
event.preventDefault()
let e = event
app.menuLeft = e.clientX + "px"
app.menuTop = e.clientY + "px"
app.menuVisibleState = true
}
// 提交
function fnCommit() {
if (app.id) {
let val = {
id: app.id,
label: app.name,
desc: app.desc,
children: app.children
}
fnOperateNode(app.id, val, app.orgDataList, 'modify')
fnGeneratorTreeNode(app.orgDataList)
app.id = ''
} else {
let val = {
id: app.lastId,
label: app.name,
desc: app.desc,
children: []
}
if (app.hoverId) {
fnOperateNode(app.hoverId, val, app.orgDataList, 'create')
} else {
app.$set(app.orgDataList, app.orgDataList.length, val, 'create')
fnGeneratorTreeNode(app.orgDataList)
}
app.lastId = app.lastId + 1
}
app.formVisibleState = false
app.id = ''
app.name = ''
app.desc = ''
app.children = []
}
// 查找、操作节点
function fnOperateNode(id, val, dataList, type) {
if (dataList && dataList.length) {
for (let i = 0; i < dataList.length; i++) {
if (dataList[i] && dataList[i].id == id) {
if (type == 'create') {
app.$set(dataList[i].children, dataList[i].children.length, val)
fnGeneratorTreeNode(app.orgDataList)
} else if (type == 'detail') {
app.detail = dataList[i]
} else if (type == 'modify') {
dataList[i] = val
fnGeneratorTreeNode(app.orgDataList)
} else if (type == 'delete') {
dataList.splice(i,1)
console.log(app.orgDataList)
fnGeneratorTreeNode(app.orgDataList)
}
} else if (dataList[i].children && dataList[i].children.length) {
fnOperateNode(id, val, dataList[i].children, type)
}
}
}
}
// 悬浮选中ID
function fnSetHoverId(id) {
app.hoverId = id
}
// 移除悬浮选中ID
function fnUnsetHoverId() {
app.hoverId = ''
}
// 生成根节点 html
function fnGeneratorTreeNode(dataList) {
app.treeHtml = ''
dataList.forEach((item) => {
if (item) {
app.treeHtml += '<div class="node"><div class="content"><span οnmοuseοver="fnSetHoverId(' + item.id + ')">' + item.label + '</span>';
if (item.children && item.children.length) {
app.treeHtml += fnGeneratorLeafNode(item.children)
}
app.treeHtml += '</div></div>'
}
})
}
// 生成叶节点 html
function fnGeneratorLeafNode(dataList) {
let content = ''
for (let i = 0; i < dataList.length; i++) {
content += '<div class="node"><div class="content"><span οnmοuseοver="fnSetHoverId(' + dataList[i].id + ')">' + dataList[i].label + '</span>';
if (dataList[i].children && dataList[i].children.length) {
content += fnGeneratorLeafNode(dataList[i].children)
}
content += '</div></div>'
}
return content
}
fnGeneratorTreeNode(app.orgDataList)
</script>
<style>
* {
padding: 0;
margin: 0;
}
html, body {
width: 100%;
height: 100%;
}
ul {
list-style: none;
}
#app {
width: 100%;
height: 100%;
}
.box {
width: 100%;
height: 100%;
background-color: #ffffff;
}
.menu {
width: 100px;
height: auto;
position: fixed;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border: 1px solid #000000;
border-radius: 5px;
background-color: #ffffff;
}
.menu .btn {
width: 80px;
height: 20px;
padding: 10px;
text-align: center;
}
.menu .btn:hover {
background-color: black;
}
.menu .btn:hover span {
color: #ffffff;
}
.tree {
padding: 50px;
}
.node {
padding-left: 20px;
width: 100%;
height: 100%;
cursor: pointer;
}
.wrapper {
width: 100%;
height: 100%;
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 1000;
background-color: black;
opacity: 0.7;
text-align: center;
}
.dialog {
width: 320px;
height: 300px;
z-index: 1001;
background-color: #ffffff;
display: inline-block;
vertical-align: middle;
text-align: left;
}
.wrapper:after {
height: 100%;
width: 0px;
content: "";
display: inline-block;
text-align: center;
vertical-align: middle;
}
.form {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-items: start;
justify-content: left;
padding: 20px;
}
.form input {
width: 280px;
line-height: 25px;
margin-bottom: 10px;
}
.form textarea {
width: 280px;
}
button {
width: 50px;
height: 30px;
}
label {
font-size: 14px;
}
.btn-group {
margin-bottom: -1px;
}
</style>
</html>
展示效果
右键显示操作菜单
弹窗
直接复制代码导致鼠标悬浮事件失效