Vue(一):基础知识

125 阅读14分钟

什么是 MVVM

MVVM(Model-View-ViewModel)一种软件设计模式,旨在将应用程序的数据模型(Model)与视图层(View)分离,并通过 ViewModel 来实现它们之间的通信。降低了代码的耦合度。

Model 代表数据模型,是应用程序中用于处理数据的部分。在 Vue.js 中,Model 通常指的是组件的 data 函数返回的对象,或者 Vue 实例的 data 属性。

View 是用户界面,是用户与应用程序进行交互的部分。在 Vue.js 中,View 通常指的是 Vue 组件的模板(template),它使用声明式的语法来描述如何渲染数据。Vue 的模板语法允许开发者以声明的方式将 DOM 绑定到底层 Vue 实例的数据上。

ViewModel 是 MVVM 模式的核心,它作为 View 和 Model 之间的桥梁,负责将 Model 的数据同步到 View 显示出来,以及将用户对 View 的操作反映到 Model 上。

Vue 实例通过其响应式系统(基于 ES6 的 Proxy 或 Object.defineProperty 实现)来监听 Model 的变化,并自动更新 DOM(View)

Vue 钩子函数

Vue 八大生命周期钩子函数:

函数调用时间
beforeCreatevue实例初始化之前调用
createdvue实例初始化之后调用
beforeMount挂载到DOM树之前调用
mounted挂载到DOM树之后调用
beforeUpdate数据更新之前调用
updated数据更新之后调用
beforeDestroyvue实例销毁之前调用
destroyedvue实例销毁之后调用

Vue.js 官方文档

Vue 实例

每个 Vue 应用都是通过用 Vue 构造函数创建一个新的 Vue 实例开始的

var vm = new Vue({
  // 选项
})

Vue 实例和 Vue 生命周期有什么关系?

在 Vue 中,当创建一个组件或 Vue 实例时,它会经历一系列初始化步骤,这就是所谓的生命周期。

Vue 生命周期描述了 Vue 实例从创建到销毁的整个过程。

Vue 实例提供了一系列有用的方法,用于与 Vue 实例进行交互、操作 DOM、监听事件等。以下是一些常用的 Vue 实例方法:

  1. DOM 操作方法:

    标题
    $mount(elementOrSelector, [options])手动挂载 Vue 实例到一个 DOM 元素上。如果未提供参数,则创建一个新的 DOM 元素并将 Vue 实例挂载到该元素上。
    $appendTo(elementOrSelector, [callback])将 Vue 实例的根 DOM 元素插入到目标元素中。
    $before(elementOrSelector, [callback])将 Vue 实例的根 DOM 元素插入到目标元素之前。
    $after(elementOrSelector, [callback])将 Vue 实例的根 DOM 元素插入到目标元素之后。
    $remove([callback])从 DOM 中移除 Vue 实例的根 DOM 元素。
  2. 事件监听与触发

    标题
    $emit(eventName, […args])触发当前实例上的自定义事件。
    $on(eventName, callback)监听当前实例上的自定义事件。
    $once(eventName, callback)监听一个自定义事件,但是只触发一次。
    $off([eventName, callback])移除自定义事件监听器。
  3. DOM 更新与延迟执行

    标题
    $nextTick(callback)在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用它,然后等待 DOM 更新。
  4. 数据监听与更新

    标题
    $watch(expOrFn, callback, [options])监听 Vue 实例上某个表达式的变化,当表达式的值改变时调用回调函数。
    $set(target, key, value)向响应式对象中添加一个属性,并确保这个新属性同样是响应式的,且触发视图更新。
    $delete(target, key)删除对象上的属性。如果对象是响应式的,确保删除能触发视图更新。
    $forceUpdate()强制 Vue 实例重新渲染。注意它仅仅影响实例本身和插入插槽内容的子组件,而不是所有子组件。
  5. 实例属性访问

    标题
    $data访问 Vue 实例的 data 对象。
    $options访问用于当前 Vue 实例的初始化选项。
    $refs一个对象,持有注册过 ref 特性的所有 DOM 元素和子组件实例。

上述方法并非全部 Vue 实例方法,但涵盖了常用的部分。

