vue纯手写思维导图,拒绝插件(cv即用)
已完成功能点:折叠、放大、缩小、移动
后续增加功能点:添加、删除
先看结果:
有这么个需求,按照层级关系,把表格放在思维导图上,我第一时间想到用插件,但是找了好久都没有找到比较合适的插件,决定自己手写一个。
第一步:
理论猜想
模拟一个带有层级关系的数据格式,并且可以在vue组件中需要做成组件递归形势,左侧父级永远包含右侧子集。左侧A盒子,右侧F盒子用flex布局B/C/D竖着排列,右侧3个div分别用伪元素分别做3根横线,F盒子设置border-left
竖线,这样一拼接就感觉像是一个思维导图了,理论先这样,但是还没有想到B盒子的左侧横线和F盒子竖线交叉之后,上面圆圈多余的部分怎么去除。先动手再说,碰到问题再想着怎么处理问题。
第二步:
动手实践
模拟数据: 设置listCache
模拟数据 带有层级关系的格式,id是唯一的,这样做为了后期可能操作表格的时候方便找到唯一的表格。
递归组件: mindItem.vue
里面的name属性名称设置 mindItem
,然后再mindItem.vue
组件里面再次引入<mindItem :list="item.children"></mindItem>
即可递归
多余线段去除: 刚开始的做法是直接设置子集的border-left
,这样的问题会造成有线段空出来,显得很多余,转换一个思路。
- 设置B和A的连接:请看图2 把第一个div的伪元素
::after
设置border-left: solid 2px blue;height: 50%;bottom: 0;
这样做是让线段1向下展示,高度只有B盒子的一半,这样就感觉像线段拐弯了,从A连接到B的样式,其实是多个线段拼接起来而已。 - 设置D和A的连接:请看图2 把第最后一个div的伪元素
::after
设置border-left: solid 2px #000; height: 50%; top: 0;
这样做是让线段3向上展示,高度只有C盒子的一半,这样就感觉像线段拐弯了,从A连接到C的样式,其实是多个线段拼接起来而已。 - 设置C和A的连接:请看图2 把中间div的伪元素
::after
设置border-left: solid 2px yellowgreen; height: 100%;
处在中间地段的div盒子不必考虑线段拐弯问题,高度100%就行了和上下的盒子的线段连接起来就好了
图2:
src/views/mind/components/mindItem.vue
<template>
<transition name="el-zoom-in-center">
<div class="warps">
<template v-for="(item, i) in list">
<div
:key="i"
class="bodyDefault"
:class="[
item.first ? 'bodyOuter' : '',
i === 0 ? 'bodyFirst' : list.length - 1 === i ? 'bodyLast' : '',
]"
>
<i
v-if="!item.first"
class="iconremove"
:class="[
!item.isExpandBefore
? 'el-icon-remove-outline'
: 'el-icon-circle-plus-outline',
]"
type="primary"
@click="expendBefore(item)"
>
</i>
<div class="listTable" v-show="!item.isExpandBefore">
<el-table :data="item.tableData" style="width: 300px" border size="small">
<el-table-column prop="name" label="姓名" align="center"> </el-table-column>
<el-table-column prop="age" label="年龄" align="center"> </el-table-column>
</el-table>
</div>
<i
v-if="item.children && !item.isExpandBefore"
class="iconremove"
:class="[
!item.isExpandAfter
? 'el-icon-remove-outline'
: 'el-icon-circle-plus-outline',
]"
@click="expendAfter(item)"
>
</i>
<div
v-if="item.children && !item.isExpandAfter && !item.isExpandBefore"
class="box transition-box"
>
<mindItem :list="item.children"></mindItem>
</div>
</div>
</template>
</div>
</transition>
</template>
<script>
import { expendfn } from "./index.js";
export default {
name: "mindItem",
components: {},
props: {
list: {
type: Array,
default: [],
},
},
data() {
return {};
},
computed: {},
watch: {
list: {
deep: true,
handler(newVal) {
this.list = newVal;
},
},
},
created() {},
mounted() {},
methods: {
expendBefore(val) {
val.isExpandBefore = !val.isExpandBefore;
this.$forceUpdate();
console.log("后-expendBefore", val);
},
expendAfter(val) {
val.isExpandAfter = !val.isExpandAfter;
this.$forceUpdate();
console.log("前-expendAfter", val);
},
},
};
</script>
<style scoped lang="less">
.warps {
& > .bodyOuter,
& > .bodyFirst,
& > .bodyLast,
& > .bodyDefault {
padding: 10px 0 10px 24px;
position: relative;
border-left: none;
.listTable {
display: inline-block;
display: flex;
align-items: center;
.expend {
width: 10px;
height: 100%;
// border: 1px solid blue;
}
}
display: flex;
align-items: center;
.box {
flex: 1;
margin-left: 30px;
display: inline-block;
position: relative;
}
.box::before {
content: "";
width: 30px;
border: solid 1px skyblue;
white-space: nowrap;
display: inline-block;
position: absolute;
left: -15px;
top: 50%;
transform: translate(-50%, -50%);
}
}
& > .bodyFirst::before,
& > .bodyOuter::before,
& > .bodyLast::before,
& > .bodyDefault::before {
content: "→";
width: 30px;
letter-spacing: 2px;
white-space: nowrap;
display: inline-block;
position: absolute;
left: 0px;
}
// 横线
.bodyDefault::before {
}
.bodyFirst::before {
}
.bodyLast::before {
}
.bodyFirst::before {
}
.bodyOuter::before {
content: "";
border: solid 1px transparent;
}
// 竖线
& > .bodyFirst::after,
& > .bodyDefault::after,
& > .bodyOuter::after,
& > .bodyLast::after {
content: "";
width: 2px;
height: 50%;
border-left: solid 2px transparent;
white-space: nowrap;
display: inline-block;
position: absolute;
left: 0px;
}
& > .bodyDefault::after {
border-left: solid 2px red;
height: 100%;
}
& > .bodyFirst::after {
border-left: solid 2px yellowgreen;height: 50%;bottom: 0;
}
& > .bodyLast::after {
border-left: solid 2px blue;height: 50%;top: 0;
}
// 外层
.bodyOuter::after {
border-left: solid 2px transparent;
}
// 最外层无线条
.bodyOuter {
background: transparent;
border-left: 2px solid transparent;
&.box::before {
background: transparent;
}
}
.bodyOuter::before {
background: transparent;
}
}
.iconremove {
color: #409eff;
width: 22px;
font-size: 20px;
cursor: pointer;
}
</style>
src/views/mind/mind.vue
<template>
<div class="warp">
<div class="header">
<div>
<el-button type="primary" size="small" @click="expendAll">展开所有</el-button>
</div>
<div>
<el-input-number
v-model="num"
:precision="2"
:step="0.1"
:max="2"
:min="0"
style="width: 100px"
size="mini"
controls-position="right"
@change="numberChange"
>
</el-input-number>
倍
</div>
<div>
<el-button
:type="isRank ? 'primary' : ''"
icon="el-icon-rank"
circle
@click="rankfn"
>
</el-button>
</div>
</div>
<div class="mind" :class="{ mindRank: isRank }" v-drag ref="refresh">
<mindItem :list="list" :style="'transform: scale(' + num + ')'"></mindItem>
</div>
</div>
</template>
<script>
import mindItem from "./components/mindItem.vue";
import { expendfn } from "./components/index.js";
export default {
name: "",
props: {},
components: { mindItem },
data() {
return {
isRank: false,
list: [],
num: 1,
listCache: [
{
id: 11,
first: true,
tableData: [
{ id: 112, name: "李四 1级-1", age: 2 },
{ id: 113, name: "李四 1级-2", age: 4 },
],
children: [
{
parent: 11,
id: 21,
tableData: [
{ id: 122, name: "李四 2级-1", age: 30 },
{ id: 123, name: "李四 2级-2", age: 34 },
],
},
{
parent: 11,
id: 22,
tableData: [
{ id: 124, name: "李四 2级-3", age: 65 },
{ id: 125, name: "李四 2级-4", age: 23 },
],
},
{
parent: 11,
id: 23,
tableData: [
{ id: 126, name: "李四 2级-5", age: 45 },
{ id: 127, name: "李四 2级-6", age: 25 },
],
children: [
{
parent: 23,
id: 33,
tableData: [
{ id: 128, name: "李四 3级-1", age: 32 },
{ id: 129, name: "李四 3级-2", age: 623 },
],
},
{
parent: 23,
id: 34,
tableData: [
{ id: 130, name: "李四 3级-3", age: 623 },
{ id: 131, name: "李四 3级-4", age: 256 },
],
},
{
parent: 23,
id: 35,
tableData: [
{ id: 132, name: "李四 3级-5", age: 352 },
{ id: 133, name: "李四 3级-6", age: 2345 },
],
},
{
parent: 23,
id: 36,
tableData: [
{ id: 134, name: "李四 3级-7", age: 35 },
{ id: 135, name: "李四 3级-8", age: 4124 },
],
},
],
},
],
},
],
};
},
computed: {},
watch: {
num(newVal, oldVal) {
console.log(newVal, oldVal);
if (newVal < oldVal && oldVal <= 0.5) {
this.num = 0.5;
}
},
},
directives: {
drag: {
bind: function (el) {
let odiv = el;
let moveing = false;
let moves = false;
odiv.onmousedown = (e) => {
let arr = Array.from(odiv.classList);
if (!arr.includes("mindRank")) return;
let disX = e.clientX - odiv.offsetLeft;
let disY = e.clientY - odiv.offsetTop;
document.onmousemove = (e) => {
let left = e.clientX - disX;
let top = e.clientY - disY;
if (top <= 80 && left <= 300) {
// top = 80;
// left = 300;
}
odiv.style.left = left + "px";
odiv.style.top = top + "px";
moveing = true;
};
document.onmouseup = (e) => {
document.onmousemove = null;
document.onmouseup = null;
moveing = false;
};
};
},
},
},
created() {},
mounted() {
this.init();
},
methods: {
rankfn() {
this.isRank = !this.isRank;
},
numberChange() {
console.log(" this.num--", this.num);
},
init() {
let { listCache } = this;
this.list = JSON.parse(JSON.stringify(listCache));
},
expendAll() {
this.init();
},
},
};
</script>
<style scoped lang="less">
.warp {
padding: 10px;
}
.mind {
padding: 20px;
// height: calc(100vh - 150px);
// width: calc(100vw - 60px);
position: fixed;
user-select: none;
overflow: auto;
background-color: #fff;
}
.mindRank {
cursor: move;
}
.header {
display: inline-block;
align-items: center;
position: fixed;
z-index: 2;
background-color: #fff;
& > div {
display: inline-block;
margin-right: 20px;
}
}
</style>
src/views/mind/components/index.js
export function expendfn({
list = [],
id = '',
isExpend = false // 默认展开/关闭
}) {
if (list.length === 0) return [];
let arr = JSON.parse(JSON.stringify(list));
id === '' && !isExpend && defaultfn(arr, id, isExpend); // 刷新
// 刷新
function defaultfn(lists) {
lists.forEach((x) => {
x.isExpandBefore = false;
x.isExpandAfter = false
if (x.children) defaultfn(x.children);
});
}
return arr;
}
src/router/index.js
import Vue from 'vue'
Vue.use(VueRouter)
const routes = [{
path: '/mind',
naem: 'mind',
component: () => import('@/views/mind/mind.vue')
}, ]
const router = new VueRouter({
routes
})
export default router
src/main.js
import '@/directive/index.js'
import 'element-ui/lib/theme-chalk/index.css';
import App from './App.vue'
import ElementUI from 'element-ui';
import Vue from 'vue'
import jm from 'vue-jsmind'
import router from './router'
import store from './store'
Vue.config.productionTip = false
Vue.use(ElementUI);
Vue.use(jm)
if (window.jsMind) {
console.log('wind')
Vue.prototype.jsMind = window.jsMind
}
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')