// 创建一个新的 Vue 实例,但没有指定 el 选项  
var vm = new Vue({  
  data: {  
    message: 'Hello Vue!'  
  },  
  // 生命周期钩子和其他选项...  
});  
  
// 手动挂载到 #app 元素上  
vm.$mount('#app');
new Vue({  
  el: '#app',  
  data: {  
    message: 'Hello Vue!'  
  },  
  // 生命周期函数
  mounted: function() {  
    console.log('Component mounted!');  
    // 此时可以执行依赖于 DOM 的操作  
  }  
  // 其他选项...  
});

012-cn.vuejs.org/api/instanc…

Vue.prototype

用途:

  • Vue.prototype 是 Vue 构造函数的原型对象,用于向所有 Vue 实例添加共享的方法和属性。

  • 通过在 Vue.prototype 上定义方法或属性,可以确保这些方法或属性在所有 Vue 实例中都是可用的。

用法:

  • 添加全局方法:Vue.prototype.$myMethod = function() { ... };

  • 添加全局属性:Vue.prototype.$myProperty = 'some value';

为什么要以 $ 符号开头?

这是 Vue 中的一个简单约定,为了避免和组件中定义的数据、方法、计算属性产生冲突。

Vue.prototype 加一个变量,只是给每个组件加了一个属性,这个属性的值并不具有全局性。

例如:

// main.js
Vue.prototype.$test = "测试"

//test1.vue
mounted() {
    this.$test = "哈哈"
    console.log(this.$test) // 哈哈
    this.$router.push("/test2")
}

//test2.vue
mounted() {
    console.log(this.$test) // 测试
}

如果要实现全局变量的功能,需要把属性变为引用类型

// main.js
Vue.prototype.$test = { name: "测试" }

//test1.vue
mounted() {
    this.$test.name= "哈哈"
    console.log(this.$test) // 哈哈
    this.$router.push("/test2")
}

//test2.vue
mounted() {
    console.log(this.$test) // 哈哈
}

created 和 mounted 区别

  • created:在模板渲染成 html 前调用,通常初始化某些属性值,然后再渲染成视图。

  • mounted:在模板渲染成 html 后调用,通常是初始化页面完成后,再对 html 的 dom 节点进行一些需要的操作。

    例如 chart.js 的使用 var ctx = document.getElementById(id),这个就一定要等 html 渲染完成后才可以完成,这就要用 mounted

computed 和 watch 区别

computed 计算属性

  • 只有当计算属性所依赖的响应式数据发生变化时,计算属性才会重新求值。如果多次访问计算属性,但依赖项没有变化,那么它将返回之前的计算结果,而不是重新计算。

  • 在模板中可以直接使用计算属性

  • 计算属性总是返回一个值

watch 监听器

  • watch 中的回调函数可以执行异步操作,而 computed 则不可以。

  • 没有缓存,每次触发时重新计算

  • 接收两个参数(newValue, oldValue)

总结

  • 当多个属性通过计算影响一个属性的时候,建议用 computed

  • 当一个值发生变化之后,会引起一系列的操作,这种情况就适合用 watch

computed 如何传参

<test :text="text('123')" />

computed: {
    text(){
        return function (params) {
            console.log(params) // 123
            return params
        }
    }
}

执行顺序

父子组件生命周期执行顺序

  • 页面初始化时:

    父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created -> 子 beforeMount -> 子 mounted-> 父 mounted

  • 页面销毁时:

    父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed-> 父 destroyed

页面跳转的生命周期执行顺序

  • 旧页面跳转到新页面:

    新页面 created > 新 beforeMount > 旧 beforeDestroy > 旧 destroyed > 新 mounted

computed 、watch、created 、mounted 的先后顺序

  • immediate 为 false 时: created => computed => mounted => watch

  • immediate 为 true 时: watch =>created=> computed => mounted

    [ɪˈmiːdiət]

$nextTick

原理

nextTick 是 Vue 中用于等待下一次 DOM 更新完成后的一个方法。

由于 Vue 的 DOM 更新是异步的,即数据变化后,Vue 会等待当前执行栈中的所有同步任务完成后,才进行 DOM 的更新。如果在数据变化后直接尝试获取或操作 DOM,可能会得到更新前的状态。nextTick 提供了一个机制,允许我们在 DOM 更新完成后再执行某些操作,从而确保我们操作的是更新后的 DOM。

nextTick 的实现原理主要是基于 JavaScript 的事件循环机制

  • 如果浏览器支持 Promise,Vue 会使用 Promise.resolve().then(callback) 的方式将回调函数放入微任务队列。

  • 如果浏览器支持 MutationObserver,则利用 MutationObserver 的回调函数放入微任务队列。

    • MutationObserver 是 Web API 的一部分,它提供了一种能够异步监视 DOM 变动的能力。这个 API 可以用来检测 DOM 树的变化,比如节点的增减、属性的变化等,并在这些变化发生时执行指定的回调函数。
  • 如果不支持上述 2 个,则会使用 setImmediate 或 setTimeout 来模拟异步操作,会放入宏任务队列。

使用场景

  • 比如一个下拉框,我在 methods 方法里手动赋值,然后我想获取赋值后的 label,这时候就要用 $nextTick

  • 两个 tab 切换,切换后需要让 tab 中的一个 input 获取焦点,这时候就要在 $nextTick 中使用 focus()

@click.native

原生点击事件。

通常情况下,我们在组件上使用 @click 监听点击事件时,实际上是监听了组件内部封装的一个模拟点击事件,并不是真正的原生点击事件。

而使用 @click.native 可以直接监听原生的点击事件,不需要经过组件的封装和处理。这对于一些特殊的场景可能会更加方便和灵活。

<el-card @click="handleClick1"></el-card>
<el-card @click.native="handleClick2"></el-card>

<script>
methods: {
    handleClick1() {
        // 不会触发
    },
    handleClick2() {
        // 可以触发
    }
}
</script>

@click.stop

阻止事件冒泡,即阻止事件向父级元素传递。

<div id="app">
    <div v-on:click="parentClick">
        <button v-on:click="childClick">阻止单击事件继续传递</button>
    </div>
</div>

<script>
var app = new Vue({
    el: "#app",
    data: {
        name: "Vue.js"
    },
    methods: {
        parentClick: function () {
            alert("parentClick");
        },
        childClick: function () {
            alert("childClick");
        },
    }
});
</script>
// 将会先弹出 "childClick", 再弹出 "parentClick"。
<div id="app">
    <div v-on:click="parentClick">
        <button v-on:click.stop="childClick">阻止单击事件继续传播</button>
    </div>
</div>

<script>
var app = new Vue({
    el: "#app",
    data: {
        name: "Vue.js"
    },
    methods: {
        parentClick: function () {
            alert("parentClick");
        },
        childClick: function () {
            alert("childClick");
        },
    }
});
</script>
// 只弹出 "childClick"

使用场景:点击子元素区域的的时候,不触发父级元素的点击事件。

@click.prevent

阻止事件的默认行为。

// 阻止 a 标签跳转,仅执行函数 test4
<a href="http://www.baidu.com" @click.prevent="test4">百度一下</a>   

// 阻止表单提交,仅执行函数 test5
<form  action="/xxx" @submit.prevent="test5">   
    <input type="submit" value="注册">
</form>

@keyup.enter

按键修饰符。

// 按下 enter时,执行方法 test7
<input type="text" @keyup.enter="test7">

directive 自定义指令

什么是自定义指令?

vue2.js 官方链接

vue3.js 官方链接

Vue 的自定义指令允许你对 DOM 进行低级别的操作。自定义指令通过 Vue 的全局方法 Vue.directive() 或组件的 directives 选项来注册。注册之后,你可以在 Vue 模板中通过 v- 前缀加上指令名来使用这个指令。

低级别操作是指直接通过 JavaScript 对 DOM 树中节点的进行增删改查,以及对节点的属性、样式、事件等进行操作。

全局自定义指令

Vue2:

// 在main.js中
// 注册一个全局自定义指令 `v-focus`
Vue.directive('focus', {
  // 当被绑定的元素插入到 DOM 中时……
  inserted: function (el) {
    // 聚焦元素
    el.focus()
  }
})

Vue3:

const app = createApp({})

// 使 v-focus 在所有组件中都可用
app.directive('focus', {
  /* ... */
})

局部组件指令

Vue2:

created() {},
directives: {
  focus: {
    // 指令的定义
    inserted: function (el) {
      el.focus()
    }
  }
}

Vue3:

在 <script setup> 中,任何以 v 开头的驼峰式命名的变量都可以被用作一个自定义指令。

<script setup>
// 在模板中启用 v-focus
const vFocus = {
  mounted: (el) => el.focus()
}
</script>

<template>
  <input v-focus />
</template>
export default {
  setup() {
    /*...*/
  },
  directives: {
    // 在模板中启用 v-focus
    focus: {
      /* ... */
    }
  }
}

然后你可以在模板中任何元素上使用新的 v-focus property,如下:

<input v-focus>

钩子函数

Vue2:

函数描述
bind只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
inserted被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
update所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。
componentUpdated指令所在组件的 VNode 及其子 VNode 全部更新后调用。
unbind只调用一次,指令与元素解绑时调用。

Vue3:

函数描述
created在绑定元素的 attribute 前或事件监听器应用前调用
beforeMount在元素被插入到 DOM 前调用
mounted在绑定元素的父组件及他自己的所有子节点都挂载完成后调用
beforeUpdate绑定元素的父组件更新前调用
updated在绑定元素的父组件及他自己的所有子节点都更新后调用
beforeUnmount绑定元素的父组件卸载前调用
unmounted绑定元素的父组件卸载后调用

钩子函数参数:

  • el:指令绑定到的元素。这可以用于直接操作 DOM。

  • binding:一个对象,包含指令值(value)、旧的指令值(oldValue)、参数(arg)等等

  • vnode

  • oldVnode

原理

当 Vue 组件的模板被编译时,Vue 会识别出模板中所有的 v- 前缀指令,这会包含 Vue 的内置指令和自定义指令。对于自定义指令,Vue 会根据注册的钩子函数生成相应的指令对象,这些对象包含了指令的名称、参数、表达式等信息。然后会将这些信息存储在之后生成的 VNode 对象中。最后在生成真实 DOM 的时候,Vue 会在相应的 DOM 元素上调用自定义指令的钩子函数。

v-model 的原理

v-model 是 Vue.js 中的一个语法糖,它内部实际上是使用了 v-bind 和 v-on 两个指令。

  • v-bind:实现数据到 DOM 的绑定

    • 修改 data 中的属性值会体现在页面上

    • v-bind 的原理主要是基于 Vue 的响应式系统,当数据变化时触发相应的 setter,来触发渲染过程,以更新 DOM

  • v-on:实现 DOM 到数据的绑定

    • 在页面上修改的属性值会作用到 data 中

    • v-on 的原理是监听 DOM 元素上指定的事件,当事件发生时,执行相应的 JavaScript 代码或方法,从而更新属性值。

Vue 中使用 CSS 变量

<template>
    <!-- 方式一:
     在根或父或当前节点使用 --varName 来定义变量 
     然后当前节点使用 var(--varName) 来使用变量 -->
    <div class="parent">

        <!-- 方式二:可以在 data 中定义变量,在 template 中直接使用 -->
        <span :style="{ color: myColor1 }">parent</span>

        <!-- 方式三:data 中定义变量,css 中使用 v-bind 绑定变量 -->
        <div class="child1">child1</div>

        <!-- 方式四:通过 style module -->
        <div :class="classModule.child2">child2</div>

        <!-- 方式五:不好理解,不知道这样写的意义在哪里 -->
        <div class="child3" :style="{ '--my-color': myColor3 }">child3</div>
    </div>
</template>

<script lang="ts" setup>
const myColor1 = 'red'; // 方式二
const myColor2 = 'yellow'; // 方式三
const myColor3 = 'blue'; // 方式五
</script>

<style>
.parent {
    /* 方式一 */
    --parent-bg-color: skyblue;

    /* 方式一 */
    background-color: var(--parent-bg-color);
}

.child1 {
    /* 方式三 */
    color: v-bind(myColor2);
}

.child3 {
    /* 方式五 */
    color: var(--my-color);
}
</style>

<style module="classModule">
/* 方式四 */
.child2 {
    color: green;
}
</style>

image.png

Vue 项目结构

文件名注释
build项目构建(webpack)相关代码
config项目配置
dev.env.js开发环境变量
prod.env.js生产环境变量
test.env.js测试环境变量
index.js项目配置文件
package.jsonnpm 包配置文件,npm 脚本
src日常开发主要在这个文件夹
asset放置静态资源,会被 webpack 构建
main.js项目入口文件
components公共组件
App.vue根组件
pages页面文件
api接口文件
router路由文件
static纯静态资源,不会被 webpack 构建
index.html首页入口文件
README.md项目的说明文档
dist打包之后的文件

Vue 组件规范

1、按功能分

  • 组件分为容器组件,和 UI 组件(或称为展示组件);

    • 将每个 .vue 文件拆成一个文件夹,其中 index.vue 作为容器组件;

    • 其余如 Table.vue 和 Search.vue 等作为 UI 组件。

2、按可复用性分

  • 组件分为全局基础组件和页面级组件;

    • 将全局基础组件提取至 /src/components 内;

    • 页面组件放至 /src/pages 内。

Vue 功能代码

动态加载组件

<template>
  <div>
    <div class="block-menus" v-show="$store.state.modulesFlag">
      <div
        v-for="(item, index) in modulesMenu"
        :key="index"
        class="block-menu"
        @click="handleClick(item)"
      >
        <span class="el-icon-menu"></span><span>{{ item.meta.title }}</span>
      </div>
    </div>

    <component :is="dynamicComponent"  v-show="!$store.state.modulesFlag"></component>

  </div>
</template>

<script>
export default {
  data() {
    return {
      dynamicComponent: null
    };
  },
  created() {
    this.modulesMenu = this.$store.state.modulesMenu;
  },
  mounted() {
  },
  methods: {
    handleClick(val) {
      this.dynamicComponent = () => Promise.resolve(require(`@/views/${val.component}.vue`).default)
      this.$store.commit('changeModulesFlag', false)
    },
  },
  watch: {
    '$store.state.modulesMenu': {
      handler(val) {
        this.modulesMenu = val
      }
    }
  }
};
</script>

Vue Tagsview

<template>
  <div class="el-tagsview">
    <!-- contextmenu.prevent: vue的鼠标右键事件-->
    <div
      @contextmenu.prevent="openMenu(item, $event)"
      :class="isActive(item.url) ? 'active' : ''"
      class="tagsview"
      v-for="(item, index) in tags"
      :key="index"
      @click="handleClick(item)"
    >
      {{ item.meta.title }}
      <!-- 这个地方一定要click加个stop阻止,不然会因为事件冒泡一直触发父元素的点击事件,无法跳转另一个路由 -->
      <span
        class="el-icon-close tagsicon"
        @click.stop="handleClose(item, index)"
      ></span>
      <ul
        v-if="visible && url === item.url"
        class="contextmenu"
        :style="{ left: left + 'px', top: top + 'px' }"
      >
        <li @click.stop="handleRefresh(item, index)">刷新</li>
        <li @click.stop="handleClose(item, index)">关闭</li>
        <li @click.stop="handleCloseOther(item, index)">关闭其他</li>
        <li @click.stop="handleCloseAll($route.path)">关闭所有</li>
      </ul>
    </div>
  </div>
</template>

<script>
//这个就是导入vuex的数据,配合下面...map用
import { mapState, mapMutations } from "vuex";
export default {
  data() {
    return {
      //右键菜单隐藏对应布尔值
      visible: false,
      //右键菜单对应位置
      top: 0,
      left: 0,
      url: ""
    };
  },
  computed: {
    //引入vuex中state中的tags数据,一样this调用就行
    ...mapState(["tags"]),
  },
  mounted() {
    
  },
  watch: {
    //监听右键菜单的值是否为true,如果是就创建全局监听点击事件,触发closeMenu事件隐藏菜单,如果是false就删除监听
    visible(value) {
      if (value) {
        document.body.addEventListener("click", this.closeMenu);
      } else {
        document.body.removeEventListener("click", this.closeMenu);
      }
    },
  },
  methods: {
    //引入vuex中mutation方法,可以直接this.xxx调用他
    ...mapMutations(["removeTag", "removeTags", "addTags"]),
    //点击叉叉删除的事件
    handleClose(item, index) {
      //先把长度保存下来后面用来比较做判断条件
      let length = this.tags.length - 1;
      // 如果length=0,也就是说只剩最后一个标签,且是首页,那么什么都不做
      if (length === 0 && this.$route.path === "/dashboard") {
        return;
      }
      //vuex调方法,上面...map引入的vuex方法,不会这种方法的看vue官网文档
      this.removeTag(item);
      // 如果关闭的标签不是当前路由的话,就不跳转
      if (item.url !== this.$route.path) {
        return;
      }
      // 判断:如果index和length是一样的,那就代表都是一样的长度,就是最后一位,那就往左跳转一个
      if (index === length) {
        //再判断:如果length=0,也就是说你删完了所有标签
        if (length === 0) {
          //那么再判断:如果当前路由不等于首页(不需要)
          if (this.$route.path !== "/dashboard") {
            //那么就跳转首页。这一步的意思是:如果删除的最后一个标签不是首页就统一跳转首页,如果你删除的最后一个标签是首页标签,已经在这个首页路由上了,你还跳个什么呢。这不重复操作了吗。
            this.$router.push({ path: "/" });
          }
        } else {
          //那么,如果上面的条件都不成立,没有length=0.也就是说你还有好几个标签,并且你删除的是最后一位标签,那么就往左边挪一位跳转路由
          this.$router.push({ path: this.tags[index - 1].url });
        }
      } else {
        // 如果你点击不是最后一位标签,点的前面的,那就往右边跳转
        this.$router.push({ path: this.tags[index].url });
      }
      this.closeMenu();
    },
    // 刷新
    handleRefresh(item, index) {
      this.$router.replace({ path: item.url });
      // this.$router.push({path: item.url})
      this.closeMenu();
    },
    // 关闭其他
    handleCloseOther(item, index) {
      const routePaht = this.$route.path
      this.removeTags();
      if(routePaht !== item.url) {
        this.$router.push({ path: item.url }).catch(err => err);
      }
      this.addTags(item)
      this.closeMenu();
    },
    //点击跳转路由
    handleClick(item) {
      //判断:当前路由不等于当前选中项的url,也就代表你点击的不是现在选中的标签,是另一个标签就跳转过去,如果你点击的是现在已经选中的标签就不用跳转了,因为你已经在这个路由了还跳什么呢。
      if (this.$route.path !== item.url) {
        //用path的跳转方法把当前项的url当作地址跳转。
        this.$router.push({ path: item.url });
      }
    },
    //通过判断路由一致返回布尔值添加class,添加高亮效果
    isActive(url) {
      return url === this.$route.path;
    },
    //右键事件,显示右键菜单,并固定好位置。
    openMenu(tag, e) {
      this.url = tag.url
      this.visible = true;
      this.selectedTag = tag;
      const offsetLeft = this.$el.getBoundingClientRect().left;
      this.left = e.clientX - offsetLeft + 210; //右键菜单距离左边的距离
      this.top = e.clientY + 10; //右键菜单距离上面的距离           这两个可以更改,看看自己的右键菜单在什么位置,自己调
    },
    //隐藏右键菜单
    closeMenu() {
      this.url = ""
      this.visible = false;
    },
    //右键菜单关闭所有选项,触发vuex中的方法,把当前路由当参数传过去用于判断
    handleCloseAll(val) {
      this.removeTags();
      //跳转到首页,val接受传过来的当前路由
      if (val !== "/dashboard") {
        this.$router.push({ path: "/" });
      } else {
        this.addTags(this.$store.state.routers[0].children[0])
      }
      this.closeMenu();
    },
  },
};
</script>

<style scoped>
/*标签导航样式*/
.tagsview {
  cursor: pointer;
  /* margin: 10px 4px 10px 0px; */
  margin: 2px 6px 2px 0px;
  line-height: 26px;
  padding: 0 8px;
  border: 1px solid #d8dce5;
  /* border-radius: 5px; */
  color: #000;
  font-size: 12px;
  display: inline-block;
}
/*叉号鼠标经过样式*/
.tagsicon:hover {
  color: #f56c6c;
}
/*标签高亮*/
.active {
  /* background-color: #40ba84; */
  color: #fff;
  background-color: #6bb4f8;
  border-color: #6bb4f8;
}
/*右键菜单样式*/
.contextmenu {
  margin: 0;
  background: #fff;
  z-index: 100;
  position: absolute;
  list-style-type: none;
  padding: 5px 0;
  border-radius: 4px;
  font-size: 12px;
  font-weight: 400;
  color: #333;
  box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, 0.3);
}
.contextmenu li {
  margin: 0;
  padding: 5px 16px;
  cursor: pointer;
}
.contextmenu li :hover {
  background: #eee;
}
</style>

addTags(state, val) {
    //如果等于-1说明tabs不存在,那么插入,否则什么都不做
    //findindex找角标,循环判断一下,如果等于那么就代表有相同的,就不必添加,如果找不到那就是-1.就添加
    let result = state.tags.findIndex(item => item.url === val.url)
    result === -1 ? state.tags.push(val) : ''
    if(state.tags.length > 10) {
        state.tags = state.tags.slice(state.tags.length - 10, state.tags.length)
    }
},
//关闭标签
removeTag(state, val) {
    //同上,找角标,然后用角标的位置对应删除一位。splice:这是数组的删除方法
    let result = state.tags.findIndex(item => item.name === val.name)
    state.tags.splice(result, 1)
},
//关闭所有tagsview标签
removeTags(state) {
    //清空数组
    state.tags = []
},

el-tree-select

npm 地址

在线 API

<template>
  <div>
    <el-form :model="form" :rules="rules" ref="form" style="width: 500px;">
      <el-form-item prop="org" ref="treeSelect">
        <!-- popoverClass="myClass" 自定义弹出层样式,注意style中不能加 scoped -->
        <el-tree-select
          v-model="form.org"
          :treeParams="treeParams"
          popoverClass="myClass"
        />
      </el-form-item>
      <el-form-item prop="name">
        <el-select v-model="form.name" clearable>
          <el-option label="哈哈" value="haha" />
          <el-option label="呵呵" value="hehe" />
        </el-select>
      </el-form-item>
    </el-form>

    <el-button @click="reset">清除</el-button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      treeParams: {
        data: [
          {
            label: "1",
            value: "1",
            children: [
              { label: "11", value: "11" },
              {
                label: "12",
                value: "12",
                children: [
                  { label: "111", value: "111" },
                  { label: "112", value: "112" },
                ],
              },
            ],
          },
          { label: "2", value: "2" },
          { label: "3", value: "3" },
        ],
        props: {
          children: "children",
          label: "label",
          value: "value",
        },
        defaultExpandedKeys: [], // 默认展开节点
        clickParent: true // 可以点击父节点,默认 false
      },
      form: {
        org: "",
        name: "",
      },
      rules: {
        org: [{ required: true, message: "请选择", trigger: "change" }],
        name: [{ required: true, message: "请选择", trigger: "change" }],
      },
    };
  },
  mounted() {
    setTimeout(() => {
      this.form.org = '12'
      this.treeParams.defaultExpandedKeys = [this.form.org]
    }, 100);
  },
  methods: {
    reset() {
    // 重置清空的时候必须先赋空值,再使用 resetFields() 函数,否则会报红
      this.form.org = "";
      this.$nextTick(() => {
        this.$refs.form.resetFields();
      });
    },
  },
};
</script>

<style lang="less">
.myClass {
  padding: unset; // 去掉原始边距
  .el-tree-node__content {
    padding: 5px 0; // 设置 node 间距
  }
  .is-current { // 设置选中字体颜色
    color: #409eff;
    .el-tree-node__children {
      color: #606266; // 设置选中节点的子节点字体颜色
    };
  }
  overflow: hidden; // 隐藏原始滚动条
}
</style>

Vue 做权限管理的几种方案

点击这里