9、Vue之#app|v-简写|key|$refs实例和组件异同'@'滚动加载set、内置组件、自定义组件图标|季度半年|26小时|添加水印、组件传值传参可演示、组件七种写法、插槽slot后备、vue2源码、vue2和vue3区别生命周期setup、vuex|pinia、vue-router(含权限)、vue-cli|vue.config.js|跨域proxy代理、mock、表单验证(4850行)

一、vue之#app、指令v-简写、key、$refs、实例和组件异同、computed与methods的区别、'@'的定义、滚动加载、set应用实例
注意:在组件中,可以用自定义属性代替自定义事件
1、App.vue组件中的’#app’替换掉了index.html中的’#app’,成为最终的’#app’
(1)简单实用(可演示)
  <!DOCTYPE html>
  <html>
  <head>
    <title></title>
    <meta charset="utf-8">
    <script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
  </head>
  <body>
    <div id="box">{{msg}}||{{reMsg}}</div>
    <script type="text/javascript">
      var vm = new Vue({
        el:'#box',
        data:{
          msg:'12345'
        },
        computed:{
          reMsg:function(instance){
            console.log(instance===this);//true
            return this.msg.split('').reverse().join('')
          }
        }
      });
    </script>
  </body>
  </html>
(2)真实项目
  A、index.html
    <body>
      <div id="app"></div>
      <!-- built files will be auto injected -->
    </body>
  B、APP.vue
  <template>
    <div id="app">
      <router-view/>
    </div>
  </template>
  <script>
    export default {
      name: 'App'
    }
  </script>
  C、main.js
    new Vue({
      el: '#app',
      data: {},
      router,
      store,
      render: h => h(App)
    })
2、指令v-
  //搜索“与插槽相关的注释”
  //v-for的优先级高于v-if
(1)v-bind,标签的属性绑定,v-bind:attr=""简写为:attr=""
  (1-1)绑定单属性,如id、class、style,覆盖原来的属性
    A、全写<div v-bind:id="id">...</div>,简写<div :id="id">...</div>
    B、全写<div v-bind:class="class">...</div>,简写<div :class="class">...</div>,相当于angular1中的ng-class
    C、全写<div v-bind:style="style">...</div>,简写<div :style="style">...</div>,相当于angular1中的ng-style
    D、全写<div v-bind:href="url">...</div>,简写<div :href="url">...</div>,相当于angular1中的ng-href
    E、全写<div v-bind:num="1">...</div>,简写<div :num="1">...</div>
  (1-2)绑定多属性(scope对象),它们和“单属性一起并入”标签的属性对象,可能与单属性覆盖
    A、2.x,单属性覆盖多属性
      代码<div id="red" v-bind="{ id: 'blue' }"></div>
      效果<div id="red"></div>
    B、3.x,后面属性覆盖前面属性
      代码1<div id="red" v-bind="{ id: 'blue' }"></div>
      效果1<div id="blue"></div>
      代码2<div v-bind="{ id: 'blue' }" id="red"></div>
      效果2<div id="red"></div>
(2)v-on,标签的事件绑定,v-on:click=""简写为@click="" 
  A、相当于angular1中的ng-click、ng-change
  B、全写,<div v-on:click="doSomething">...</div>;简写,<div @click="doSomething">...</div> 
(3)v-slot,标签的子标签绑定,插槽
  A、只绑定插槽名,不绑定作用域,<template v-slot:head></template>,简写<template #header></template> 
  B、只绑定作用域,不绑定插槽名,<template v-slot="scope"></template>,没有简写
  C、既绑定插槽名,又绑定作用域,<template v-slot:head="scope"></template>,简写<template #header="scope"></template>
  D、组件中,插槽及后备内容,vue版本不同,写法相同
    <div>
      <slot name="head" :user="obj">
        {{ user.text }}<!-- 后备内容 -->
      </slot>
    </div>
(4)v-text,标签的内容绑定,文字
  A、相当于angular1中的ng-bind
  B、两者都可以用{{}}代替,只是用{{}}时,屏幕有可能闪烁
(5)v-model,在input textarea select中使用
  A、v-model.lazy 只有在input输入框发生blur时才触发change事件,如<input v-model.lazy="msg">
  B、v-model.trim 将用户输入的前后的空格去掉,如<input v-model.trim="msg">
  C、v-model.number 将用户输入的字符串转换成number,如<input v-model.number="msg">
(6)v-if、v-else-if、v-else
  <div v-if="type==='A'">A</div>
  <div v-else-if="type=='B'">B</div>
  <div v-else-if="type=='C'">C</div>
  <div v-else-if="type=='D'">D</div>
  <div v-else>not A/B/C/D</div>
(7)v-show与key搭配会报错,但不影响运行
  不出错:v-if="item.mark"  :key="item.mark"
  会出错:v-show="item.mark"  :key="item.mark"
(8)自定义指令示例,v-title
  A、index.html
    <head>
      <title><%= htmlWebpackPlugin.options.title %></title>
    </head>
  B、router.js
    const routes = [
      {
        path: '/',
        name: 'Index',
        component: () => import( /* webpackChunkName: "about" */ '../views/Index.vue'),
        meta: {title: '游戏活动'}
      }
    ]
    const router = new VueRouter({
      routes
    })
  C、main.js
    Vue.directive('title', {//单个修改标题
      update: function (el) {
        document.title = el.getAttribute("data-title") //给title赋值
      }
    })
  D、form.vue
    <template>
      <div class="page">
        <div v-title :data-title="basicData.gameName" ></div>  //宽度100%,高度为0,不显示
      </div>
    </template>
(9)v-bind用法示例(可演示)
  <!DOCTYPE html>
  <html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>样式</title>
    <style>
      .title{font-size: 30px;font-weight: bolder;}
      .red{color:red}
      .green{color:green}
      .fontSize{font-weight:bolder}
    </style>
    <script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
  </head>
  <body>
    <div id="box">
      <div class="title">1、class</div>
      <div>
        <div :class="isTrue?'red':'green'">(1)三元</div>
        <div :class="['red','green'][num1]">(2-1)数组-多中选一</div>
        <div :class="['one'=='one'?'green':null,'two'=='two'?'fontSize':null,]">(2-2)数组-多中全选</div>
        <div :class="{'red':isTrue,'green':!isTrue}">(3)对象</div>
      </div>
      <div class="title">2、style</div>
      <div>
        <div v-bind:style="isTrue?{'color':'green','width':'200px'}:{'color':'red','width':'300px'}">(1)三元</div>
        <div :style="{'color': ['red','green'][num1], }">(2)数组</div>
        <div :style="{'color': isTrue? 'red':'green','width': isTrue? '200px':'300px', }">(3)对象</div>   
      </div>
      <div class="title">3、v-bind:可以简写为:</div>
      <div>
        <div><span>(1)bind正常写法</span><span><input type="text" v-bind:value="num1"></span></div>
        <div><span>(2)bind简单写法</span><span><input type="text" :value="num1"></span></div>
      </div>
      <div class="title">4、插值表达式</div>
      <div>
        <div><span>(1)三元取值:</span><span>{{isTrue?'三元':'0'}}</span></div>
        <div><span>(2)数组取值:</span><span>{{['9','数组'][num1]}}</span></div>
      </div>
    </div>
  </body>
  </html>
  <script type="text/javascript">
    var vm = new Vue({
      el:'#box',
      data:{
        isTrue: true,
        num1: 1,
        width: '200px',
        height: '20px',
      }
    });
  </script>     
3、vue组件的key属性('key'、"key"、‘key’、“key”)?
  (1)key属性的作用,标识节点,让Diff算法更高效地识别节点、更新虚拟DOM
  (2)index不能做key,用index做key时,新增或删除节点的操作,会使一个节点使用另一节点的index,进而使用它的key,进而使用它的data,进而产生错误
  4、$refs,获取DOM元素或组件的引用
  (1)$refs,加在普通的元素上,用this.$refs.name获取到的是dom元素
    <template>
      <div>
        <div ref="btn">我是一个按钮</div>
      </div>
    </template>
    <script>
      export default {
        name: 'App',
        mounted() {
          console.log(this.$refs.btn);//获取到正确元素
        }
      }
    </script>
  (2)$refs,加在组件上,用this.$refs.name 获取到的是组件实例,可以使用组件的所有方法。
4、实例和组件的异同
  (1)相同点:两者接收相同的配置,例如 data、computed、watch、methods以及生命周期钩子等。
  (2)不同点:
    A、自定义组件没有el配置项,
    B、Vue2.x和Vue3.x版本要求自定义组件的
      a、data,默认值必须是一个工厂函数,用来返回默认对象
      b、props的某项如果是对象,默认值必须是一个工厂函数,用来返回默认对象
    C、原因是
      a、如果数据是对象,那么组件实例在不同的地方调用,数据指向的是相同的地址,此处数据改变,它处数据也改变;
      b、如果数据是函数,那么组件实例在不同的地方调用,数据指向数据此次的执行结果,是不同的地址,此处数据改变,它处数据不改变。
    Vue.component('my-component', {
      data: function() {
        return {
          count: 0
        };
      },
      props: {
        propA: Number,// 基本的类型检查
        propB: [String, Number],// 多个可能的类型
        propC: {
          type: String,
          required: true// 必填的字符串
        },
        propD: {
          type: Object,
          default() {// 带有默认值的对象
            return { message: 'hello' };
          }
        },
        propE: {
          type: Array,
          default() {// 带有默认值的数组
            return [];
          }
        },
        propF: {
          validator: function (value) {// 自定义验证函数
            return value > 10;
          }
        }
      }
    });
5、computed与methods的区别(可演示)
  案例来源:https://www.jb51.net/article/137040.htm
  <!DOCTYPE html>
  <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>computed的使用</title>
      <script src="https://cdn.bootcss.com/vue/2.5.16/vue.js"></script>
    </head>
    <body>
      <div>computed与methods的区别</div>
      <div>1、正常情况下,二者没区别;</div>
      <div>2、computed是属性调用,methods是方法调用;</div>
      <div>3、computed在标签(非属性)上调用时不用加(),methods在标签(非属性)上调用时用加();</div>
      <div>4、computed在关联值发生改变时才调用,methods在不管关联值是否发生改变都调用;</div>
      <div id="root">
      </div>
    </body>
  </html>
  <script>
    var vm = new Vue({
      el: "#root",
      data: {
        name: "老夫",
        age: 40,
        hobby: '测试computed和methods的区别',
        nameAgeStyle: {
          fontSize: "20px",
          color: "#0c8ac5"
        },
        inputStyle: {
          display: "block",
          width: "350px"
        }
      },
      template: 
        `<div>
          <div v-bind:style="nameAgeStyle">computed方式渲染: {{computeNameAndAge}}</div>
          <div v-bind:style="nameAgeStyle">methods方式渲染: {{methodNameAndAge()}}</div>
          <br>
          <input type="text" v-model="hobby"  v-bind:style="inputStyle">
          <div>爱好: {{hobby}}</div>
          <div>{{testComputeAndMethod()}}</div>
        </div>`,
      computed: {
        computeNameAndAge() {
          console.log('computed在运行');
          return `${this.name} == ${this.age}岁`;
        }
      },
      methods: {
        methodNameAndAge() {
          console.log('methods在运行');
          return `${this.name} == ${this.age}岁`;
        },
        testComputeAndMethod() {
          console.log("testComputeAndMethod在运行");
        }
      }
    })
  </script>
6、'@'
  (1)'@'定义
    function resolve(dir) {
      return path.join(__dirname, '..', dir)
    }
    module.exports = {
      context: '/',
      entry: {},
      output: {},
      resolve: {
        alias: {
          '@': resolve('src')
        }
      },
    }
  (2)'~@'示例,作用类似于'@'
    <style lang="scss" scoped>
      @import "~@/styles/mixin.scss";
      .bg {
        background: url(~@/assets/logo.png) no-repeat;
      }
    </style> 
    <img src="~@/assets/logo.png" alt="" /> 
7、滚动加载(vue-infinite-scroll)
  <div v-infinite-scroll="loadMore" //无限滚动的回调函数是loadMore。其内部判断:如果当前页大于总页数则return,否则再次执行loadMore
    infinite-scroll-throttle-delay="500" //下次检查和这次检查之间的间隔
    infinite-scroll-disabled="isBusy" //isBusy为false,执行无限滚动的回调函数loadMore,即不繁忙的时候执行。
    infinite-scroll-distance="10"> //这里10决定了页面滚动到离页尾多少像素的时候触发回调函数,10是像素值。通常我们会在页尾做一个几十像素高的“正在加载中...”,这样的话,可以把这个div的高度设为infinite-scroll-distance的值即可。
    <div v-for="item in data" :key="item.index">{{item.name}}</div>
  </div>
  export default {
    data(){
      return{
        data:[],
        isBusy:false,
        itemsNumber:5,
        pageIndex:1
      };
    },  
    methods:{
      loadMore:function () {
        var self = this;
        if(this.pageIndex>response.data.total_pages){
          this.isBusy = true;
        }else{
          this.ajax.get('https:Xxxx',{
            params:{
              page:self.pageIndex,
              page_size:self.itemsNumber
            }
          }).then(function (response) {
            this.data.push({name:response.data.list});
            this.pageIndex++;
          }).catch(function (error) {
            this.error = error;
          })
        }
      }
    }
  }
8、Vue.set应用(可演示)
  附、不能检测到数组项变化的情形
  (1)用索引设置一个数组项,Vue.set(vm.items, indexOfItem, newValue)
  (2)修改数组的长度,vm.items.splice(indexOfItem, 1, newValue)
    A、Vue框架只对数组方法中的'push','pop','shift','unshift','splice','sort','reverse'实现了响应式。
    B、通过索引改变数组,没有执行发布函数,没法执行订阅函数,需要通过Vue.set来执行发布函数,实现响应式。
  <!DOCTYPE html>
  <html>
    <head lang="en">
      <meta charset="UTF-8">
      <title></title>
      <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    </head>
    <body>
      <div id="app2">
        <p v-for="item in items" :key="item.id">
          {{item.message}}
        </p>
        <button class="btn" @click="btn2Click()">动态赋值</button><br />
        <button class="btn" @click="btn3Click()">为data新增属性</button>
      </div>
    </body>
  </html>
  <script>
    var vm2 = new Vue({
      el: "#app2",
      data: {
        items: [
          { message: "Test one", id: "1" },
          { message: "Test two", id: "2" },
          { message: "Test three", id: "3" }
        ]
      },
      methods: {
        btn2Click: function () {
          Vue.set(this.items, 0, { message: "Change Test", id: '10' })
        },
        btn3Click: function () {
          var itemLen = this.items.length;
          Vue.set(this.items, itemLen, { message: "Test add attr", id: itemLen });
        }
      }
    });
  </script>

二、内置组件
1、5个内置组件
  (1)component
    渲染一个“元组件”为动态组件。依 is 的值,来决定哪个组件被渲染。
  (2)transition
    A、作为单个元素/组件的过渡效果
    B、只会把过渡效果应用到其包裹的内容上,而不会额外渲染DOM元素,也不会出现在可被检查的组件层级中
    C、name属性:用于自动生成CSS动画类名
      如果transition标签元素没有设置name属性,则对应的动画类名为v-XXX;如果设置了name属性,则对应的动画类名为属性值-XXX
    D、appear属性:一开始就生效显示动画
    E、示例,来源,https://blog.csdn.net/Superman_H/article/details/122851610
      <template>
        <div>
          <button @click="bol = !bol">隐藏/显示</button>
          <!-- transition 标签元素设置了 name、appear 属性 -->
          <transition name="moveCartoon" appear>
            <!-- 动画会在一开始便生效 -->
            <h1 v-show="bol">组件动画效果</h1>
          </transition>
        </div>
      </template>
      <script>
        export default {
          name: 'App',
          data() {
            return { bol: true };
          },
        };
      </script>
      <style>
        /* 类名要对应回 name 的属性值 */
        .moveCartoon-enter-active {
          animation: move 1s;
        }
        .moveCartoon-leave-active {
          animation: move 1s reverse;
        }
        @keyframes move {
          from {
            transform: translateX(-100%);
          }
          to {
            transform: translate(0);
          }
        }
      </style>
  (3)transition-group
    A、作为多个元素/组件的过渡效果。
    B、可以使被包含的组件保留状态,或避免重新渲染。
    C、它渲染一个真实的DOM元素。默认渲染<span>,可以通过tag、attribute配置哪个元素应该被渲染。它每个子节点必须有独立的key,动画才能正常工作。
    D、标签里面的元素需要设置 key 属性,作为当前元素的唯一标识,
    E、其他用法都和 transition 标签一样
  (4)keep-alive
    A、概念,包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。
    B、它是一个抽象组件:它自身不会渲染一个DOM元素,也不会出现在组件的父组件链中。
    C、原理,在created函数调用时将需要缓存的VNode节点保存在this.cache中,
  (5)slot
    作为组件模板之中的内容分发插槽。它自身将被替换。
2、动态组件效果的实现方案
  (1)在内置组件<component>里面使用 v-bind: is。没有keep-alive的配合,只能实现切换,不能实现缓存
    <div id="app">
      <component v-bind:is="whichcomp"></component>
      <button v-on:click="choosencomp('a')">a</button>
      <button v-on:click="choosencomp('b')">b</button>
      <button v-on:click="choosencomp('c')">c</button>
    </div>
    var app=new Vue({
      el: '#app',
      components:{
        acomp:{
          template:`<p>这里是组件A</p>`
        },
        bcomp:{
          template:`<p>这里是组件B</p>`
        },
        ccomp:{
          template:`<p>这里是组件C</p>`
        }
      },
      data:{whichcomp:""},
      methods:{
        choosencomp:function(x){
        this.whichcomp=x+"comp"}
      }
    })
  (2)把组件作为子组件放在内置组件<keep-alive>里,后者包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。
    A、include:字符串或正则表达式。只有名称匹配的组件会被缓存。
    B、在 2.2.0 及其更高版本中,activated 和 deactivated 将会在 <keep-alive> 树内的所有嵌套组件中触发。
      <keep-alive include="a,b" include='a' :include="/a|b/" :include="['a', 'b']">
      /* 字符串、正则、数组,也可以没有这些 */
        <component :is="view"></component>
      </keep-alive>
      <keep-alive>
        <comp-a v-if="a > 1"></comp-a>
        <comp-b v-else></comp-b>
      </keep-alive>
      <keep-alive>
        <router-view v-if="$route.meta.keepAlive"></router-view>
      </keep-alive>
      <transition>
        <keep-alive>
          <component :is="view"></component>
        </keep-alive>
      </transition>
 
三、自定义组件
1、自定义组件-图标svg-icon,来源,ai-web
  (1)配置
    A、package.json,引入依赖
      //npm i vite-plugin-svg-icons -D
      {
        "devDependencies": {
          "vite-plugin-svg-icons": "^2.0.1",
        }
      }
    B、vite.config.js,使用插件
      import createVitePlugins from './vite/plugins'
      export default defineConfig(
        ({command,mode})=>{
          plugins: [
            createVitePlugins(),
          ],
        }
      )
    C、createVitePlugins,存入插件
      import vue from '@vitejs/plugin-vue'
      import createSvgIcon from './svg-icon'
      export default function createVitePlugins(viteEnv, isBuild = false) {
        const vitePlugins = [vue()]//插件的存储容器
        vitePlugins.push(createSvgIcon(isBuild))
        return vitePlugins
      }
    D、createSvgIcon,生成插件
      import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
      import path from 'path'
      export default function createSvgIcon(isBuild) {
        return createSvgIconsPlugin({
          iconDirs: [path.resolve(process.cwd(), 'src/assets/icons/svg')],
          symbolId: 'icon-[dir]-[name]',//与iconName关联,注意iconName的前缀#
          svgoOptions: isBuild
        })
      }
    附、B,C,D步骤可以简化为本步骤
      来源,https://blog.csdn.net/weixin_53731501/article/details/125478380  
      //vite.config.js中配置,使用vite-plugin-svg-icons插件显示本地svg图标
      import path from 'path'
      import {createSvgIconsPlugin} from 'vite-plugin-svg-icons'
      export default defineConfig((command) => {
        return {
          plugins: [
            createSvgIconsPlugin({
              iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')],// 指定要缓存的文件夹
              symbolId: '[name]'// 指定symbolId格式
            })
          ],
        }
      })
  (2)定义
    <template>
      <svg :class="svgClass">
        <use :xlink:href="iconName" :fill="color" />
      </svg>
    </template>
    <script>
      export default defineComponent({
        props: {
          nameIcon: {
            type: String,
            required: true
          },
          className: {
            type: String,
            default: ''
          },
          color: {
            type: String,
            default: ''
          },
        },
        setup(props) {
          return {
            iconName: computed(() => `#icon-${props.nameIcon}`),
            svgClass: computed(() => {
              if (props.className) {
                return `svg-icon ${props.className}`
              }
              return 'svg-icon'
            })
          }
        }
      })
    </script>
  (3)注入
    import { createApp } from 'vue'
    import App from './App.vue'
    import SvgIcon from '@/components/SvgIcon'
    const app = createApp(App)
    app.component('svg-icon', SvgIcon)//全局引入使用
    app.mount('#app')
  (4)使用,
    <svg-icon name-icon="clock" />,clock指向clock.svg
2、自定义组件-季度半年,来源:bcbf-web
  附、注入图标,main.js --注释版
    import { createApp } from 'vue'
    import App from './App.vue'
    const app = createApp(App)
    import router from './router'
    import '@/permission'
    app.use(router)
    import store from './store'
    import useUserStore from '@/store/modules/user'
    import { findArrByObj } from '@/utils/index'
    const userStore = useUserStore(store) //defineStore的返回值是useUserStore,本行代码写在$hasPermit内部较为适宜,自悟
    app.config.globalProperties.$hasPermit = function (name) {//判断有无权限
      let menus = JSON.parse(JSON.stringify(userStore.allMenus))
      let find = findArrByObj(menus, name, 'children', 'name')
      return !!find
    }
    app.use(store)
    //以下注入图标(插件)
    import * as ElementPlusIconsVue from '@element-plus/icons-vue'
    for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
      app.component(key, component)
    }
    //以下注入图标(自定义)
    import 'virtual:svg-icons-register' //导入一个虚拟模块,自动注册项目中的SVG图标,使项目使用SVG图标时,无需手动导入
    import SvgIcon from '@/components/SvgIcon'
    app.component('svg-icon', SvgIcon)
    //以下注入全局方法(自定义),格式化数字为美国数字
    app.config.globalProperties.$formatNumber = 
      function (value) {
        if (!value) return '';
        let nf = new Intl.NumberFormat('en-US')
        return nf.format(value);
      }
    //以下注入其它
    import "element-plus/theme-chalk/el-message-box.css"
    import "element-plus/theme-chalk/el-loading.css";
    import "element-plus/theme-chalk/el-message.css";
    import "element-plus/theme-chalk/el-notification.css";
    //以下似乎没必要
    import '@/mock'
    app.mount('#app')
  附、使用全局方法(自定义),格式化数字为美国数字//manyDate.vue
    <div>{{ $formatNumber(1234567.89) }}</div> 
    <script setup>
      import { getCurrentInstance } from 'vue';
      var formatNumber = getCurrentInstance().appContext.config.globalProperties.$formatNumber
      function clickArrow() {
        var num = formatNumber(1234567.89); 
      };
    </script>
  (1)引入依赖,package.json
    {
      "dependencies": {
        "@element-plus/icons-vue": "^2.1.0",
      }
    }
  (2)定义组件,src\view\report-normal\manyDate.vue
    <script setup>
      import { DArrowLeft, DArrowRight, Calendar, } from '@element-plus/icons-vue'
      const emit = defineEmits(['dateOk'])
      var { typeObj } = defineProps({
        typeObj: { }, //按四季显示,还是按半年显示
        isDisabled:{
          type:Boolean,
          default:false
        }
      });
      var nowWhich = ref('');//当前所在的季节或半年
      var clickWhich = ref(typeObj.type);//默认的或被点击的季节或半年
      var nowYear = typeObj.year;//当前所在的年份
      var usedYear = ref(nowYear);//被使用的年份
      var twoHalfYearRef = ref('');
      var fourSeasonsRef = ref('');
      var dateRangeStr = ref('');
      var dateRangeTempStr = ref('');
      var isShowTwoHalfYear = ref(false);
      var isShowFourSeasons = ref(false);
      function getNowWhich() {//获取当前所在的季节或半年
        var type = typeObj.type;
        var four = ['one','two','three','four','one'];
        var two = ['up','down','up'];
        if(four.indexOf(type) > -1){
          nowWhich.value = four[four.indexOf(type) + 1]
        }
        if(two.indexOf(type) > -1){
          nowWhich.value = four[four.indexOf(type) + 1]
        }
      }
      function clickInput() {
        usedYear.value = nowYear;
        if( ['up','down'].indexOf(clickWhich.value) !== -1 ){
          isShowTwoHalfYear.value = true;
          isShowFourSeasons.value = false;
          setTimeout(function(){
            twoHalfYearRef.value.focus();
          }, 150);
        }
        if( ['one','two','three','four'].indexOf(clickWhich.value) !== -1 ){
          isShowTwoHalfYear.value = false;
          isShowFourSeasons.value = true;
          setTimeout(function(){
            fourSeasonsRef.value.focus();
          }, 150);
        }
      }
      function getBlur() {
        isShowTwoHalfYear.value = false;
        isShowFourSeasons.value = false;
      }
      function clickArrow(type) {
        if(type === 'left') usedYear.value--;
        if(type === 'right') usedYear.value++;
      }
      function addSpace(num) { //添加空格
        var str = '\xa0';
        for(var i = 0; i < num; i++) str += '\xa0'
        return str;
      }
      function clickHalfYearOrSeason(type, isUseLifecycle, isOnMounted) {
        clickWhich.value = type;
        var obj = {
          one: {
            start: usedYear.value + '-01',
            end: usedYear.value + '-03'
          },
          two: {
            start: usedYear.value + '-04',
            end: usedYear.value + '-06'
          },
          three: {
            start: usedYear.value + '-07',
            end: usedYear.value + '-09'
          },
          four: {
            start: usedYear.value + '-10',
            end: usedYear.value + '-12'
          },
          up: {
            start: usedYear.value + '-01',
            end: usedYear.value + '-06'
          },
          down: {
            start: usedYear.value + '-07',
            end: usedYear.value + '-12'
          },
        };
        var start = obj[type].start;
        var end = obj[type].end;
        dateRangeTempStr.value = addSpace(6) + start + addSpace(8) + '-' + addSpace(8) + end;
        if(!isUseLifecycle){
          isShowTwoHalfYear.value = false;
          isShowFourSeasons.value = false;
          dateRangeStr.value = dateRangeTempStr.value; 
          emit('dateOk',[start, end])
        }else if(isOnMounted){
          dateRangeStr.value = dateRangeTempStr.value; 
          emit('dateOk',[start, end])
        }
      }
      onMounted( function() {
        getNowWhich();
        clickHalfYearOrSeason(clickWhich.value, true, true)
      });
      onUpdated( function() {
        clickHalfYearOrSeason(clickWhich.value, true)
      });
    </script>
    <template>
      <div style="width: 100%;">
        <el-input v-model="dateRangeStr" :disabled="isDisabled" :readonly="true" @click="clickInput()"> <!-- @blur="outerBlur" -->
          <template #prefix>
            <el-icon><Calendar /></el-icon>
          </template>
        </el-input>
        <div class="many-date" tabindex="-1" v-show="isShowFourSeasons" ref="fourSeasonsRef" @blur="getBlur">
          <div class="many-date-up">
            <el-icon class="many-date-up-content" @click="clickArrow('left')" ><DArrowLeft /></el-icon>
            <div class="many-date-up-content" >{{ usedYear }}</div>
            <el-icon class="many-date-up-content" @click="clickArrow('right')"><DArrowRight /></el-icon>
          </div>
          <el-divider class="many-date-divider"/>
          <div class="many-date-down">
            <div class="many-date-down-content" @click="clickHalfYearOrSeason('one')" 
              :class="[
                nowWhich=='one'?'many-date-down-content-bgNow':null,
                clickWhich=='one'?'many-date-down-content-bgClick':null,
              ]"
            >第一季度</div>
            <div class="many-date-down-content" @click="clickHalfYearOrSeason('two')" 
              :class="[
                nowWhich=='two'?'many-date-down-content-bgNow':null,
                clickWhich=='two'?'many-date-down-content-bgClick':null,
              ]"
            >第二季度</div>
          </div>
          <div class="many-date-down">
            <div class="many-date-down-content" @click="clickHalfYearOrSeason('three')" 
              :class="[
                nowWhich=='three'?'many-date-down-content-bgNow':null,
                clickWhich=='three'?'many-date-down-content-bgClick':null,
              ]"
            >第三季度</div>
            <div class="many-date-down-content" @click="clickHalfYearOrSeason('four')" 
              :class="[
                nowWhich=='four'?'many-date-down-content-bgNow':null,
                clickWhich=='four'?'many-date-down-content-bgClick':null,
              ]"
            >第四季度</div>
          </div>
        </div>
        <div class="many-date" tabindex="-1" v-show="isShowTwoHalfYear" ref="twoHalfYearRef" @blur="getBlur">
          <div class="many-date-up">
            <el-icon class="many-date-up-content" @click="clickArrow('left')"><DArrowLeft /></el-icon>
            <div class="many-date-up-content" >{{ usedYear }}</div>
            <el-icon class="many-date-up-content" @click="clickArrow('right')"><DArrowRight /></el-icon>
          </div>
          <el-divider class="many-date-divider"/>
          <div class="many-date-down">
            <div class="many-date-down-content" @click="clickHalfYearOrSeason('up')"  
              :class="[
                nowWhich=='up'?'many-date-down-content-bgNow':null,
                clickWhich=='up'?'many-date-down-content-bgClick':null,
              ]"
            >上半年</div>
            <div class="many-date-down-content" @click="clickHalfYearOrSeason('down')"  
              :class="[
                nowWhich=='down'?'many-date-down-content-bgNow':null,
                clickWhich=='down'?'many-date-down-content-bgClick':null,
              ]"
            >下半年</div>
          </div>
        </div>
      </div>
    </template>
    <style lang="scss">
      .many-date{
        width: 60%; 
        margin-left: 20%;
        border-radius: 4px;
        margin-top: 10px;
        position: absolute;
        z-index: 100;
        background: var(--el-bg-color-overlay);
        box-shadow: var(--el-box-shadow-light);
        &-up {
          display: flex;
          justify-content: space-between;
          align-items: center;
          padding: 10px 10% ;
          &-content {
            cursor: pointer;
            user-select: none;
            color: var(--el-text-color-regular);
          }
          &-content:hover {
            color: var(--el-color-primary);
          }
        }
        &-divider {
          margin: 0 10%; 
          width: 80%
        }
        &-down {
          display: flex; 
          justify-content: space-around;
          align-items: center;
          padding: 10px 10% ;
          &-content {
            padding: 4px 20px ;
            cursor: pointer;
            user-select: none;
            color: var(--el-text-color-regular);
            &-bgClick {
              background: #2162db;
              border-radius: 18px;
              color: #ffffff !important;
              font-weight: bolder;
            }
            &-bgNow {
              color: #2162db;
              font-weight: bolder;
            }
          }
          &-content:hover {
            color: var(--el-color-primary);
          }
        }
      }
    </style>
  (4)使用组件
    <script setup>
      import manyDate from './manyDate.vue'
      const ruleForm = ref({
        cycle: '',
      })
      const rules = ref({
        cycle: [
          {
            required: true,
            message: '请选择统计周期',
            trigger: 'blur',
          },
        ],
      })
      //以下为新增或改动内容
      var formRef = ref(null);
      const typeObj = ref({
        year: '',
        type: '',
      })
      function validateSelect() {
        formRef.value.validateField('period');//校验
      }
      function focusSelect() {
        formRef.value.clearValidate('period');//清除验证
      }
      function addZero(number) {
        return number.toString()[1] ? number : "0" + number;
      }
      function periodChange() {
        //需求:可根据选择的周期自动填充上一年、上半年、上一季度、上个月的日期
        var date = new Date;
        var month = date.getMonth();
        var fullYear = date.getFullYear();
        var period = createForm.value.period;
        if (period === '月') {
          if (month == 0) {//如果现在处在今年一月,那么默认为去年十二月
            month = 12;
            fullYear--;
          }
          myDate.value = [fullYear + '-' + addZero(1), fullYear + '-' + addZero(month)];//month可以用于表示上个月
        } else if (period === '季度') {
          var four = {
            3: 'four',
            6: 'one',
            9: 'two',
            12: 'three',
          };
          for (var key in four) {
            if (month < key) {
              if (key == 3) fullYear--;//如果现在处在今年第一季度,那么默认为去年第四季度
              typeObj.value.year = fullYear;
              typeObj.value.type = four[key];
              myDate.value = true;//为了绕过为空校验 @blur="validateSelect"
              break;
            }
          }
        } else if (period === '半年') {
          var two = {
            6: 'down',
            12: 'up',
          }
          for (var key in two) {
            if (month < key) {
              if (key == 6) fullYear--;//如果现在处在今年上半年,那么默认为去年下半年
              typeObj.value.year = fullYear;
              typeObj.value.type = two[key];
              myDate.value = true;//为了绕过为空校验 @blur="validateSelect"
              break;
            }
          }
        } else if (period === '全年') {
          fullYear--;
          myDate.value = fullYear.toString();
        }
      } 
      function handleDateOk(data){
        myDate.value = data
      }
      onMounted( function() {
        periodChange()
      });
    </script>
    <template>
      <el-form ref="formRef" style="max-width: 580px" :model="createForm" :rules="rules" label-width="auto"
        class="content-table-createForm">
        <el-form-item label="报表名称" prop="name">
          <el-input v-model="createForm.name" maxlength="50"
          show-word-limit placeholder="请输入报表名称" />
        </el-form-item>
        <el-form-item label="统计周期" prop="period">
          <!-- 如果在触发@change时,不给myDate赋值;那么触发@blur时,验证myDate不通过;many-date渲染后,myDate就有值了 -->
          <el-select 
            v-model="createForm.period" 
            placeholder="请选择统计周期" 
            @focus="focusSelect" 
            @change="periodChange" 
            @blur="validateSelect"  
            :disabled="!isAdd"
          >
            <el-option label="月" value="月" />
            <el-option label="季度" value="季度" />
            <el-option label="半年" value="半年" />
            <el-option label="全年" value="全年" />
          </el-select>
        </el-form-item>
        <el-form-item label="日期范围" prop="myDate" v-if="createForm.period === '月'">
          <el-date-picker v-model="myDate" type="monthrange" :editable="false" start-placeholder="开始日期"
            end-placeholder="结束日期" :disabled="!createForm.period" style="width: 100%;" format="YYYY-MM"
            value-format="YYYY-MM" @change="handleChange" />
        </el-form-item>
        <!-- 以下,使用自定义-组件 -->
        <el-form-item label="日期范围" prop="myDate" v-if="createForm.period === '季度'">
          <many-date :isDisabled="!isAdd" :typeObj="typeObj" @dateOk="handleDateOk" />
        </el-form-item>
        <el-form-item label="日期范围" prop="myDate" v-if="createForm.period === '半年'">
          <many-date :isDisabled="!isAdd" :typeObj="typeObj" @dateOk="handleDateOk" />
        </el-form-item>
        <!-- 以上,使用自定义-组件 -->
        <el-form-item label="日期范围" prop="myDate" v-if="createForm.period === '全年'">
          <el-date-picker v-model="myDate" type="year" :editable="false" start-placeholder="开始日期" end-placeholder="结束日期"
            :disabled="!createForm.period" style="width: 100%;" format="YYYY" value-format="YYYY" />
        </el-form-item>
        <el-form-item label="考核规则版本" prop="rule" style="position: relative;">
          <el-select v-model="assessmentRule" value-key="sid" placeholder="请选择考核规则">
            <el-option :label="item.name" :value="item" v-for="item in rulesData" :key="item.ruleSid" />
          </el-select>
          <el-button type="primary" text @click="jumpVersionDetail" class="jump-version-btn">查看详情</el-button>
        </el-form-item>
        <el-form-item label="报表字段" prop="displayField">
          <el-button type="primary" :icon="Edit" @click="showColumnDialog" />
        </el-form-item>
      </el-form>
    </template>
3、自定义组件-26小时-半成品,可演示,来源:bcbf-web
  (1)定义组件
    <template>
      <div class="timeInputBox" :id="'t1-'+id" :ref="'ref-'+id">
        <div style="display: flex;">
          <el-input ref="timeInput" :id="'t2-'+id" @focus="focus" v-model="input" :placeholder="placeholder"   class="centered-input"/>
          <span style="padding: 0 50px;">至</span>
          <el-input ref="timeInput" :id="'t2-'+id" @focus="focus" v-model="input" :placeholder="placeholder"   class="centered-input"/> 
        </div>
        <div :class="{ menuBox: true, activemenuBox: isShowOption }" :id="'t3-'+id">
          <div class="triangle" :id="'t4-'+id"></div>
          <div class="menuMain" :id="'t5-'+id" style="display: flex">
            <div class="two">
              <div class="container" :id="'t6-'+id" ref="startHour">
                <div class="timeItem" :id="'t7-'+id" :style="{ height:(clientHeight+7)+'px' }" v-for="item in [1, 2]"
                  :key="item+'-1'" />
                <div @click="clickTime('startHour', index)" :ref="'timeItem'+index" v-for="(num, index) in timeList.hour"
                  :key="index+'01'" :class="{ activeTime: index == selectHourIndex }" :id="'t8-'+id">{{ textbuil(num) }}
                </div>
                <div class="timeItem" :id="'t9-'+id" :style="{ height:(clientHeight+7)+'px' }" v-for="item in [1, 2]"
                  :key="item+'-2'" />
              </div>
              <div class="container" :id="'t10-'+id" ref="startMinute">
                <div class="timeItem" :id="'t11-'+id" :style="{ height:(clientHeight+7)+'px' }" v-for="item in [1, 2]"
                  :key="item+'-3'" />
                <div :id="'t12-'+id" @click="clickTime('startMinute', index)" v-for="(num, index) in timeList.minute"
                  :key="index+'02'" :class="{ activeTime: index == selectMinuteIndex }">{{ num }}
                </div>
                <div class="timeItem" :id="'t13-'+id" :style="{ height:(clientHeight+7)+'px' }" v-for="item in [1, 2]"
                  :key="item+'-4'" />
              </div>
              <div class="container" :id="'t30-'+id" ref="startSecond">
                <div class="timeItem" :id="'t31-'+id" :style="{ height:(clientHeight+7)+'px' }" v-for="item in [1, 2]"
                  :key="item+'-3'" />
                <div :id="'t32-'+id" @click="clickTime('startSecond', index)" v-for="(num, index) in timeList.second"
                  :key="index+'02'" :class="{ activeTime: index == selectSecondIndex }">{{ num }}
                </div>
                <div class="timeItem" :id="'t33-'+id" :style="{ height:(clientHeight+7)+'px' }" v-for="item in [1, 2]"
                  :key="item+'-4'" />
              </div>
              <div class="botBut" :id="'t4-'+id">
              </div>
              <div class="line" :id="'t17-'+id" :style="{ top: `calc(50% - ${clientHeight - 5}px)` }"></div>
              <div class="line" :id="'t18-'+id" :style="{ top: `calc(50% + ${5}px)` }"></div>
            </div>
            <div style="width:10%">至</div>
            <div class="two"> 
              <div class="container" :id="'t6-'+id" ref="overHour">
                <div class="timeItem" :id="'t7-'+id" :style="{ height:(clientHeight+7)+'px' }" v-for="item in [1, 2]"
                  :key="item+'-1'" />
                <div @click="clickTime('overHour', index)" :ref="'timeItem'+index" v-for="(num, index) in timeList.hour"
                  :key="index+'01'" :class="{ activeTime: index == selectHourIndex }" :id="'t8-'+id">{{ textbuil(num) }}
                </div>
                <div class="timeItem" :id="'t9-'+id" :style="{ height:(clientHeight+7)+'px' }" v-for="item in [1, 2]"
                  :key="item+'-2'" />
              </div>
              <div class="container" :id="'t10-'+id" ref="overMinute">
                <div class="timeItem" :id="'t11-'+id" :style="{ height:(clientHeight+7)+'px' }" v-for="item in [1, 2]"
                  :key="item+'-3'" />
                <div :id="'t12-'+id" @click="clickTime('overMinute', index)" v-for="(num, index) in timeList.minute"
                  :key="index+'02'" :class="{ activeTime: index == selectMinuteIndex }">{{ num }}
                </div>
                <div class="timeItem" :id="'t13-'+id" :style="{ height:(clientHeight+7)+'px' }" v-for="item in [1, 2]"
                  :key="item+'-4'" />
              </div>
              <div class="container" :id="'t30-'+id" ref="overSecond">
                <div class="timeItem" :id="'t31-'+id" :style="{ height:(clientHeight+7)+'px' }" v-for="item in [1, 2]"
                  :key="item+'-3'" />
                <div :id="'t32-'+id" @click="clickTime('overSecond', index)" v-for="(num, index) in timeList.second"
                  :key="index+'02'" :class="{ activeTime: index == selectSecondIndex }">{{ num }}
                </div>
                <div class="timeItem" :id="'t33-'+id" :style="{ height:(clientHeight+7)+'px' }" v-for="item in [1, 2]"
                  :key="item+'-4'" />
              </div>
              <div class="botBut" :id="'t4-'+id">
                <el-button class="elbutton" :id="'t15-'+id" type="text" @click="confirmTime()">确定</el-button>
                <el-button class="elbutton" :id="'t16-'+id" style="color: #333;" type="text"
                  @click="cancelTime()">取消</el-button>
              </div>
              <div class="line" :id="'t17-'+id" :style="{ top: `calc(50% - ${clientHeight - 5}px)` }"></div>
              <div class="line" :id="'t18-'+id" :style="{ top: `calc(50% + ${5}px)` }"></div>
            </div>
          </div>
        </div>
      </div>
    </template>
    <script>
      export default {
        name: "timePicker",
        components: {},
        props: {
          id: { //多个时必传且不能相同
            type: String,
            default: '',
          },
          time: { //格式,10:30
            type: String,
            default: '00:00:00',
          },
          placeholder: {
            type: String,
            default: '请选择时间',
          },
          timeList: { //hour,24时=>次日00时,25时=>次日01时,以此类推
            type: Object,
            default: () => {
              return {
                hour: ["00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30"],
                minute: ["00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30", "31", "32", "33", "34", "35", "36", "37", "38", "39", "40", "41", "42", "43", "44", "45", "46", "47", "48", "49", "50", "51", "52", "53", "54", "55", "56", "57", "58", "59"],
                second: ["00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30", "31", "32", "33", "34", "35", "36", "37", "38", "39", "40", "41", "42", "43", "44", "45", "46", "47", "48", "49", "50", "51", "52", "53", "54", "55", "56", "57", "58", "59"],
              }
            }
          }
        },
        data() {
          return {
            startTime: '',
            overTime: '',
            input: '',
            isMounted: true,
            isShowOption: false,
            clientHeight: 36,
            selectHourIndex: -1,
            selectMinuteIndex: -1,
            selectSecondIndex: -1,
            setTimeing: false,
          };
        },
        mounted() {
          // 点击页面其他地方关闭时间选择组件
          document.addEventListener('click', (e) => {
            const ids = e.target.id?.split("-")[1]
            if (this.id != ids) {
              this.isShowOption = false
            }
          })
          // 监听时/分/秒滚动
          this.$refs.startHour.addEventListener('scroll', () => {
            const scrollTop = this.$refs.startHour.scrollTop
            const index =this.selectHourIndex = Math.round(scrollTop / this.clientHeight); // 加粗选中数字
            this.input = this.textbuil(this.timeList.hour[this.selectHourIndex])+':'+this.timeList.minute[this.selectMinuteIndex]+':'+this.timeList.second[this.selectSecondIndex]
          })
          this.$refs.startMinute.addEventListener('scroll', () => {
            const scrollTop = this.$refs.startMinute.scrollTop;
            const index =this.selectMinuteIndex = Math.round(scrollTop / this.clientHeight); // 加粗选中数字
            this.input = this.textbuil(this.timeList.hour[this.selectHourIndex])+':'+this.timeList.minute[this.selectMinuteIndex]+':'+this.timeList.second[this.selectSecondIndex]
          })
          this.$refs.startSecond.addEventListener('scroll', () => {
            const scrollTop = this.$refs.startSecond.scrollTop;
            const index =this.selectSecondIndex = Math.round(scrollTop / this.clientHeight); // 加粗选中数字
            this.input = this.textbuil(this.timeList.hour[this.selectHourIndex])+':'+this.timeList.minute[this.selectMinuteIndex]+':'+this.timeList.second[this.selectSecondIndex]
          })
          // 监听时/分/秒滚动
          this.$refs.overHour.addEventListener('scroll', () => {
            const scrollTop = this.$refs.overHour.scrollTop
            const index =this.selectHourIndex = Math.round(scrollTop / this.clientHeight); // 加粗选中数字
            this.input = this.textbuil(this.timeList.hour[this.selectHourIndex])+':'+this.timeList.minute[this.selectMinuteIndex]+':'+this.timeList.second[this.selectSecondIndex]
          })
          this.$refs.overMinute.addEventListener('scroll', () => {
            const scrollTop = this.$refs.overMinute.scrollTop;
            const index =this.selectMinuteIndex = Math.round(scrollTop / this.clientHeight); // 加粗选中数字
            this.input = this.textbuil(this.timeList.hour[this.selectHourIndex])+':'+this.timeList.minute[this.selectMinuteIndex]+':'+this.timeList.second[this.selectSecondIndex]
          })
          this.$refs.overSecond.addEventListener('scroll', () => {
            const scrollTop = this.$refs.overSecond.scrollTop;
            const index =this.selectSecondIndex = Math.round(scrollTop / this.clientHeight); // 加粗选中数字
            this.input = this.textbuil(this.timeList.hour[this.selectHourIndex])+':'+this.timeList.minute[this.selectMinuteIndex]+':'+this.timeList.second[this.selectSecondIndex]
          })
          this.setInput()
        },
        methods: {
          focus() {
            this.isShowOption = true
            this.$emit('focus')
          },
          clickTime(type, index) {
            this.$refs[type].scrollTop = index * this.clientHeight;
          },
          confirmTime() {
            if (this.setTimeing) return
            this.setTimeing = true
            setTimeout(() => {
              this.setTimeing = false
            }, 100);
            // 以上阻止连续点击
            this.startTime = this.timeList.hour[this.selectHourIndex]+':'+this.timeList.minute[this.selectMinuteIndex]+':'+this.timeList.second[this.selectSecondIndex]
            this.startTime = this.timeList.hour[this.selectHourIndex]+':'+this.timeList.minute[this.selectMinuteIndex]+':'+this.timeList.second[this.selectSecondIndex]
            this.$emit('sendTime', this.startTime);
            this.isShowOption = false
          },
          cancelTime() {
            this.$emit('sendTime', this.startTime);
            this.isShowOption = false
          },
          textbuil(data) {
            /* 
            if (data > 23) {
              data = data * 1
              return '次日0'+(data - 24)
            } */
            return data
          },
          setInput() {
            const [hourDistance, minuteDistance, secondDistance] = this.time.split(':')
            this.startTime = this.input = this.textbuil(hourDistance)+':'+minuteDistance+':'+secondDistance
          },
        },
        watch: {
          isShowOption: {
            handler: function (newVal) {
              if (newVal) {
                var num = this.isMounted? 8:0;
                this.isMounted = false;
                this.clientHeight = this.$refs.timeItem0[0].clientHeight;
                const [hourDistance = 0, minuteDistance = 0, secondDistance = 0] = this.startTime.split(':');
                this.$refs.startHour.scrollTop = hourDistance * this.clientHeight + num;
                this.$refs.startMinute.scrollTop = minuteDistance * this.clientHeight + num;
                this.$refs.startSecond.scrollTop = secondDistance * this.clientHeight + num;
                /////////////////////////////////////////////
                this.$refs.overHour.scrollTop = hourDistance * this.clientHeight + num;
                this.$refs.overMinute.scrollTop = minuteDistance * this.clientHeight + num;
                this.$refs.overSecond.scrollTop = secondDistance * this.clientHeight + num;
              } 
            }
          },
        },
      };
    </script>
    <style lang="scss">
      .timeInputBox {
        position: relative;
        .menuBox {
          position: absolute;
          width: 500px;
          height: 230px;
          z-index: 9;
          transition: .25s;
          overflow: hidden;
          transform: scaleY(0);
          transform-origin: top;
          box-shadow: 10px 9px 20px -10px #e1dfdf;
          .triangle {
            width: 0;
            height: 0;
            border-top: 6px solid transparent;
            border-bottom: 6px solid #fff;
            border-left: 6px solid transparent;
            border-right: 6px solid transparent;
            margin-left: 10px;
            margin-bottom: -1px;
            z-index: 10;
            position: absolute;
            top: 0;
            left: 10px;
          }
          .menuMain {
            background-color: #fff;
            height: calc(100% - 10px);
            width: 100%;
            border-radius: 4px;
            border: 1px solid #eaeaea;
            z-index: 9;
            margin-top: 10px;
            box-shadow: 2px 1px 20px -11px #333;
            display: flex;
            .two {
              width: 40%;
              .container {
                float: left;
                height: calc(100% - 36px);
                width: 33%;
                overflow: auto;
                text-align: center;
                font-size: 12px;
                color: #606266;
                cursor: pointer;
                :hover {
                  background-color: #4d7fff16;
                }
                .timeItem:hover {
                  background-color: transparent;
                }
                .activeTime {
                  font-weight: bold;
                  color: #000;
                  &:hover {
                    background-color: transparent;
                  }
                }
              }
              .botBut {
                width: 50%;
                height: 36px;
                border-top: 1px solid #d7d7d7;
                position: absolute;
                bottom: 2px;
                background-color: #ffffff;
                .elbutton {
                  font-size: 12px;
                  float: right;
                  margin: auto 10px;
                  font-weight: 500;
                }
              }
              .line {
                position: absolute;
                height: 1px;
                width: calc((100% - 40px));
                left: 20px;
                background-color: #e1e1e1;
                // border-top: 1px solid #e1e1e1;
                // border-bottom: 1px solid #e1e1e1;
              }
            }
          }
        }
        .activemenuBox {
          transform: scaleY(1);
        }
      }
      .container::-webkit-scrollbar {
        width: 7px;
        // height: 5px;
      }
      .container::-webkit-scrollbar-thumb {
        // background: linear-gradient(to bottom right, #4d7fff 0%, #1a56ff 100%);
        background-color: #dfdfdf;
        border-radius: 5px;
      }
      .container::-webkit-scrollbar-track {
        background-color: #fafafa;
        // border: 1px solid #ccc;
      }
      .container::-webkit-scrollbar-button {
        background-color: #fff;
        border-radius: 5px;
      }
      .container::-webkit-scrollbar-button:hover {
        background-color: #c1c1c1;
      }
      .centered-input .el-input__inner {
        text-align: center !important;
      }
    </style>
  (2)使用组件 
    import TimePicker from "./timePicker";
    <el-form-item label="播出时间--1" >
      <TimePicker :id="'32'" :time="'20:34:58'" />
    </el-form-item>
4、自定义组件-添加水印,来源:bcbf-web
  附、添加水印组件的实现逻辑
    A、向组件传递图片地址、水印初始文字、监听事件(把base64的url转化成二进制文件,发给后台)
    B、在组件中,监听图片地址,加载图片完成后,将图片和初始文字分别绘制到canvas上
    C、在组件中,监听文字配置,如旋转角度变化时,将文字绘制到canvas上
    D、在组件中,发射事件,将canvas的base64的url发给父组件
  附、为什么vue3的有些逻辑写在watch里,而不写在updated里
    A、逻辑写在updated里,watch数据没有改变时,逻辑也会执行
  (1)定义组件
    <script setup>
      const props = defineProps({
        imgUrl: {
          type: String,
          default: ''
        },
        content:{
          type: String,
          default: ''
        }
      });
      const emit = defineEmits(['watermarkUpdate']);
      let maxHeight = ref(650)
      let maxWidth = ref(540)
      const config = reactive({
        content: '',
        fontSize: 22,
        color: 'rgba(157, 141, 141, 0.86)',
        rotate: -22,
        gap: [10, 10],
      });
      const graph = ref(null);
      const canRefVas = ref(null);
      const tempImg = ref(null); //这是一个img标签,里面包含src属性,指向图片地址
      const loading = ref(false)
      const handleSave = () => {
        loading.value = true
        // toDataURL
        // 
        const imageData = canRefVas.value.toDataURL('image/png'); 
        // 将canvas对象中的图像数据转换为base64编码的dataURL
        // 这种dataURL可以直接在HTML中使用,或者通过JavaScript发送给服务器进行保存或传输
        emit('watermarkUpdate', imageData, config.content);
        loading.value = false
      };
      const drawImg = () => {
        // 重新设置canvas的宽高,会清空画布
        let w = tempImg.value.width;
        let h = tempImg.value.height;
        canRefVas.value.width = w;
        canRefVas.value.height = h;
        let hs = maxHeight.value/h;
        let ws = maxWidth.value/w;
        if(hs>ws){
          maxHeight.value = h*ws
          maxWidth.value = 540
        } else {
          maxWidth.value = w*hs
          maxHeight.value = 650
        }
        console.log('----',tempImg.value.width, tempImg.value.height, maxHeight.value, maxWidth.value)
        canRefVas.value.getContext('2d').drawImage(tempImg.value, 0, 0, w, h);
        // drawImage(image, x, y, width, height)方法用于在canvas上下文中绘制图像
        // image:要绘制的图像源。这可以是一个
        //    HTMLImageElement(例如通过new Image()创建并加载后的图像对象)
        //    HTMLCanvasElement(另一个canvas元素)
        //    HTMLVideoElement(视频元素)
        // x和y:图像在画布上的起始坐标,坐标原点(0,0)位于canvas的左上角
        // width和height:用于指定绘制图像的宽度和高度,可选
        //    如果不提供,图像将以其原始大小进行绘制;如果提供,可以对图像进行缩放,假如,
        //    图像的宽高为(3840 2160),正好铺满标签样式(class、style)的宽高,<canvas style="width:400px;height:400px"></canvas>
        //    设置为(1920,1080),图像的宽高将缩小为原来的一半绘制在canvas标签里,标签里将会有3/4的空间是空白
        //    设置为(7680,4320),图像的宽高将放大为原来的二倍绘制在canvas标签里,图像将会有3/4的面积隐藏在标签外
        //    图像的宽高绘制在标签样式的宽高上,图像的宽高导致,标签属性的宽高失效,<canvas width=400 height=400></canvas>
        //    图像的宽高、标签属性的宽高、标签样式的宽高
      };
      const drawText = () => {
        /* if(!config.content){//http://10.70.38.84/browse/YANSHOU-10133
          return 
        } */
        drawImg();
        let w = tempImg.value.width;
        config.fontSize = parseInt(w/30);
        let textCtx = canRefVas.value.getContext('2d');
        textCtx.fillStyle = config.color;
        textCtx.font = config.fontSize + 'px Arial' ;
        //以下获取文字的宽高
        let text = textCtx.measureText('啊')
        let textWidth = text.width;
        let textHeight = text.actualBoundingBoxAscent + text.actualBoundingBoxDescent;
        //基线:是一条虚拟的线,文本中的字符都是基于这条线进行排列的(‌26个大小写字母在四线三格中的书写规范)
        //actualBoundingBoxAscent:从文本的基线到文本的最顶部的垂直距离,它衡量的是文本在基线上方所占据的空间高度
        //actualBoundingBoxDescent:从文本的基线到文本的最底部的垂直距离。它衡量的是文本在基线下方所占据的空间高度
        const list = config.content? config.content.split(/[(\r\n)\r\n]+/):[]; //一次水印可能有多行
        let multiple = 0;
        list.forEach((item) => {
          multiple = Math.max(multiple, item.length);
        });
        console.log(list)
        let maxWidth = multiple*textWidth + config.gap[0]*10; //一次水印所需的最大宽度
        let maxHeight = multiple*textHeight + config.gap[1]*10; //一次水印所需的最大高度
        maxWidth = Math.max(maxWidth, 100);
        maxHeight = Math.max(maxHeight, 100);
        let iii = 0;
        for (let x=0; x<canRefVas.value.width; x+=maxWidth) { //每行多次水印的总宽度不超过画布的宽度(从左向右绘制)
          for (let y=0,z=0; y<canRefVas.value.height; y+=maxHeight,z++) { //每列多次水印的总高度不超过画布的高度(从上向下绘制)
            textCtx.save(); //保存当前环境
            console.log('--'+(iii++)+'--', x, [0,maxWidth/2][z%2], y );
            textCtx.translate(x+[0,maxWidth/2][z%2], y); //translate(x,y)每次水印的起始坐标(x,y)
            textCtx.rotate(config.rotate*Math.PI/180); //每次水印,文字旋转的角度
            list.forEach((item, index) => {//每次水印
              textCtx.fillText(item, 0, (config.fontSize+80)*index);//textCtx.fillText(text,x,y)每行文字的起始坐标(x,y),与strokeText相似
            });
            textCtx.restore(); //返回已保存过的环境
          }
        }
      };
      watch(() => props.imgUrl, () => {
        if (props.imgUrl.length === 0) {
          return;
        }
        tempImg.value = new Image;
        tempImg.value.onload = function () {
          drawText();
          return canRefVas.value;
        };
        config.content = props.content;
        tempImg.value.src = props.imgUrl;
      }, { immediate: true });
      watch(config, () => {
        drawText()
      })
    </script>
    <template>
      <div class="watermark__wrap">
        <div ref="graph" class="watermark__image">
          <canvas ref="canRefVas" :style="{'max-height': maxHeight+'px','max-width':maxWidth+'px'}" ></canvas>
        </div>
        <el-form class="watermark__form" :model="config" label-position="top" label-width="50px">
          <el-form-item label="文字">
            <el-input  
              type="textarea"  
              :rows="6"
              style="width: 600px"
              maxlength="100"
              placeholder="请输入水印信息"
              show-word-limit
              v-model="config.content" 
            />
          </el-form-item>
          <el-form-item label="颜色">
            <el-color-picker v-model="config.color" show-alpha />
          </el-form-item>
          <el-form-item label="字号">
            <el-slider v-model="config.fontSize" :max="500" />
          </el-form-item>
          <el-form-item label="旋转">
            <el-slider v-model="config.rotate" :min="-180" :max="180" />
          </el-form-item>
          <el-form-item label="间隙">
            <el-space>
              <el-input-number v-model="config.gap[0]" controls-position="right" />
              <el-input-number v-model="config.gap[1]" controls-position="right" />
            </el-space>
          </el-form-item>
          <el-form-item>
            <el-button type="primary" @click="handleSave" :loading="loading">确定</el-button>
          </el-form-item>
        </el-form>
      </div>
    </template>
    <style lang="scss">
      .watermark {
        &__wrap {
          display: flex;
          flex-direction: row;
        }
        &__image {
          display: flex;
          align-items: center;
          justify-content: center;
          width: 558px;
          margin-right: 10px;
          height: 650px;
        }
      }
    </style>
  (2)使用组件 
    <el-dialog v-model="showEditWatermark" width="1200">
      <Watermark :imgUrl="editWatermarkImageUrl" v-if="showEditWatermark" :content="watermark" @watermarkUpdate="handleWatermarkUpdate"/>
    </el-dialog>
    const handleWatermarkUpdate = (imageURL, content) => {//添加水印
      showEditWatermark.value = false;
      applicationInfo.value.licenses[editWatermarkIndex.value].licenseImageUrl = imageURL;
      applicationCheckForm.value.licenses[editWatermarkIndex.value].watermark = content
      let file = {
        raw: dataURLtoFile( imageURL, applicationInfo.value.licenses[editWatermarkIndex.value].licenseName + '.png' )
      }
      uploadFileTool( file, 'license' ).then(res => {
        applicationCheckForm.value.licenses[editWatermarkIndex.value].fileId = res.data.fileInfoId
      })
    }
    // 将base64转换为文件
    function dataURLtoFile(dataUrl, filename) { 
      var arr = dataUrl.split(','),
      mime = arr[0].match(/:(.*?);/)[1],
      bstr = atob(arr[1]),
      n = bstr.length,
      u8arr = new Uint8Array(n);
      while (n--) {
        u8arr[n] = bstr.charCodeAt(n);
      }
      return new File([u8arr], filename, { type: mime });
    }

四、组件传值通信(“传值传参传数据”)
  附、子组件改变父组件数据的方式,与单向数据流无关
    A、子组件发射事件,父组件监听事件,改变自身的值
    B、子组件给父组件传来的函数传参并执行
    C、子组件给父组件传来的对象的某个属性赋值
  附、单向数据流:js改变数据源(本地存储vuex),数据源改变页面,不是js直接改变页面
1、在Vue2中,有以下几种组件通信方式
  (1)Vuex(全局状态管理)
    A、Vuex是一个专为Vue.js应用程序开发的状态管理模式
    B、通过mutations、actions和getters来修改和获取状态,实现不同组件之间共享数据
  (2)Props/$emit(父子组件通信)
    A、父组件通过props向子组件传递数据
    B、子组件通过$emit触发自定义事件并向父组件传递数据
  (3)Provide/Inject(祖先和后代组件通信)
    A、在祖先组件中使用provide提供数据或方法
    B、在后代组件中使用inject来注入这些数据或方法。这种方式在Vue2中需要借助插件来实现响应式
    C、示例
      a、祖先组件(Provider 组件)
        <template>
          <div>
            <child-component></child-component>
          </div>
        </template>
        <script>
          import ChildComponent from './ChildComponent.vue';
          export default {
            components: {
              ChildComponent,
            },
            provide() {
              return {
                message: 'Hello from parent',
              };
            },
          };
        </script>
      b、后代组件(Consumer 组件)
        <template>
          <div>{{ injectedMessage }}</div>
        </template>
        <script>
          export default {
            inject: ['message'],
            data() {
              return {
                injectedMessage: '',
              };
            },
            created() {
              this.injectedMessage = this.message;
            },
          };
        </script>
  (4)Event Bus(任意通信,自定义事件总线)
    A、创建一个新的Vue实例作为事件总线
    B、在组件中通过$emit触发事件,在其他组件中通过$on监听事件来实现通信
    C、示例
      a、创建事件总线
        //eventBus.js
        import Vue from 'vue';
        export const eventBus = new Vue();
      b、发送事件
        <template>
          <div>
            <button @click="sendEvent">Send Event</button>
          </div>
        </template>
        <script>
          import { eventBus } from './eventBus';
          export default {
            methods: {
              sendEvent() {
                eventBus.$emit('customEvent', { message: 'Hello from component A' });
              },
            },
          };
        </script>
      c、接收事件
        <template>
          <div>{{ receivedMessage }}</div>
        </template>
        <script>
          import { eventBus } from './eventBus';
          export default {
            data() {
              return {
                receivedMessage: '',
              };
            },
            created() {
              eventBus.$on('customEvent', (data) => {
                this.receivedMessage = data.message;
              });
            },
          };
        </script>
  (5)$parent/$children(父子组件直接访问)
    A、在子组件中可以通过$parent访问父组件实例
    B、在父组件中可以通过$children访问子组件实例,但这种方式不推荐,因为它使组件之间的关系变得不清晰且难以维护
    C、示例
      a、父组件
        <template>
          <div>
            <h1>Parent Component</h1>
            <child-one></child-one>
            <child-two></child-two>
            <button @click="parentMethod">Parent Method</button>
            <button @click="callChildrenMethods">Call Children Methods</button>
          </div>
        </template>
        <script>
          import ChildOne from './ChildOne.vue';
          import ChildTwo from './ChildTwo.vue';
          export default {
            components: {
              ChildOne,
              ChildTwo
            },
            methods: {
              parentMethod() {
                console.log('Parent method called.');
              },
              callChildrenMethods() {
                // 使用 $children 访问子组件实例并调用子组件方法
                this.$children.forEach(child => {
                  if (child.name === 'ChildOne') {
                    child.childOneMethod();
                  } else if (child.name === 'ChildTwo') {
                    child.childTwoMethod();
                  }
                });
              }
            }
          };
        </script>
      b、子组件1
        <template>
          <div>
            <h2>Child One</h2>
            <button @click="childOneMethod">Son Method</button>
          </div>
        </template>
        <script>
          export default {
            name: 'ChildOne',
            methods: {
              childOneMethod() {
                this.$parent.parentMethod();
                console.log('Child One method called.');
              }
            }
          };
        </script>
      c、子组件2
        <template>
          <div>
            <h2>Child Two</h2>
            <button @click="childTwoMethod">Son Method</button>
          </div>
        </template>
        <script>
          export default {
            name: 'ChildTwo',
            methods: {
              childTwoMethod() {
                this.$parent.parentMethod();
                console.log('Child Two method called.');
              }
            }
          };
        </script>
2、在Vue3中,有以下几种组件通信方式
  (1)Vuex(全局状态管理)
    A、Vuex仍然是一种用于集中管理应用状态的方式
    B、通过mutations、actions和getters来修改和获取状态,实现不同组件之间共享数据
  (2)Props/emits(父子组件通信)
    A、父组件通过props向子组件传递数据
    B、子组件通过defineEmits定义并使用emit触发自定义事件向父组件传递数据
  (3)Provide/Inject(祖先和后代组件通信)
    A、在祖先组件中使用provide提供数据或方法
    B、在后代组件中使用inject注入这些数据或方法。在Vue3中可以直接提供响应式数据
    C、示例一
      a、祖先组件(Provider 组件)
        <script setup>
          import { provide } from 'vue';
          const message = 'Hello from provider!';
          provide('sharedMessage', message);
        </script>
      b、后代组件(Consumer 组件)
        <script setup>
          import { inject } from 'vue';
          const sharedMessage = inject('sharedMessage');
          console.log(sharedMessage);
        </script>
    D、示例二,提供的数据是一个响应式对象
      a、祖先组件(Provider 组件)
        <script setup>
          import { reactive, provide } from 'vue';
          const sharedData = reactive({
            count: 0
          });
          provide('sharedData', sharedData);
        </script>
      b、后代组件(Consumer 组件)
        <script setup>
          import { inject } from 'vue';
          const sharedData = inject('sharedData');
          function incrementCount() {
            sharedData.count++;
          }
        </script>
  (4)Event Bus(任意通信,自定义事件总线)
    A、创建一个新的Vue实例作为事件总线
    B、通过$emit和$on进行非父子组件之间的通信
    C、示例
      a、创建事件总线
        //eventBus.js
        import { createApp } from 'vue';
        const eventBus = createApp({});
        export default eventBus;
      b、发送事件
        <script setup>
          import eventBus from './eventBus';
          function sendEvent() {
            eventBus.$emit('customEvent', { message: 'Hello from component A' });
          }
        </script>
      c、接收事件
        <script setup>
          import eventBus from './eventBus';
          eventBus.$on('customEvent', (data) => {
            console.log(data.message);
          });
        </script>
  (5)组合式API(CompositionAPI)中的响应式变量共享
    A、通过创建一个包含响应式变量的模块
    B、在多个组件中引入并使用这些变量来实现通信
    C、示例
      a、创建共享模块(sharedData.js)
        import { reactive } from 'vue';
        const sharedData = reactive({
          count: 0,
          message: 'Initial message',
        });
        const incrementCount = () => {
          sharedData.count++;
        };
        const updateMessage = (newMessage) => {
          sharedData.message = newMessage;
        };
        export { sharedData, incrementCount, updateMessage };
      b、组件A使用共享数据
        <script setup>
          import { sharedData, incrementCount, updateMessage } from './sharedData';
          console.log('Component A - Initial count:', sharedData.count);
          console.log('Component A - Initial message:', sharedData.message);
          incrementCount();
          updateMessage('Updated message from Component A');
        </script>
      c、组件B使用共享数据
        <script setup>
          import { sharedData } from './sharedData';
          console.log('Component B - Updated count:', sharedData.count);
          console.log('Component B - Updated message:', sharedData.message);
        </script>
  (6)Teleport(传送组件内容)
    A、不是严格意义上的组件通信方式
    B、但可以将一个组件的内容传送到指定的DOM节点,在某些场景下可以实现特定的布局和交互效果
2、以下vue2通过属性传参(函数)实现子向父传值(可演示)
  <!DOCTYPE html>
  <html>
    <head>
      <meta charset="utf-8">
      <title>Vue子组件给父组件传值</title>
      <script src="https://cdn.staticfile.org/vue/2.2.2/vue.min.js"></script>
    </head>
    <body>
      <div id="el">
        <my-parent></my-parent>
      </div>
      <script>
        Vue.component('my-parent', {
          template: "<div>" + "<div>父组件改变值为:{{parentData}}</div><my-child v-bind:son-method='parentAdd'></my-child></div>",
          data: function() {
            return {
              parentData: 0
            }
          },
          methods: {
            parentAdd: function(key) {
              this.parentData = key;
            }                                
          },
        })
        Vue.component('my-child', {
          template: "<div><button @click='thisMethod(\"222\")'>点击子组件-就向父组件传值</button></div>",
          props: {      
            sonMethod: {
              type: Function
            }
          },
          data: function() {
            return {
              counter: 0,
              sonData: 0
            }
          },
          methods: {
            thisMethod : function (key) {
              this.counter += 1;
              this.sonData = key + this.counter;
              this.sonMethod(this.sonData)
            }                               
          },
        })
        new Vue({
          el: '#el',
          data: {
            total: 0
          }
        })
      </script>
    </body>
  </html>
3、以下vue3通过监听发射(自定义事件)实现子向父传值(可演示,包含element-plus、element-plus-icons-vue)
  <html>
    <head>
      <meta http-equiv="content-type" content="text/html; charset=UTF-8">
      <link href="https://cdn.bootcdn.net/ajax/libs/element-plus/2.8.1/index.css" rel="stylesheet">
      <script src="https://cdn.jsdelivr.net/npm/vue@3.2.2"></script>
      <script src="https://cdn.bootcdn.net/ajax/libs/element-plus/2.8.1/index.full.js"></script>
      <script src="https://cdn.bootcdn.net/ajax/libs/element-plus-icons-vue/2.3.1/global.iife.js"></script>
    </head>
    <body>
      <div id="app">
        <div>以下是父组件内容</div>
        <div>案例来源:http://vue3_demo.sweetysoft.com/</div>
        <div>
          <el-input v-model="douBao" :readonly="true" style="width:400px;">
            <template #prefix>
              <el-icon><Calendar /></el-icon>
            </template>
          </el-input>
        </div>
        <el-button @click="changeSon">改变自身,进而改变子组件</el-button>
        <el-divider content-position="left" style="margin: 40px auto;" >分割线</el-divider>
        <div>以下是子组件内容</div>
        <my-component :custom-prop="douBao"  @custom-event="changeSon"></my-component>
      </div>
    </body>
  </html>
  <script>
    //以下是子组件的定义(在.html文件中,只能用vue2语法定义子组件)
    var MyComponent = {
      props: ['customProp'],
      template: 
        `<div>
          <div>{{ message }}</div>
          <div>这是来自父组件的数据:{{ customProp }}(通过属性传参)</div>
          <el-button @click="emitEvent">改变父组件,进而改变自身</el-button>
        </div>`,
      data() {
        return {
          message: '子组件自身数据',
        };
      },
      methods: {
        emitEvent() {
          this.$emit('custom-event');
        }
      }
    };
    //以下是父组件的js 
    var app = Vue.createApp({
      setup() {
        var msg1 = '在.html文件中,用vue3.2定义数据并使用';
        var msg2 = '在.html文件中,用vue3.2定义组件的属性、数据、方法';
        var isFlag = true;
        var douBao = Vue.ref(msg1);
        const changeSon = (son) => {
          isFlag = !isFlag;
          douBao.value = isFlag? msg1: msg2;
        };
        return {
          douBao,
          changeSon
        };
      }
    })
    //以下是全局js 
    app.component('my-component', MyComponent);
    app.use(ElementPlus);
    for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
      app.component(key, component);
    }
    app.mount('#app')
  </script>

五、vue组件的七种写法(组件定义),区别:html写在哪
 来源:https://www.cnblogs.com/hanguidong/p/9381830.html
1、字符串
  Vue.component('my-checkbox', {
    template: `<div class="checkbox-wrapper" @click="check()"><div :class="{ checkbox: true, checked: checked }"></div><div class="title"></div></div>`,
    data() {
      return { checked: false, title: 'Check me' }
    },
    methods: {
      check() { this.checked = !this.checked; }
    }
  });
2、字面量模板
  Vue.component('my-checkbox', {
    template: 
      `<div class="checkbox-wrapper" @click="check()">
        <div :class="{ checkbox: true, checked: checked }"></div>
        <div class="title"></div>
      </div>`,
    data() {
      return { checked: false, title: 'Check me' }
    },
    methods: {
      check() { this.checked = !this.checked; }
    }
  });
3、x-template
  <!-- 以下放在父组件的html里 -->
  <script type="text/x-template" id="checkbox-template">
    <div class="checkbox-wrapper" @click="check()">
      <div :class="{ checkbox: true, checked: checked }"></div>
      <div class="title"></div>
    </div>
  </script>
  <!-- 以下放在js文件里 -->
  Vue.component('my-checkbox', {
    template: '#checkbox-template',
    data() {
      return { checked: false, title: 'Check me' }
    },
    methods: {
      check() { this.checked = !this.checked; }
    }
  });
4、inline-template
  <!-- 以下放在父组件的html里 -->
  <my-checkbox inline-template>
    <div class="checkbox-wrapper" @click="check()">
      <div :class="{ checkbox: true, checked: checked }"></div>
      <div class="title"></div>
    </div>
  </my-checkbox>
  <!-- 以下放在js文件里 -->
  Vue.component('my-checkbox', {
    data() {
      return { checked: false, title: 'Check me' }
    },
    methods: {
      check() { this.checked = !this.checked; }
    }
  });
5、JSX
  Vue 中最有争议的模板选项是 JSX,一些开发者认为它丑陋、不直观,是对 Vue 精神的背叛。
  JSX 需要你先编译,因为它不能被浏览器运行。
  不过,如果你需要完整的 JavaScript 语言功能,又不太适应 render 函数过于抽象的写法,那么 JSX 是一种折衷的方式。
  Vue.component("my-checkbox", {
    data() {
      return { checked: false, title: "Check me" };
    },
    methods: {
      check() {
        this.checked = !this.checked;
      },
    },
    render() {
      return (
        <div class="checkbox-wrapper" onClick={this.check}>
          <div class={{ checkbox: true, checked: this.checked }}></div>
          <div class="title">{this.title}</div>
        </div>
      );
    },
  });
6、render 函数
  render 函数需要你将模板定义为 JavaScript 对象,这显然是最详细和抽象的模板选项。
  不过,优点是你的模板更接近编译器,并允许你使用完整的 JavaScript 功能,而不是指令提供的子集。
  Vue.component("my-checkbox", {
    data() {
      return { checked: false, title: "Check me" };
    },
    methods: {
      check() {
        this.checked = !this.checked;
      },
    },
    render(createElement) {
      return createElement(
        "div",
        {
          attrs: {
            class: "checkbox-wrapper",
          },
          on: {
            click: this.check,
          },
        },
        [
          createElement("div", {
            class: {
              checkbox: true,
              checked: this.checked,
            },
          }),
          createElement(
            "div",
            {
              attrs: {
                class: "title",
              },
            },
            [this.title]
          ),
        ]
      );
    },
  });
7、单文件
  (1)vue2.0之组件的es6写法
    <template>
      <div class="checkbox-wrapper" @click="check()">
        <div :class="{ checkbox: true, checked: checked }"></div>
        <div class="title"></div>
      </div>
    </template>
    <script>
      import comTab from '@/components/ComTab/com-tab'//导入别处组件
      export default {
        name: 'ComTpl',//组件名
        components: {   
          comTab,//此组件依赖的组件
        },
        props: {//用于接收父组件向子组件传递的数据
          tester: {
            type: Object
          }
        },
        data() {//本组件的数据
          return {
            tests: [],
            selectedTest: {}
          };
        },
        computed: {//计算属性,所有get,set的this上下文都被绑定到Vue实例
          方法名() {
            //.....
          }
        },
        created() {//生命周期之一。在模板渲染成html前调用,即通常初始化某些属性值,然后再渲染成视图
          //ajax请求放在created里,因为此时已经可以访问this和操作DOM了。
          this.classMap = ['a', 'b', 'c', 'd', 'e'];
          //如进行异步数据请求
          this.$http.get('/api/tests').then((response) => {
            response = response.body;
          if (response.errno === 0) {
            this.goods = response.data;
          }
        });
        },
        mounted() { //在模板渲染成html后调用,通常是初始化页面完成后,再对html的dom节点进行一些需要的操作
          this.$nextTick(() => {
            this._initScroll();
            this._initPics();
          });
        },
        methods: {//定义方法
          方法名(参数) {
            //...
          }
        },
        filters: {  //过滤器,可用来写,比如格式化日期的代码
          //例
          formatDate(time) {
            let date = new Date(time);
            return formatDate(date, 'yyyy-MM-dd hh:mm');
          }
        },
        watch: {
          // 第一种方式:监听整个对象,每个属性值的变化都会执行handler
          // 注意:属性值发生变化后,handler执行后获取的 newVal 值和 oldVal 值是一样的
          food: {// 每个属性值发生变化就会调用这个函数
            handler(newVal, oldVal) {
                console.log('oldVal:', oldVal)
                console.log('newVal:', newVal)
            },
            immediate: true,// 立即处理 进入页面就触发
            deep: true// 深度监听 属性的变化
          },
          // 第二种方式:监听对象的某个属性,被监听的属性值发生变化就会执行函数    
          'food.name'(newVal, oldVal) {// 函数执行后,获取的 newVal 值和 oldVal 值不一样
              console.log('oldVal:', oldVal)   // 冰激凌
              console.log('newVal:', newVal)   // 棒棒糖
          }
          // 其它
          demo(val,oldVal) {
            this.value = this.demo;
          }
        }
      },
    </script>
    <style rel="stylesheet/scss" lang="scss" scoped>
    </style>
  (2)vue3.0之组件的es6写法,见,vue3.0之“setup”
8、VUE3-组件定义(可演示)
  <!DOCTYPE html>
  <html>
  <head>
    <script src="https://cdn.bootcdn.net/ajax/libs/vue/3.2.39/vue.global.js"></script> 
  </head>
  <body>
    <div id="box">
      <vue3-component :myname="nameMy">插槽</vue3-component>
    </div>    
  </body>
  </html>   
  <script>
    var obj = {
      data(){
        return {
          nameMy:"张三"
        }
      },
      methods:{},
      computed:{}
    }
    var app = Vue.createApp(obj)
    app.component("vue3-component",{
      props:["myname"],
      template:`
        <div>
          {{myname}}
          <slot></slot>
        </div>
      `
    })
    app.mount("#box")
  </script>

六、插槽slot
  vue2.6.0前、后版本,定义时不一样,使用时都一样
1、vue2.6.0以前版本,
  (1)定义插槽,并在组件内使用
    <base-layout>
      <template slot="head" slot-scope="scope">
        {{ scope.user.firstName }}
      </template>
    </base-layout>
  (2)定义组件,并渲染插槽,两个版本都一样
    <div>
      <slot name="head" :user="obj">
        {{ user.text }}<!-- 后备内容 -->
      </slot>
    </div>
2、vue2.6.0及以后版本,(仍支持以前版本的写法)
  (1)定义插槽,并在组件内使用
    <base-layout>
      <template v-slot:head="scope">
        {{ scope.user.firstName }}
      </template>
    </base-layout>
    <router-view v-slot="{ Component }">/* 只绑定作用域,不绑定插槽名 */
      <component :is="Component" />
    </router-view>
    <router-view v-slot="{ Component, route }">/* 只绑定作用域,不绑定插槽名 */
      <transition name="fade">
        <component :is="Component" :key="route.path" />
      </transition>
    </router-view>
  (2)定义组件,并渲染插槽,两个版本都一样
    <div>
      <slot name="head" :user="obj">
        {{ user.text }}<!-- 后备内容 -->
      </slot>
    </div>
3、插槽实例:(“传值传参传数据”,可演示)
  //调用组件时,不直接向插槽传数据,经组件向插槽传数据
  //使用插槽后,如插槽为空,则使用后备内容,是改造后的传数据
  <!DOCTYPE html>
  <html>
  <head>
    <meta charset="UTF-8">
    <title></title>
    <script src="https://cdn.bootcdn.net/ajax/libs/vue/2.6.9/vue.common.dev.js"></script>
  </head>
  <style type="text/css">
    .slot{
      width:200px;
      height:50px;
      line-height:50px;
      text-align: center;
      border:1px solid #ccc;
      border-radius: 12px;
      margin: 10px auto;
    }
  </style>
  <body>
    <div id="app">
      <!-- 
        1、以下2种插槽写法等效
          <template v-slot:head>
          <template #header> 
        2、以下2种插槽写法等效
          <template v-slot:head="scope">
          <template #header="scope">
        3、传数据路径:组件标签->组件定义->插槽标签->插槽定义,组件标签和插槽定义写在一起,但不直接传数据
      -->
      <this-div :list="list">
        <div class="transclude">默认插槽</div>
        <!-- 调用组件时,用template标签定义插槽,指定插槽名和作用域名,使用值 -->
        <template v-slot:head="scope">
          <div v-if="scope.innerItem.id == 2" v-text="scope.innerItem.name"></div>
        </template>
      </this-div>
    </div>
  </body>
  </html>
  <script type="text/javascript">
    // 搜索“与插槽相关的注释”
    // 在模板字符串中,用`<div> ${ /* 注释内容 */'' } </div>`加注释,常用在html中
    // 在模板字符串中,用`字符串 ${变量}`把“字符串和变量拼接起来”,常用在js中
    Vue.component('this-div', {
      props: ['list'],
      template: `<div>
              <slot/>
              <div> 
                <div :key='item.id' v-for='item in list' class="transclude">
                  ${ /* 定义组件时,用slot标签调用插槽,指定插槽名和作用域(的属性)名,传数据 */'' }
                  ${ /* 给slot“标签的作用域”绑定“自定义属性”(innerItem) */'' }
                  ${ /* 插槽head多次在组件的<slot name='head' v-bind:innerItem='item'>处渲染 */'' }
                  <slot name='head' :innerItem='item'>后备内容{{item.name}}</slot>
                </div>
              </div>
            </div>`,
    });
    var vm = new Vue({
      el: '#app',
      data: {
        list: [{
          id: 1,
          name: 'apple'
        }, {
          id: 2,
          name: 'banane'
        }, {
          id: 3,
          name: 'orange'
        }]
      }
    });
  </script>

七、Vue2源码(Observer、Dep和Watcher)
1、自动执行混入
  (1)执行initMixin(Vue);stateMixin(Vue);eventsMixin(Vue);lifecycleMixin(Vue);renderMixin(Vue);
  (2)其中执行initMixin(Vue),产生Vue.prototype._init
  (3)使用情形,多个组件有相同选项
  (4)混入对象可以包含组件任意选项,示例
    定义
    export default {
      watch: {
        $route(route) {}
      },
      beforeMount() {
        window.addEventListener('resize', this.$_resizeHandler)
      },
      beforeDestroy() {
        window.removeEventListener('resize', this.$_resizeHandler)
      },
      mounted() {},
      methods: {}
    }
    使用
    import ResizeMixin from './mixin/ResizeHandler'
    export default {
      name: 'Layout',
      components: {},
      mixins: [ResizeMixin],
      computed: {},
      methods: {}
    }
2、手动执行类
  (1)执行new Vue(options);执行this._init(options);执行initState(vm);initLifecycle(vm);initEvents(vm);initRender(vm);callHook(vm,"beforeCreate");initInjections(vm);initProvide(vm);callHook(vm,"created");
  (2)其中执行initState(vm);执行initProps();initData();initComputed();initMethods();initWatch();vm.$mount(vm.$options.el)
3、Vue响应式(Observer、Dep和Watcher)
  (1)initProps(vm,opts.props)用defineReactive$$1将属性定义为响应式;
  (2)initData(vm)用observe将data定义为响应式,vue的data和Observer实例互相绑定;
  (3)initComputed(vm,opts.computed)执行new Watcher,vue实例和Watcher实例互相绑定,不执行this.get函数,用vm._computedWatchers[key]存储Watcher实例,用defineComputed定义计算属性为只读(get)响应式,页面渲染时才会触发get,此时所用的值都已确定;
  (4)initWatch或mountComponent执行,new Watcher执行,vue实例和Watcher实例互相绑定,执行this.get函数,给Dep.target赋值,执行this.getter函数,触发响应式的get函数,获取value的旧值,通过dep.depend把watcher实例存放到dep.subs里;
  (5)获取数据,触发响应式的get函数,通过dep.depend向dep.subs里注入watcher实例的cb函数;
  (6)改变数据,触发响应式的set函数,通过dep.notify执行dep.subs里watcher实例的cb函数;
  (7)在computed中,A、函数名不能和data中的属性名一致,B、对data的属性进行赋值操作,会再次触发computed函数,形成死循环
  (8)在watch中,A、函数名必须和data中的属性名一致,B、对data的属性进行赋值操作,会触发computed函数,不会形成死循环,C、属性值发生改变的时候,watch中的函数执行
  (9)Observer:数据的观察者,让数据对象的读写操作都处于自己的监管之下
  (10)页面初次渲染时,用初始值和由初始值计算而来的计算属性值渲染页面;更新初始值时,执行watch监听函数,用更新值和由更新值计算而来的新计算属性值渲染页面。
4、如何实现数据双向绑定
  来源,https://mp.weixin.qq.com/s?__biz=MjM5MDA2MTI1MA==&mid=2649091937&idx=1&sn=1d08ebe716e00555e18aa34896ffa7a7&chksm=be5bc8cc892c41daaefa76d5747d5e1eccd2934c3c178e70e16702d729aa2e40409621ffa137&scene=27
  (1)定义,数据变化更新视图,视图变化更新数据
  (2)实现
    A、实现一个监听器 Observer:对数据对象进行遍历,包括子属性对象的属性,利用 Object.defineProperty() 对属性都加上 setter 和 getter。这样的话,给这个对象的某个值赋值,就会触发 setter,那么就能监听到了数据变化。
    B、实现一个解析器 Compile:解析 Vue 模板指令,将模板中的变量都替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,调用更新函数进行数据更新。
    C、实现一个订阅者 Watcher:Watcher 订阅者是 Observer 和 Compile 之间通信的桥梁 ,主要的任务是订阅 Observer 中的属性值变化的消息,当收到属性值变化的消息时,触发解析器 Compile 中对应的更新函数。
    D、实现一个订阅器 Dep:订阅器采用 发布-订阅 设计模式,用来收集订阅者 Watcher,对监听器 Observer 和 订阅者 Watcher 进行统一管理。
5、如何实现对象和数组的监听
  (1)Object.defineProperty()只能对属性进行数据劫持,不能对整个对象进行劫持
  (2)递归遍历对象和数组,逐层进行数据劫持
    observeArray (items: Array<any>) {
      for (let i = 0, l = items.length; i < l; i++) {
        observe(items[i])  // observe 功能为监测数据的变化
      }
    }
    let childOb = !shallow && observe(val) // observe 功能为监测数据的变化
6、给数组方法绑定响应式
  var methodsToPatch = ["push","pop","shift","unshift","splice","sort","reverse"];
  var arrayProto = Array.prototype;
  var arrayMethods = Object.create(arrayProto);
  {}.__proto__ = arrayMethods.__proto__ = arrayProto;
  methodsToPatch.forEach(function (method) {
    def(arrayMethods, method, function mutator() {});
  });
  ['a','b','c'].__proto__ = arrayMethods;
  function def(obj, key, val, enumerable) {
    Object.defineProperty(obj, key, {
      configurable: true,
      enumerable: !!enumerable,
      value: val,
      writable: true,
    });
  }
  Object.defineProperty(myObj, "key", {
    configurable: false,
    enumerable: false,
    get: function () {
      console.log(this);
      return key+2;
    },
    set: function (value) {
      console.log(this);
      key = value + 1;
    },
  });

八、vue2和vue3的不同(区别)
  附、Vue,Vue-cli,Vue-router,Vuex 版本对应关系
  来源,https://blog.51cto.com/knifeedge/5616852
  (1)Vue2.x
    Vue-cli: 3.x、4.x.
    Vue-router: 3.x
    Vuex: 3.x
  (2)Vue3.x
    Vue-cli: 4.x 
    Vue-router: 4.x
    Vuex: 4.x
1、实例化不同
  (1)vue2下,用new获取和挂载vue实例
    import Vue from 'vue';
    import router from './router';
    import store from './store';
    new Vue({
      el: '#app',
      router,
      data,
      store,
      render: createElement => createElement(App)//render执行,即createElement执行
    })
    //附、全局方法添加与调用
    //以下添加
      Vue.prototype.$myGlobalMethod = function () {
        console.log(11111);
      };
      new Vue({})
    //以下调用
      export default {
        mounted() {
          this.$myGlobalMethod();
        }
      }
  (2)vue3下,用createApp、mount,获取、挂载vue实例
    import { createApp } from 'vue'
    import App from './App.vue'
    import router from './router'
    import store from './store'
    import ElementPlus from 'element-plus'
    createApp(App).use(store).use(router).use(ElementPlus).mount('#app')
    //附、全局方法添加与调用
    //以下添加  
      const app = createApp(App)
      app.config.globalProperties.aaa = function(){
        console.log( 22222 );
      }
      app.mount('#app')
    //以下调用
      <script setup>
        const { proxy } = getCurrentInstance()
        proxy.aaa()
2、双向绑定原理不同
  (1)vue2用ES5的一个Object.defineProperty对数据进行劫持,结合发布订阅模式的方式来实现的
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get: function reactiveGetter() {
        return value;
      },
      set: function reactiveSetter(newVal) {
        dep.notify();
      },
    });
  (2)vue3用ES6的Proxy对数据进行代理
    Proxy 的优势:
     A、可以直接监听对象、数组的变化
     B、有多达13种拦截方法
     C、返回的是一个新对象供操作
    var obj = {};
    var thisObj = new Proxy(obj, {
      get: function (target, key, receiver) {
        console.log(obj === target); //true
        if (key === 4) {
          return 5;
        }
        return 6;
      },
    });
    thisObj.count = 1;
    console.log(thisObj.count);
    console.log("count" in thisObj);    
3、生命周期不同(创建、加载、更新、卸载), <keep-alive> 激活和失活,百度-vue生命周期带不带on
  (1)对比
    A、vue2:beforeCreate--、created--、beforeMount--、mounted--、beforeUpdate--、updated--、
        beforeDestroy--、destroyed--、activated--、deactivated--、errorCaptured
        //ssr不支持beforeMount、mounted
    B、vue3:onbeforeCreate、oncreated、onBeforeMount、onMounted、onBeforeUpdate、onUpdated、
        onBeforeUnmount、onUnmounted、onActivated、onDeactivated、onErrorCaptured 
        //vue3.0.0,2020年01月04日,预发布
  (2)vue3,2020年07月08日,Vue3.0.0-beta.20之前,setup(props, context)函数作为组件的一个配置项
    A、与“vue2的写法”共存
    B、setup里面的vue3配置(如ref、reactive)覆盖外面的vue2配置(data),见“nameIcon:”
    C、setup里面的this指向undefined 
  (3)vue3,2020年07月14日,Vue3.0.0-beta.21之后,增加<script setup>实验特性
    A、与“vue2的写法”二选一
  (4)vue3,2021年08月09日,Vue3.2.0之后,正式使用<script setup>
    A、与“vue2的写法”二选一
  (5)生命周期使用说明
    A、初始化
      a、beforeCreate,实例初始化后、watcher生成前调用,data和methods未初始化
      b、created,实例创建后调用,data等配置完成
      c、beforeMount/onBeforeMount,实例挂载前调用,编译模板,调用render,生成vDom即虚拟DOM
      d、mounted/onMounted,实例挂载后调用,可以访问操作DOM
    B、更新    
      a、beforeUpdate/onBeforeUpdate,数据-改变-后调用,
      b、updated/onUpdated,虚拟DOM-重新渲染-后调用,==修改data数据,会造成死循环==
    C、销毁
      a、beforeDestroy/onBeforeUnmount,实例销毁-前调用
      b、destroyed/onUnmounted,实例销毁-后调用
    D、请求
      a、created、beforeMount、mounted,可以在里调用异步请求,进入更新生命周期
  (6)一个完整的父子组件的生命周期:
     来源:https://blog.csdn.net/leilei__66/article/details/118699960
    A、加载
      a、父beforeCreate -> 父created -> 父beforeMount -> 
      b、子beforeCreate -> 子created -> 子beforeMount -> 子mounted -> 
      c、父mounted
    B、更新:父beforeUpdate -> 子beforeUpdate -> 子updated -> 父updated 
    C、卸载:父beforeDestroy -> 子beforeDestroy -> 子destroyed -> 父destroyed
  (7)父组件监听子组件的生命周期的方案
    A、方案1
      //Parent.vue
        <Child @mounted="doSomething"/>
      //Child.vue
        mounted() {
          this.$emit("mounted");
        }
    B、方案2
      //Parent.vue
        <Child @hook:mounted="doSomething" ></Child>
        doSomething() {
          console.log('父组件监听到 mounted 钩子函数 ...');
        },
      //Child.vue
        mounted(){
          console.log('子组件触发 mounted 钩子函数 ...');
        },
4、有无瞬移组件teleport不同
  (1)vue2没有teleport
  (2)vue3有teleport,瞬移组件到指定的dom中,相当于react中的ReactDOM.createPortal
    A、代码1
      <body>
        <div id="app"></div>
        <div id="modal"></div>
      </body>
    B、代码2
      <template>
        <teleport to="#modal">
          <div v-if="visible" class="v3-modal">
            <h2 class="v3-modal-title">{{ title }}</h2>
            <div class="v3-modal-content">
              <slot>This is a modal</slot>
            </div>
            <button @click="handleClose">close</button>
          </div>
        </teleport>
      </template>
5、是否支持多根节点不同
  (1)vue2不支持了多根节点的组件
    <!-- Layout.vue -->
    <template>
      <div>
        <header>...</header>
        <main>...</main>
        <footer>...</footer>
      </div>
    </template>
  (2)vue3支持多根节点的组件
    <!-- Layout.vue -->
    <template>
      <header>...</header>
      <main v-bind="$attrs">...</main>
      <footer>...</footer>
    </template>
6、组件结构不同,按选项或按功能
  (1)vue2,按选项书写组件
  (2)vue3,按功能书写组件(用组合式API)
  (3)vue2示例
    <script>
      import comTab from '@/components/ComTab/com-tab'//引入组件
      export default {
        name: 'ComTpl',//组件名
        components: {//注册组件   
          comTab,
        },
        props: {//用于接收父组件向子组件传递的数据
          test: {
            type: Object
          }
        },
        data () {
          return {
            username: '',
            password: ''
          }
        },
        methods: {
          handleClick () {
            this.$router.push('/about')
          }
        },
        computed:{
          fullName(){
            return this.firstName+"."+this.lastName;     
          }
        }
      };
    </script>
附、vue3的setup,
  来源,https://blog.csdn.net/qq_41581588/article/details/128869363
  来源,https://www.javascriptc.com/vue3js/api/refs-api.html#ref
  来源,https://github.com/vuejs/core/blob/main/CHANGELOG.md
  来源,https://blog.csdn.net/ZYS10000/article/details/124535467
  附、2020年01月04日,vue3.0.0,预发布
  附、2020年07月08日,Vue3.0.0-beta.20之前,setup(props, context)函数作为组件的一个配置项
  附、2020年07月14日,Vue3.0.0-beta.21之后,增加了<script setup>的实验特性
  附、2021年08月09日,Vue3.2.0之后,<script setup>正式使用
  (1)“setup”函数与属性的共同特点,都有一些方法
    A、子组件,引入后无需注册
    B、组合式api,把相关内容写在一起
    C、ref(定义基本数据类型的响应式)  
      var tableData = ref([]);//tableData.value可以重新赋值
      const tableData = ref([]);//tableData.value可以重新赋值
    D、reactive(定义引用数据类型的响应式)
      var tableData = reactive([]);//tableData可以重新赋值
      const tableData = reactive([]);//tableData不可以重新赋值
    E、toRef,为响应式对象的某一属性创建响应式引用,后来用atrr代替obj.atrr
    F、toRefs,为响应式对象的所有属性创建响应式引用,后来用atrr代替obj.atrr,
      应用场景有,表单验证、导航栏组件切换、数据可视化组件切换(图表、表格)、展开响应式对象
    G、storeToRefs,为pinia实例的属性创建引用
    H、createStore,创建vuex实例,实现状态管理
    I、useRouter,获取路由实例;useRoute,获取当前路由对象
    J、watchEffect,立即监听引用数据类型的所有属性,组件卸载,则停止监听
    K、组件的(基本数据、对象数据、单个对象属性、所有对象属性)响应式生成,后两个后来用atrr代替obj.atrr
  (2)“setup”函数与属性的区别,接收属性和定义事件,vue2的配置和生命周期
    A、setup函数用props、context,接收属性、定义事件,不排斥vue2的配置和生命周期,如
      a、props
        const { title } = toRefs(props);
        const title = toRef(props, 'title');
      b、context(attrs、slots、emit、expose)
        context.attrs,获取透传过来的值,props配置中没有声明的属性或v-on没有监听的事件
        //传递给组件,却没有被该组件声明为props或emits的attribute或者v-on事件监听器
        //最常见的例子就是class、style和id
        context.slots,插槽
        //父组件调用了本组件,并添加了插槽内容
        context.emit,子组件给父组件传值
        context.expose,子组件暴露给父组件可以调用的属性和方法
      c、setup(代替vue2的data、computed、methods、watch,没有实例,this指向undefined,创建实例,onbeforeCreate、oncreated没必要存在)
      d、onBeforeMount、onMounted、onBeforeUpdate、onUpdated、onBeforeUnmount、onUnmounted、onActivated、onDeactivated、onErrorCaptured 
      e、return,数据才能供外面使用,返回对象中的数据和data中的数据同名时,setup优先级更高 
    B、setup属性用defineProps、defineEmits、defineExpose,接收属性、定义事件,排斥vue2的配置和生命周期,如
      注、不能有import { defineProps, defineEmits, defineExpose } from "vue",
      a、defineProps,定义属性。不解构时,在js中打点获取,在html中直接获取;解构后,都直接获取,
        var const props = defineProps({
          dataA:{
            type: String,
            default: 'state',
            validator: (value) => {//自定义验证函数
              return ['success', 'warning', 'danger', 'info'].includes(value)
            }
          },
          dataB:{
            type: [Array, Object],
            required: true, //是否必须传递该prop
            default: () => {
              return { name: 'jack', age: 20 }
            }
          }
        })
      b、defineEmits,定义发射器
        var emit = defineEmits(['customChange'])
        var goCourse = function() {
          emit('customChange')
        }
      c、defineExpose,子组件把属性暴露给父组件,父组件通过子组件的ref获取
        defineExpose({
          counter,
          incrementCounter
        }); 
        //以下父组件,this.$refs.myDiv
        <template>
          <div ref="myDiv">Hello, Vue 3!</div>
          <button @click="handleClick">Click me</button>
        </template>
        <script setup>
          import { ref, onMounted } from 'vue';
          export default {
            const myDiv = ref(null);
            onMounted(() => {
              console.log(myDiv.value);//输出div DOM元素
            });
          }
        </script>
  (3)setup函数的用法示例
    A、不用defineComponent,传入的属性没法验证
      <template>
        <div>
          <h1>子组件</h1>
          <button @click="sendData">发送数据</button>
          <slot name="header"></slot>
          <slot></slot>
          <slot name="footer"></slot>
          <span>{{props.name}}</span>
        </div>
      </template>
      <script>
        import { toRef, toRefs, reactive, watchEffect, computed, watch, onMounted } from 'vue';
        import vChild from "../components/child.vue" 
        export default {
          setup(props, context){
            console.log(props)
            console.log(context.attrs)
            console.log(context.slots)
            const sendData = () => { 
              context.emit('my-event', 1000)
            }
            const aaa = ref(1)
            const fn = () => {
              aaa.value = 100
            }
            context.expose({ //暴露出去的是对象
              aaa, fn
            })
            // 以上关于context
            var age = ref(0)
            var firstName = ref('hello')
            var lastName = ref('world')
            var obj = reactive({ name:'zs' });
            var titleRef = toRef(props, 'title')
            var { title } = toRefs(props)
            var handleChange = (event) => {
              context.emit("customChange", event.target.value)
            }
            var fullName = computed(() => {
              return firstName.value + '-·-' + lastName.value
            })
            watch( //watch示例
              age, 
              (newValue, oldValue) => {
                console.log(newValue, oldValue)
              }
            )
            watchEffect(() => {
              console.log('name:',obj.name)
            })
            onMounted(()=>{
              console.log('1.DOM渲染后',document.querySelector('.main')) //标签
            })
            return {
              obj, 
              handleChange
            }
          },
          beforeCreate() {
            console.log('beforeCreate执行了')
          }
        }
      </script>
    B、用defineComponent,传入的属性可以验证,帮助TypeScript识别组件选项中的类型
      import {ref, reactive, defineComponent, Ref, onMounted} from "vue";
      export default defineComponent({
        name: "LogList",
        components: {
          FilterCom,
          TableCom
        },
        props: {
          msg: {
            type: String,
            required: true
          },
          ary: {
            type: Array,
            default: function(){
              return []
            }
          },
          obj: {
            type: Object,
            default: function(){
              return {}
            }
          }
        },
        setup() {
          const str = ref('qwert')
          const list = ref([])
          const obj = reactive({})
          onMounted(() => {
            findLogList();
          });
          return {
            str,
            list,
            ...toRefs(obj)
          }
        }
      });
  (4)setup属性的用法示例
    <template>
      <span>{{props.name}}</span>
    </template>
    <script setup>
      import { ref, reactive, watch, watchEffect, computed, onMounted, toRefs } from 'vue'
      import vChild from "../components/child.vue"
      var props = defineProps({
        state:{
          type: String,
          default: 'state',
          required: true, //是否必须传递该prop
          validator: function(){} //自定义验证函数
        }
      })
      var sum = ref(5);
      var count = ref(0);
      var obj = reactive({name: '张三', age: 88});
      var emit = defineEmits(['customChange'])
      var goCourse = function() {
        emit('customChange')
      }
      var person = reactive({
        name: 'Echo',
        age: 26,
      })
      toRefs(person)
      //以下watch示例
      watch( sum, //1/2、监听整体,不管sum是基本数据类型,还是引用数据类型,监听的都不是sum.value
        function( newValue, oldValue ){
          console.log('sum或msg变了', newValue, oldValue)
        },{
          immediate: true
        }
      )
      watch(props, newVal =>{ //2/2、监听整体
        adoptLabel.value = newVal.adoptTxt
      })
      watch(props.message, newVal =>{ //1/3、监听部分,props对象本身被重新赋值时,就算props.message的值发生变化,watch也不会触发
        adoptLabel.value = newVal
      })
      watch(() => props.message, //2/3、监听部分,只要props.message的值发生变化,watch就会触发
        (newValue, oldValue) => {
          console.log(newValue, oldValue);
        }
      );
      watch(
        () => obj.name, //3/3、监听部分
        (newValue, oldValue) => {
          console.log(oldValue,newValue);
        },
        { immediate: true, deep: true }
      );
      watchEffect(() => {
        console.log(`计数是: ${count.value}`);
      }, {
        flush: 'post',//用于控制副作用函数的刷新时机
        //'pre':在组件更新之前执行副作用函数。这是默认值,它可以确保副作用函数获取到的是更新前的数据,并且在更新过程中不会出现数据不一致的情况。
        //'post':在组件更新之后执行副作用函数。这种模式可以用于需要获取更新后的数据的场景,例如在更新 DOM 之后进行一些基于 DOM 状态的操作。
        //'sync':副作用函数会在响应式数据发生变化后立即同步执行。
        onTrack: (event) => {
          console.log('追踪到响应式数据:', event.target);
        },
        onTrigger: (event) => {
          console.log('响应式数据触发了副作用函数:', event.target);
        }
      });
      watchEffect((onCleanup) => { // onCleanup,用来清除副作用
        let timer;
        timer = setTimeout(() => {
          console.log(`计数变为了${count.value}`);
        }, 1000);
        onCleanup(() => {
          clearTimeout(timer);
        });
      });
      onMounted( function() {
        console.log("组件加载完成,引用getData获取原始数据");
      }); 
    </script> 

九、vuex,跨组件、跨页面共享数据时用
  附、中文文档,https://vuex.vuejs.org/zh/
  附、子组件改变父组件数据的方式,与单向数据流无关
    A、子组件发射事件,父组件监听事件,改变自身的值
    B、子组件给父组件传来的函数传参并执行
    C、子组件给父组件传来的对象的某个属性赋值
  附、单向数据流:js改变数据源(本地存储vuex),数据源改变页面,不是js直接改变页面
  附、redux与Vuex的相似之处
    A、redux:处理同步用 dispatch action(对象类型);处理异步用 dispatch action(函数类型)
    B、Vuex:处理同步用 commit(mutations) ;处理异步用 dispatch(action),在action里执行 commit(mutations)
1、vuex3下(对应vue2下),
  (1)定义,
    A、模块定义//store/app.js
      import Cookies from 'js-cookie'
      import { makeRequest } from '@/prototype/httpRequest.js'
      const app = {
        namespaced: true, //为了解决不同模块命名冲突的问题
        state: {
          devTime: '',
        },
        mutations: {
          TIME: (state, devTime) => {
            state.devTime = devTime
          },
        },
        getters: {
          getTodoById: (state) => (id) => {
            return state.todos.find(todo => todo.id === id)
          }
        },
        // getters,是提取出来的computed属性,无法向它传参,可以让getter返回一个函数以接收参数,示例
        // store.getters.getTodoById(2) // -> { id: 2, text: '...', done: false }
        actions: {
          ToggleDevice({
            commit
          }, device) {
            commit('TOGGLE_DEVICE', device)
          }
        },
      }
      export default app 
    B、模块合并//store/index.js 
      //允许我们将 store 分割成 module,每个模块拥有自己的 state、mutation、action、getter、module
      import Vuex from 'vuex';
      import app from 'store/app.js'
      import getters from 'store/getters.js'
      Vue.use(Vuex)
      const store = new Vuex.Store({
        modules: {
          app,
          user,
          permission
        },
        getters
      })
      export default store 
  (2)注入,
    import store from 'store/index.js'
    new Vue({
      el: '#root',
      store,
    })
  (3)获取和使用
    A、在.vue组件中使用
      this.$store.commit('app/setUserInfo',userInfo);
    B、在.js中使用
      import store from '../store'; 
      store.dispatch('FedLogOut').then(() => {}) 
  (4)辅助函数
    import { mapState, mapGetters, mapMutations, mapActions } from 'vuex'
    export default {
      computed: {
        ...mapState(['count']),
        ...mapGetters(['doubleCount']),
      },
      methods: {
        ...mapMutations(['increment']),
        ...mapActions(['incrementAsync']),
      }
    }
  (5)示例
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>vuex的使用</title>
      <script src="https://cdn.bootcdn.net/ajax/libs/vuex/3.0.0/vuex.js"></script>
      <script src="https://cdn.bootcss.com/vue/2.5.16/vue.js"></script>
      <style>
        span{
          display:inline-block;
          width:200px;
        }
        div{
          margin: 20px;
        }
      </style>
    </head>
    <body>
      <div id="root">
        <my-child></my-child>
      </div>
    </body>
    </html>
    <script>
      Vue.component('my-child', {
        template: 
          `<div>
            <div><span>dataA:{{dataA}}</span><button @click='clickButton()'>普通函数改变dataA</button></div>
            <div><span>stateA:{{stateA}}</span><button @click='mutationsA(100)'>mutations改变stateA</button></div>
            <div><span>stateB:{{stateB}}</span><button @click='actionsB(2)'>actions改变stateB</button></div>
          </div>`,
        props: {},
        data: function () {
          return {
            dataA: 1,
          }
        },
        computed: {
          ...Vuex.mapState(['stateA','stateB']),
          //...Vuex.mapState({stateD:'stateC'}),
          //...Vuex.mapGetters(['getterC']),
        },
        methods: {
          ...Vuex.mapMutations([
            'mutationsA',
          ]),
          ...Vuex.mapActions([
            'actionsB'
          ]),
          clickButton() {
            this.dataA += 1;
          },
        },
      })
      const store = new Vuex.Store({
        state: {
          stateA: 100,
          stateB: 1,
        },
        getters: {
          getterC: (state, getters) => state.stateC//返回值可以为函数
        },
        mutations: {
          mutationsA: (state, num) => {
            state.stateA += num
          },
          mutationsB: (state, num) => {
            state.stateB = state.stateB*num
          }
        },
        actions: {
          actionsB: ({
            commit
          },num) => {
            setTimeout(function () {
              commit('mutationsB', num)
            }, 1000)
          }
        },
        modules: { }
      })
      new Vue({
        el: '#root',
        store,
      })
    </script>  
2、vuex4(对应vue3),
  (1)定义
    import { createStore } from 'vuex'
    import app from 'store/app.js'
    import getters from 'store/getters.js'
    export default createStore({
      modules: {
        app,
        user,
        permission
      },
      getters
    })
  (2)注入实例//main.js
    import { createApp } from 'vue';
    import App from './App.vue';
    import store from './store';
    const app = createApp(App);
    app.use(store);
    app.mount('#app'); 
  (3)获取和使用  
    <template>
      <div>
        <h2>{{ store.state.count }}</h2>
        <button @click="addCount">点击</button>
      </div>
    </template>
    import { computed, useStore } from "vuex";
    setup() {
      var store = useStore();
      var count = computed(() => store.state.count)
      var addCount = function() {
        console.log(store.state.count);
        store.commit("addCount");
        store.dispatch("addCount");
        store.commit('increment');
        store.dispatch('incrementAsync')
      };
      return { addCount };
    }
3、vuex的替代--pinia
  来源,https://pinia.web3doc.top/introduction.html
  附、简介
    A、2019年11月,pinia/piːnjʌ/出现
    B、同步和异步都用actions,没有mutations,没有modules嵌套结构,没有命名空间
    C、优势,热模块替换、支持服务端渲染
  (1)主要方法
    A、createPinia,执行结果配置到vue实例里
    B、defineStore,定义状态管理
      a、参数
        第1个参数,是一个唯一的字符串标识符,用于区分不同的store
        第2个参数,是一个配置对象,用于定义store的状态、actions和getters等
      b、返回值,可以接受store为参数
    C、storeToRefs,为pinia实例的属性创建引用
  (2)定义
    A、总状态'./store'
      const store = createPinia()
      export default store
    B、分状态-不接收store参数
      //storeA.js
      import { defineStore } from 'pinia'
      const useStoreA = defineStore('storeA', {
        state: () => ({
          count: 0,
        }),
        actions: {
          increment() {
            this.count++;
          },
        },
      });
    C、分状态-接收store参数
      //storeB.js
      import { defineStore } from 'pinia'
      const useStoreB = defineStore('storeB', {
        state: () => ({
          value: 10,
        }),
        actions: {
          someAction(store) {
            // 可以访问 storeA 的状态和方法
            if (store.count > 5) {
              this.value++;
            }
          },
        },
      });
  (3)注入
    A、vue2,
      import store from './store'
      new Vue({
        el: '#app',
        store,
      })
    B、vue3, 
      import { createApp } from 'vue'
      import App from './App.vue'
      import store from './store'
      const app = createApp(App)
      app.use(store)
  (4)获取和使用
    <template>
      <div>
        Count: {{ count }}
      </div>
    </template>
    <script setup>
      import store from './store'
      import useStoreA from '@/store/modules/storeA';
      import useStoreB from '@/store/modules/storeB';
      import { storeToRefs } from 'pinia';
      const storeA = useStoreA();
      const storeB = useStoreB(store);//defineStore返回值useStoreB可以接受store为参数
      const { count } = storeToRefs(storeA);//为状态创建引用
      console.log( storeA.count );
      console.log( storeA.increment );
      console.log( storeB.value );
      console.log( storeB.someAction );
    </script>

十、vue-router(含权限)
  来源,https://ezdoc.cn/docs/vue-router/advanced/navigation-failures
  附1、url传参对应router中的query配置
    window.open('#/create?domain=government&id=' + row.id)
    http://test.cctv.com/#/create?domain=government&id=666
1、VueRouter实例
  (1)注入实例//main.js
    import router from './router'//生成实例
    import '@/permission'//扩充实例
    new Vue({
      el: '#app',
      data: {},
      router,//混入实例
      store,
      render: h => h(App)
    })
  (2)生成实例//router.js
    import VueRouter from 'vue-router';
    Vue.use(VueRouter);
    var constantRoutes = [
      { 
        path: '/', 
        name: 'HomePage' 
        component: HomePage, 
      },
      { 
        path: '/login',
        name: 'LoginPage',  
        component: LoginPage, 
      },
    ];
    const router = new VueRouter({
      mode: 'abstract',
      // A、hash模式,最常用
      //   a、依赖于浏览器提供的 hashchange 事件。当 URL 的 hash 值发生变化时,浏览器会触发这个事件
      //   b、Vue-router在初始化的时候会监听这个事件,然后根据当前的hash值去匹配对应的路由规则,并通过这种方式来改变视图
      // B、history模式,刷新浏览器时会出错,解决这个问题需要后台配合
      //   a、通过操作HTML5 History API来实现的,允许我们去创建一个更像原生应用的体验,没有hash(#)在URL中
      //   b、Vue-Router通过history.pushState来改变浏览器的地址栏而不重新加载页面
      //   c、当用户点击浏览器的前进后退按钮时,或者使用浏览器的地址栏进行导航时,可以监听popstate事件来处理路由的变化
      //     window.onpopstate = function(event) { //这是异步函数
      //       console.log( event.state, history.state, event.state == history.state, event, ); // true
      //     };
      // C、abstract模式,抽象。为了在服务端渲染(SSR)而设计的。
      //   a、返回一个用于服务端的虚拟的 router-view 组件
      //   b、服务端可以获取到对应路由的 html 内容,然后将其嵌入到最终的 HTML 中,从而实现服务端渲染
      routes: constantRoutes,
      scrollBehavior: function (to,from,savedPosition){//使用前端路由,切换到新页面时,滚到顶部还是保持上次的位置
        // `to`和`from`都是路由地址
        // `savedPosition`可以为空,如果没有的话
        return {x: 0, y: 0};//期望滚动到的位置
      }
    });
    export default router//导出实例
    附、路由列表(权限列表)
      a、初始路由列表:包含白名单(公共页面)
      b、最终路由列表:权限清单并入初始路由列表
  (3)扩充实例-添加权限//permission.js
    A、在总登录里添加权限
      login(data).then(function(){
        if(store.permissionList.length === 0){
          store.getInfo(data)
        }
      })
    B、在总拦截里添加权限
      import router from './router'
      import useUserStore from '@/store/modules/user'
      router.beforeEach(async(to, from, next) => {
        // to,即将要进入的目标 路由对象
        // from,当前导航正要离开的路由
        // next,一定要调用该方法来resolve这个钩子,执行效果依赖next方法的调用参数
        NProgress.start();
        var isLogin = getLoginState();//从localStorage获取
        var whiteList = ['/login']; 
        /* 路由发生变化修改页面 title */
        if (to.meta.title) {
          document.title = to.meta.title //给title赋值
        }
        if(isLogin){// 已登录
          if(store.permissionList.length === 0){
            // 路过这里的情形:刷新浏览器,权限列表为空
            await store.getInfo();//获取权限清单
          }
          if(store.permissionList.indexOf(to.path) > -1 || whiteList.indexOf(to.path) > -1){
            // 判断权限-权限来自后台,如果在权限列表或白名单里
            next(from);
          }else{
            // 路过这里的情形:在浏览器输入路由,不在权限列表,不在白名单(公共页面)
            // 提示:没有权限,返回from
            next();
          }
        } else {// 未登录
          if (whiteList.indexOf(to.path) > -1) {
            next();
          } else {
            next('/login');//next(`/login?redirect=${to.path}`)
          }
        }
        NProgress.done();
      });
    附、权限相关
      a、判断权限-权限来自前端
        router.beforeEach((to, from, next) => {
          if (to.meta.permission.indexOf(roleStr)>-1) {
            next()
          } else {
            next({ path: '/login' })
          }
        })
      b、绕过权限
        var num = 0; 
        //objAry为前端模拟的后台权限列表
        router.beforeEach((to, from, next) => {
          num++;//1,4、自增
          if (whiteList.indexOf(to.path) !== -1) {
            next()//2,5、跳到目标页
            if(num < 2){//6、拦截
              store.commit('user/SET_MENU',objAry)//3、更改数据,刷新页面
            }
          }
        }) 
    附、登录相关
      a、在总拦截里跳往登录
        if(isLogin){
          // 已登录
        } else {
          if (whiteList.indexOf(to.path) > -1) {
            next();
          } else {
            next('/login');//next(`/login?redirect=${to.path}`)
          }
        }
      b、在总请求里跳往登录
        httpRequest.interceptors.response.use(function (response) { 
          if(response.code === 407){
            // 路过这里的情形:长时间没有与后台交互,向后台请求数据,后台要求重新登录
            router.push({ path: `/login?redirect=${to.fullPath}` })
          }
        });
  (4)添加权限的方法//user.js
    A、匹配说明,
      a、渲染祖级到path的各层级组件,path自身对层级渲染“没有”影响,path位置对层级渲染“有”影响
      b、路由配置既有path又有redirect,当访问path指定的路径时,自动使用redirect,渲染祖级到redirect的各层级组件
    B、方法  
      import { getUserInfo, logout } from '@/api/user'
      import { getToken } from '@/utils/auth'
      import router from '@/router'
      import { removeArrByObj } from "@/utils"
      import { defineStore } from 'pinia'
      import Layout from '@/layout'
      const modules = import.meta.glob('../../view/*/*.vue');
      function initRouterNode(routers, data) {
        for (var item of data) {
          let menu = Object.assign({}, item)
          menu.component = menu.component.indexOf('main') > -1 ? Layout : modules[`../../view/${menu.component}.vue`] 
          if (item.children && item.children.length > 0) {
            menu.children = []
            initRouterNode(menu.children, item.children)
          }
          let meta = {}
          meta.title = menu.name ? menu.name : null//给页面添加权限、标题、第三方网页链接
          meta.hideInMenu = !!menu.hideInMenu
          meta.icon = item.icon
          menu.meta = meta
          menu.redirect = item.url?item.url:null
          routers.push(menu)
        }
      }
      function SET_MENU(state,menus){
        let allMenus = menus
        let menu = JSON.parse(JSON.stringify(allMenus))
        removeArrByObj(menu,'','children','component')
        let menuList =[]
        initRouterNode(menuList,menu)
        state.menus = menuList
        state.allMenus = allMenus
        for(let i=0;i<menuList.length;i++){
          router.addRoute(menuList[i])//动态添加每一项路由哈。为什么要循环是因为addRoute值只能添加单个添加路由配置
        }
      }
      const getDefaultState = () => {
        return {
          token: getToken(),
          userInfo: null,
          sign: '',
          name: '',
          avatar: '',
          menus: null,
          allMenus: null,
        }
      }
      const useUserStore = defineStore('user',{
        state: () => (getDefaultState()),
        actions: {
          getInfo() {
            getUserInfo().then( res => {
              const { data } = res
              SET_USER_INFO(this,data.loginUser)
              SET_MENU(this,data.menuTreeList)
            })
          },
        }
      })
      export default useUserStore
  (5)用户的信息与权限-示例://getUserInfo()
    var aaa = {
      code: 0,
      msg: "成功",
      data: {
        loginUser: {
          id: "1567738052492341249",
          userName: "superAdmin",
          realName: "超级管理员",
          roleNames: ["管理员"],
          roleCodes: ["manage"],
        },
        menuTreeList: [
          {
            name: "首页",
            path: "/home",
            component: "main",
            url: null,
            children: [
              {
                name: "总编室总览",
                path: "/home/annotation",
                component: "home/index",
                url: null
              },
              { 
                name: "各中心的仪表盘",
                path: "create",
                component: "home/index",
                url: null,
              }
            ]
          },
          {
            name: "综合查询",
            path: "/search",
            component: "main",
            url: "/search/index",
            children: [
              {
                name: "自由剖析",
                path: "/search/index",
                component: "home/index",
                url: null,
              }
            ]
          },
          {
            name: "系统管理",
            path: "/sys",
            component: "main",
            url: "/sys/column-manage",
            children: [
              {
                name: "中心栏目管理",
                path: "/sys/column-manage",
                component: "sys-column-manage/index",
                url: "/sys/column-manage/list",
                children: [
                  {
                    name: "中心管理列表",
                    path: "/sys/column-manage/list",
                    component: "sys-column-manage/list",
                    url: null,
                  },
                  {
                    name: "中心详情页",
                    path: "/sys/column-manage/center-detail",
                    component: "sys-column-manage/center-detail",
                    url: null,
                  },
                ]
              },
              {
                name: "业务规则管理",//由我负责
                path: "/sys/column-rule",
                component: "sys-column-rule/index",
                url: "/sys/column-rule/list",
                children: [
                  {
                    name: "业务规则管理列表",
                    path: "/sys/column-rule/list",
                    component: "sys-column-rule/list",
                    url: null,
                  },
                  {
                    name: "新增原因",
                    path: "/sys/column-rule/reasons-edit",
                    component: "sys-column-rule/reasons-edit",
                    url: null,
                  },
                  {
                    name: "考核规则版本管理编辑",
                    path: "/sys/column-rule/detail",
                    component: "sys-column-rule/column-class-detail",
                    url: null,
                  }
                ]
              },
            ]
          }
        ]
      }
    }
2、vueRouter三大守卫
  (1)全局守卫-3个
    router.beforeEach(function(to, from, next) { 
      //在路由跳转发生前被调用
    });
    router.beforeResolve(function(to, from, next) { 
      //在导航被确认之前,且在所有组件内守卫和异步路由组件被解析之后被调用
    });
    router.afterEach(function(to, from) { 
      //在路由跳转完成后被调用
    });
    export default router
  (2)路由守卫-1个
    const routes = [
      //vue2中的this.$route,就是下面这样的对象
      {
        path: '/users/:id',
        component: UserDetails,
        meta: { title: '系统首页'},
        beforeEnter: (to, from) => {
          // reject the navigation
          return false
        },
      },
    ]
  (3)组件守卫-3个
    const UserDetails = {
      template: `...`, //name: 'YourComponent',
      beforeRouteEnter(to, from) {
        // 在渲染该组件的对应路由被验证前调用
        // 不能获取组件实例 `this` !
        // 因为当守卫执行时,组件实例还没被创建!
      },
      beforeRouteUpdate(to, from) {
        // 在当前路由改变,但是该组件被复用时调用
        // 举例来说,对于一个带有动态参数的路径 `/users/:id`,在 `/users/1` 和 `/users/2` 之间跳转的时候,
        // 由于会渲染同样的 `UserDetails` 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
        // 因为在这种情况发生的时候,组件已经挂载好了,导航守卫可以访问组件实例 `this`
      },
      beforeRouteLeave(to, from) {
        // 在导航离开渲染该组件的对应路由时调用
        // 与 `beforeRouteUpdate` 一样,它可以访问组件实例 `this`
      },
    } 
3、传参(vue-router3及以前)
  (1)隐式传参,name与params搭配,先给路由添加配置,后跳转,获取参数
    methods: {
      clickHand() {
        //以下是传参
        this.$router.push({
          name: 'user',
          params: { //参数不会出现在url路径上,
            thisId: 'id',
          }
        })
        //以下是接参
        this.$route.params.thisId
      }
    }
  (2)显式传参,path与query搭配,先问号传参跳转,后获取问号参数添加配置,最后获取配置参数
    methods: {
      clickHand() {
        //以下是传参
        this.$router.push({
          path: '/user',
          query: { //参数会出现在url路径上,
            thisId: 'id',
          },
        })
        //以下是接参
        this.$route.query.thisId
      }
    }
  (3)HTML传参,name与params搭配,path与query搭配
    <router-link to="/user/110">点击查看子页面</router-link> 
    <router-link :to="{path: '/user', query:{id: '110'}}">点击查看子页面</router-link> 
    <router-link :to="{name: 'user', params:{id: '110'}}">点击查看子页面</router-link>
  (4)动态路由传参,params或query获取参数
    const constantRoutes = [
      {
        path: '/profile/set/:id',
        name: 'set',
        component: import('@/view/profile/set.vue')
      }
    ]
    <script>
      var clickImage = function(){
        this.$router.push({
          path: '/profile/set/2'
        })
      }
    </script>
    <script>
      console.log( this.$route.params.thisId );
      console.log( this.$route.query.thisId );
    </script>
  附、传参说明
    A、path和query组合传参,参数出现在url上,页面刷新、数据不丢失,
    B、name和params组合传参,参数不会出现在url上,页面刷新、数据丢失,
    C、从2022-08-22发布的vue-router4.1.4开始,弃用name和params组合传参,起用history.state传参,需要后台配合!
4、传参(vue-router4)
  附、说明
    A、获取实例
      a、vue2用this.$router,用this.$router.back()返回(后退)前一个页面
      b、vue3用useRouter()
    B、获取当前路由对象
      a、vue2用this.$route
      b、vue3的useRoute()  
  (1)隐式传参,name与params搭配,先给路由添加配置,后跳转
    import { useRouter } from 'vue-router'
    import { useRoute } from 'vue-router'
    export default {
      setup() {
        clickHand() {
          //以下是传参
          const userRouter = useRouter()
          userRouter.push({
            name: 'Home',
            params: {
              name: 'dx',
              age: 18
            }
          })
          //以下是接参
          const route = useRoute()
          console.log(route.params)
        }
      }
    }
  (2)显式传参,path与query搭配,先问号传参跳转,后获取问号参数添加配置,最后获取配置参数
    import { useRouter } from 'vue-router'
    import { useRoute } from 'vue-router'
    export default {
      setup() {
        clickHand() {
          //以下是传参
          const userRouter = useRouter()
          userRouter.push({
            path: '/',
            query: {
              name: 'dx',
              age: 18
            }
          })
          //以下是接参
          const route = useRoute()
          console.log(route.query)
        }
      }
    }
    <script setup> 
      import { useRouter } from 'vue-router' 
      const jumpDetail = (row) => {
        router.push({
          path: '/sys/column-rule/detail',
          query: {
            sid: row.sid
          }
        })
      }
      附、浏览器地址栏,http://localhost/#/sys/column-rule/detail?sid=24i6
    </script>
  (3)HTML传参,name与params搭配,path与query搭配
    <router-link to="/user/110">点击查看子页面</router-link> 
    <router-link :to="{path: '/user', query:{id: '110'}}">点击查看子页面</router-link> 
    <router-link :to="{name: 'user', params:{id: '110'}}">点击查看子页面</router-link>
  (4)动态路由传参,params或query获取参数
    const constantRoutes = [
      {
        path: '/profile/set/:id',
        name: 'set',
        component: import('@/view/profile/set.vue')
      }
    ]
    <script setup>
      import { useRouter } from "vue-router"
      const router = useRouter()
      var clickImage = function(){
        router.push({
          path: '/profile/set/2'
        })
      }
    </script>
    <script setup>
      import { onMounted } from 'vue'
      import { useRoute } from "vue-router"
      const useRoute = useRoute()
      const useRouter = useRouter()
      onMounted(function() {
        console.log(useRoute.params)
        console.log(useRoute.query)
      });
    </script>
  (5)history.state传参
    //来源,https://github.com/vuejs/router/blob/main/packages/router/CHANGELOG.md#414-2022-08-22
    //以下是传参
    import { useRouter } from 'vue-router'
    const userRouter = useRouter()
    userRouter.push({
      name: 'set',
      path: '/profile/set',
      state: {//起用
        key1: 'value1',
        key2: 'value2'
      }
    })
    //以下是接参
    import { useRoute } from 'vue-router'
    onMounted(function() {
      const useRoute = useRoute();
      console.log(useRoute.history.state);
    });

十一、vue-cli最新版本的包和插件
1、vue语法插件:Vetur
2、vue主要版本发布时间
 来源,https://github.com/vuejs/core/releases
  (1)vue,1.0.0版,2015年10月27日
  (2)vue,2.0.0版,2016年10月01日
  (3)vue,3.0.0版,2020年01月04日,预发布
  (3)vue,3.0.0版,2020年09月18日,正式发布
 附、vue3的7个优点/优势
  (1)体积小,按需编译,Tree shaking
  (2)性能快,diff算法
  (3)复合API,如setup,将逻辑相关的代码放在一起,有利于代码维护,Compostion API
  (4)渲染API,如weex、myvue,Custom Renderer API
  (5)多根组件,vue创建一个虚拟的Fragment节点
  (6)更好地支持TS
  (7)实时请求、实时编译,用vite开发构建工具编辑
3、vue-cli主要版本发布时间
  来源,https://github.com/vuejs/vue-cli/blob/dev/CHANGELOG.md
  (1)vue-cli,3.0.0版,2018年08月10日
  (2)vue-cli,4.0.0版,2019年10月16日
  (3)vue-cli,4.5.0版,2020年07月24日,开始默认使用vue3
  (4)vue-cli,5.0.0版,2022年02月17日
  (5)vue-cli,内部高度集成了webpack,
    A、项目根目录没有webpack.config.js文件,
    B、只有vue.config.js文件,@vue/cli-service会自动加载该文件,去修改默认的webpack配置
  (6)cli,命令行接口(Command Line Interface)
4、@vue/cli 是一个npm包,全局安装,提供了终端里的vue命令
  (1)vue create 快速搭建一个新项目
  (2)vue serve 构建新想法的原型
  (3)vue ui 通过一套图形化界面管理我的所有项目
  (4)安装命令 npm install -g @vue/cli,
  (5)安装位置 C:\Users\Haier\AppData\Roaming\npm\node_modules\@vue\cli
  (6)升级命令 npm update -g @vue/cli
  (7)查看版本 vue --version
5、项目结构分析
  (1)node_modules:项目依赖文件夹
  (2)public:存放静态资源;webpack打包时,会将该文件夹中的静态资源原封不动地打包到dist文件夹中
    favicon.ion:图标
    index.html:模板,可通过webpack配置修改
  (3)src:源码目录
    assets:存放静态资源,webpack打包时,会把该文件夹中的静态资源当作一个模块,编译、打包到JS文件里面
    components:存放非页面组件。组件中的name属性应与文件名一样,方便后期维护;组件名使用大驼峰
    router:存放路由文件,主文件名为index.js
    store:存放Vuex文件,主文件名为index.js
    views:存放页面组件
    App.vue:根组件,是所有组件的父组件
    main.js:入口文件,是整个程序中最先执行的文件
  (4).gitignore:用于配置不需要被Git管理的文件(夹)
  (5)babel.config.js:babel的配置文件,用于处理ES语法的兼容问题
  (6)jsconfig.json:配置webpack的文件
  (7)package-lock.json:包版本控制文件。指定项目依赖包的版本号,保证其他人在执行npm i时下载的依赖包与原版本一致
  (8)package.json:应用包配置文件
  (9)postcss.config.js:配置(vue移动端自适应)。加载时机,在执行vue-cli里的开发或生产命令执行时,该文件在适当时机会被加载进去并执行--自悟。
  (10)README.md:项目注释
  (11).editorconfig,编辑器配置
    # 告诉EditorConfig插件,这是根文件,不用继续往上查找
    root = true
    # 匹配全部文件
    [*]
    # 设置字符集
    charset = utf-8
    # 缩进风格,可选space、tab
    indent_style = space
    # 缩进的空格数
    indent_size = 2
    # 结尾换行符,可选lf、cr、crlf
    end_of_line = lf
    # 在文件结尾插入新行
    insert_final_newline = true
    # 删除一行中的前后空格
    trim_trailing_whitespace = true
    # 匹配md结尾的文件
    [*.md]
    insert_final_newline = false
    trim_trailing_whitespace = false
  (12).env,环境配置,开发、生产环境均有效
  (13).env.development,开发环境配置,
  (14).env.production,生产环境配置
  (15).env.staging,测试环境配置
  (16).eslintignore,忽略eslint检查的文件列表,
  (17).eslintrc.js,eslint检查规则,
  (18).gitignore,忽略git上传文件列表,
  (19)vue.config.js示例,案例来源,online-class-manage
    //说明如下:
    //1、根据启动命令自动加载.env文件,如npm run dev
    //2、process.env,vue-cli通过它暴露环境对象
    //3、参考文档,https://cli.vuejs.org/zh/config
    //4、查看完整的webpack配置,用vue inspect > output.js
    'use strict'
    const path = require('path')
    function resolve(dir) {
      return path.join(__dirname, dir)
    }
    const CompressionPlugin = require('compression-webpack-plugin')
    const name = process.env.VUE_APP_TITLE || '算法管理平台' // 网页标题
    const port = process.env.port || process.env.npm_config_port || 80 // 端口
    console.log( process.env );
    // {
    //   ALLUSERSPROFILE: 'C:\\ProgramData',
    //   APPDATA: 'C:\\Users\\Haier\\AppData\\Roaming',
    //   BABEL_ENV: 'development',
    //   CHROME_CRASHPAD_PIPE_NAME: '\\\\.\\pipe\\crashpad_7464_GNJOZWUOEAADTFXC',
    //   COLOR: '1',
    //   COLORTERM: 'truecolor',
    //   CommonProgramFiles: 'C:\\Program Files\\Common Files',
    //   'CommonProgramFiles(x86)': 'C:\\Program Files (x86)\\Common Files',
    //   CommonProgramW6432: 'C:\\Program Files\\Common Files',
    //   COMPUTERNAME: 'DESKTOP-TEPB2V0',
    //   ComSpec: 'C:\\Windows\\system32\\cmd.exe',
    //   DriverData: 'C:\\Windows\\System32\\Drivers\\DriverData',
    //   EDITOR: 'notepad.exe',
    //   ENV: 'development',
    //   FPS_BROWSER_APP_PROFILE_STRING: 'Internet Explorer',
    //   FPS_BROWSER_USER_PROFILE_STRING: 'Default',
    //   GIT_ASKPASS: 'c:\\Users\\Haier\\AppData\\Local\\Programs\\Microsoft VS Code\\resources\\app\\extensions\\git\\dist\\askpass.sh',
    //   HOME: 'C:\\Users\\Haier',
    //   HOMEDRIVE: 'C:',
    //   HOMEPATH: '\\Users\\Haier',
    //   INIT_CWD: 'C:\\Users\\Haier\\Desktop\\online-class-manage',
    //   LANG: 'zh_CN.UTF-8',
    //   LOCALAPPDATA: 'C:\\Users\\Haier\\AppData\\Local',
    //   LOGONSERVER: '\\\\DESKTOP-TEPB2V0',
    //   NODE: 'C:\\Program Files\\nodejs\\node.exe',
    //   NODE_ENV: 'development',
    //   NODE_EXE: 'C:\\Program Files\\nodejs\\\\node.exe',
    //   NODE_OPTIONS: '',
    //   NPM_CLI_JS: 'C:\\Program Files\\nodejs\\node_modules\\npm\\bin\\npm-cli.js',
    //   npm_command: 'run-script',
    //   npm_config_cache: 'C:\\Users\\Haier\\AppData\\Local\\npm-cache',
    //   npm_config_globalconfig: 'C:\\Program Files\\nodejs\\etc\\npmrc',
    //   npm_config_init_module: 'C:\\Users\\Haier\\.npm-init.js',
    //   npm_config_metrics_registry: 'https://registry.npm.taobao.org/',
    //   npm_config_node_gyp: 'C:\\Users\\Haier\\AppData\\Roaming\\nvm\\v16.0.0\\node_modules\\npm\\node_modules\\node-gyp\\bin\\node-gyp.js',
    //   npm_config_noproxy: '',
    //   npm_config_prefix: 'C:\\Program Files\\nodejs',
    //   npm_config_registry: 'https://registry.npm.taobao.org/',
    //   npm_config_strict_ssl: '',
    //   npm_config_userconfig: 'C:\\Users\\Haier\\.npmrc',
    //   npm_config_user_agent: 'npm/7.10.0 node/v16.0.0 win32 x64',
    //   npm_execpath: 'C:\\Users\\Haier\\AppData\\Roaming\\nvm\\v16.0.0\\node_modules\\npm\\bin\\npm-cli.js',
    //   npm_lifecycle_event: 'dev',
    //   npm_lifecycle_script: 'vue-cli-service serve',
    //   npm_node_execpath: 'C:\\Program Files\\nodejs\\node.exe',
    //   npm_package_engines_node: '>=8.9',
    //   npm_package_engines_npm: '>= 3.0.0',
    //   npm_package_json: 'C:\\Users\\Haier\\Desktop\\online-class-manage\\package.json',
    //   npm_package_name: 'vue-admin-template',
    //   npm_package_version: '4.4.0',
    //   NPM_PREFIX_NPM_CLI_JS: 'C:\\Program Files\\nodejs\\node_modules\\npm\\bin\\npm-cli.js',
    //   NUMBER_OF_PROCESSORS: '6',
    //   NVM_HOME: 'C:\\Users\\Haier\\AppData\\Roaming\\nvm',
    //   NVM_SYMLINK: 'C:\\Program Files\\nodejs',
    //   OneDrive: 'C:\\Users\\Haier\\OneDrive',
    //   ORIGINAL_XDG_CURRENT_DESKTOP: 'undefined',
    //   OS: 'Windows_NT',
    //   Path: 'C:\\Users\\Haier\\Desktop\\online-class-manage\\node_modules\\.bin;C:\\Users\\Haier\\Desktop\\node_modules\\.bin;C:\\Users\\Haier\\node_modules\\.bin;C:\\Users\\node_modules\\.bin;C:\\node_modules\\.bin;C:\\Users\\Haier\\AppData\\Roaming\\nvm\\v16.0.0\\node_modules\\npm\\node_modules\\@npmcli\\run-script\\lib\\node-gyp-bin;C:\\Windows\\system32;C:\\Windows;C:\\Windows\\System32\\Wbem;C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\;C:\\Windows\\System32\\OpenSSH\\;C:\\Users\\Haier\\AppData\\Roaming\\npm;C:\\Program Files\\TortoiseGit\\bin;C:\\Program Files\\Git\\cmd;C:\\Program Files (x86)\\Tencent\\微信web开发者工具\\dll;C:\\Users\\Haier\\AppData\\Roaming\\nvm;C:\\Program Files\\nodejs;C:\\Users\\Haier\\AppData\\Local\\Microsoft\\WindowsApps;C:\\Users\\Haier\\AppData\\Local\\Programs\\Microsoft VS Code\\bin;C:\\Users\\Haier\\AppData\\Roaming\\nvm;C:\\Program Files\\nodejs;C:\\Users\\Haier\\AppData\\Roaming\\npm',
    //   PATHEXT: '.COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC;.CPL',
    //   PROCESSOR_ARCHITECTURE: 'AMD64',
    //   PROCESSOR_IDENTIFIER: 'Intel64 Family 6 Model 158 Stepping 10, GenuineIntel',
    //   PROCESSOR_LEVEL: '6',
    //   PROCESSOR_REVISION: '9e0a',
    //   ProgramData: 'C:\\ProgramData',
    //   ProgramFiles: 'C:\\Program Files',
    //   'ProgramFiles(x86)': 'C:\\Program Files (x86)',
    //   ProgramW6432: 'C:\\Program Files',
    //   PROMPT: '$P$G',
    //   PSModulePath: 'C:\\Users\\Haier\\Documents\\WindowsPowerShell\\Modules;C:\\Program Files\\WindowsPowerShell\\Modules;C:\\Windows\\system32\\WindowsPowerShell\\v1.0\\Modules',
    //   PUBLIC: 'C:\\Users\\Public',
    //   SESSIONNAME: 'Console',
    //   SystemDrive: 'C:',
    //   SystemRoot: 'C:\\Windows',
    //   TEMP: 'C:\\Users\\Haier\\AppData\\Local\\Temp',
    //   TERM_PROGRAM: 'vscode',
    //   TERM_PROGRAM_VERSION: '1.92.2',
    //   TMP: 'C:\\Users\\Haier\\AppData\\Local\\Temp',
    //   USERDOMAIN: 'DESKTOP-TEPB2V0',
    //   USERDOMAIN_ROAMINGPROFILE: 'DESKTOP-TEPB2V0',
    //   USERNAME: 'Haier',
    //   USERPROFILE: 'C:\\Users\\Haier',
    //   VSCODE_GIT_ASKPASS_EXTRA_ARGS: '',
    //   VSCODE_GIT_ASKPASS_MAIN: 'c:\\Users\\Haier\\AppData\\Local\\Programs\\Microsoft VS Code\\resources\\app\\extensions\\git\\dist\\askpass-main.js',
    //   VSCODE_GIT_ASKPASS_NODE: 'C:\\Users\\Haier\\AppData\\Local\\Programs\\Microsoft VS Code\\Code.exe',
    //   VSCODE_GIT_IPC_HANDLE: '\\\\.\\pipe\\vscode-git-9d30400437-sock',
    //   VSCODE_INJECTION: '1',
    //   VUE_APP_BASE_API: 'http://10.75.28.17:7777/',
    //   VUE_APP_SYS_API: 'http://10.75.28.17:7777/',
    //   VUE_CLI_TRANSPILE_BABEL_RUNTIME: 'true',
    //   WEBPACK_DEV_SERVER: 'true',
    //   windir: 'C:\\Windows'
    // }  
    module.exports = {
      pages: { // 在多页面(multi-page)模式下构建应用,webpack发现pages有对象属性,判断为多页面,然后逐个导入导出js、注入到html
        page1: {
          entry: 'src/main.js',// 默认入口
          template: 'public/index.html',// 默认模板来源
          filename: 'index.html',// 默认输出 dist/index.html
          title: 'Index Page',// 当使用title选项时,template中的title标签需要是<title><%= htmlWebpackPlugin.options.title %></title>
          chunks: ['chunk-vendors', 'chunk-common', 'index']// 在这个页面中包含的块,默认情况下会包含提取出来的通用chunk和vendor chunk
        },
        page2: {},
        page3: {},
        subpage: 'src/subpage/main.js' // 主页面,模板文件默认是`public/subpage.html`,如果不存在,就是`public/index.html`,输出文件默认是`subpage.html`.
      },
      pages: { // 在单页面(single-page)模式下构建应用
        entry: 'src/main.js',// 默认入口
        template: 'public/index.html',// 默认模板来源
        filename: 'index.html',// 默认输出 dist/index.html
        title: 'Index Page',// 当使用title选项时,template中的title标签需要是<title><%= htmlWebpackPlugin.options.title %></title>
        chunks: ['chunk-vendors', 'chunk-common', 'index']// 在这个页面中包含的块,默认情况下会包含提取出来的通用chunk和vendor chunk
      },
      publicPath: process.env.NODE_ENV === "production" ? "/admin/" : "../audit/operate",// 基本url,默认为'/',部署在https://www.cccc.com/admin/上
      outputDir: 'dist',// 打包文件存放目录,默认为'dist'
      assetsDir: 'static',// 打包文件存放目录里的静态资源目录,默认为'',从生成的资源覆写filename或chunkFilename时,assetsDir会被忽略
      // dist的目录结构如下
      // dist/favicon.ico
      // dist/index.html
      // dist/static/css
      // dist/static/fonts
      // dist/static/js
      // “dist”目录下的“文件”应放在服务器的“admin”目录下
      // admin的目录结构如下
      // admin/favicon.ico
      // admin/index.html
      // admin/static/css
      // admin/static/fonts
      // admin/static/js
      // dist/index.html 或 admin/index.html引入打包后的静态资源(如下图),当某个页面需要某张图片时,src会指向对应的js模块;用二次封装的axios交互
      lintOnSave: process.env.NODE_ENV === 'development',// 是否开启eslint保存检测,有效值:ture | false | 'error' 
      productionSourceMap: false,// 如果你不需要生产环境的 source map,可以将其设置为 false 以加速生产环境构建 
      devServer: {// webpack-dev-server 相关配置
        host: '0.0.0.0',
        port: port,
        open: true,
        headers: {
          'Access-Control-Allow-Origin': '*'
        },
        //问:在vue.config.js中,在before中引入mock.js,devServer.proxy还有效吗
        //答:1个请求用3种方案中的1种来处理,优先级从大往小为,Mock.js、devServer.proxy、直接发出(浏览器跨域)
        //验:在mock.js、devServer.proxy里都配置了相同的请求,结果只有Mock.js的请求生效
        before: require('./mock/mock-server.js'), 
        proxy: { 
          // 原理:改变源(不是跨域)
          // (1)在开发环境中,请求从浏览器发到开发服务器,在开发服务器改变源后,再发到后台服务器
          // (2)在生产环境中,请求从浏览器发到后台服务器,不可以改变源
          // 仅在“开发环境”中,请求动态资源时,下面配中的,会被代理
          "/app": {
            target: 'http://192.168.10.231:8080/', //目标接口域名
            // 以下跨域问题解决方案,前端改变源、后端判断源
            // 1、前端,webpack解决方案,changeOrigin: true,
            //   A、Request headers-请求头中的origin由默认的浏览器的origin更改为target的origin
            //   B、原生解决方案,xhr.setRequestHeader('Origin', 'https://xxxx.baidu.com/');
            // 2、后台,if(request.getHeader("Origin") === 'https://xxxx.baidu.com/')
            //   app.use(function(req, res, next) {
            //     if(req.getHeader("Origin") === 'https://xxxx.baidu.com/'){
            //       //以下,告诉浏览器,允许任何来源的代码访问资源
            //       res.header("Access-Control-Allow-Origin", "*");
            //       //以下,告诉浏览器,允许"https://www.cnblogs.com"访问资源
            //       res.header("Access-Control-Allow-Origin", "https://www.cnblogs.com");
            //       //以下,向客户端表明,服务器的返回会根据"Origin"请求标头而有所不同
            //       res.header("Vary", "Origin");
            //       next();
            //     }
            //   });
            changeOrigin: true,
            pathRewrite: {
              "^/app": "/" //重写接口
            }
          },
        },
        proxy: {//代理
          // detail: https://cli.vuejs.org/config/#devserver-proxy
          [process.env.VUE_APP_BASE_API]: {
            target: `http://localhost:8080`,
            changeOrigin: true,
            pathRewrite: {
              ['^' + process.env.VUE_APP_BASE_API]: ''
            }
          }
        },
        disableHostCheck: true
      },
      css: {
        loaderOptions: {
          sass: {
            sassOptions: { outputStyle: "expanded" }
          },
          scss:{
            prependData:"@import '../../styles/global-variables-style.scss';"
          }
        }
      },
      configureWebpack: {
        name: name,
        resolve: {
          extensions: ['.js', '.vue', '.json'],
          alias: {
            '@': resolve('src'), // 本项目
            '~menu': resolve('../../src/menu'),
            '~styles': resolve('../../styles'),
          }
        },
        plugins: [
          // http://doc.cctv.com/ruoyi-vue/other/faq.html#使用gzip解压缩静态文件
          new CompressionPlugin({
            cache: false,// 不启用文件缓存
            test: /\.(js|css|html)?$/i,// 压缩文件格式
            filename: '[path].gz[query]',// 压缩后的文件名
            algorithm: 'gzip',// 使用gzip压缩
            minRatio: 0.8// 压缩率小于1才会压缩
          }),
          //以下,浏览器打开多个页面
          new HtmlWebpackPlugin({
            filename: 'page1/index.html',
            chunks: ['page1'],
            template: './src/template.html'
          }),
          new HtmlWebpackPlugin({
            filename: 'page2/index.html',
            chunks: ['page2'],
            template: './src/template.html'
          }),      
        ],
      },
      chainWebpack(config) {
        config.plugins.delete('preload') // TODO: need test
        config.plugins.delete('prefetch') // TODO: need test
        // set svg-sprite-loader
        config.module
          .rule('svg')
          .exclude.add(resolve('src/assets/icons'))
          .end()
        config.module
          .rule('icons')
          .test(/\.svg$/)
          .include.add(resolve('src/assets/icons'))
          .end()
          .use('svg-sprite-loader')
          .loader('svg-sprite-loader')
          .options({
            symbolId: 'icon-[name]'
          })
          .end()
        config
          .when(process.env.NODE_ENV !== 'development',
            config => {
              config
                .plugin('ScriptExtHtmlWebpackPlugin')
                .after('html')
                .use('script-ext-html-webpack-plugin', [{
                  // `runtime` must same as runtimeChunk name. default is `runtime`
                  inline: /runtime\..*\.js$/
                }])
                .end()
              config
                .optimization.splitChunks({
                  chunks: 'all',
                  cacheGroups: {
                    libs: {
                      name: 'chunk-libs',
                      test: /[\\/]node_modules[\\/]/,
                      priority: 10,
                      chunks: 'initial' // only package third parties that are initially dependent
                    },
                    elementUI: {
                      name: 'chunk-elementUI', // split elementUI into a single package
                      priority: 20, // the weight needs to be larger than libs and app or it will be packaged into libs or app
                      test: /[\\/]node_modules[\\/]_?element-ui(.*)/ // in order to adapt to cnpm
                    },
                    commons: {
                      name: 'chunk-commons',
                      test: resolve('src/components'), // can customize your rules
                      minChunks: 3, //  minimum common number
                      priority: 5,
                      reuseExistingChunk: true
                    }
                  }
                })
              config
                .optimization.runtimeChunk('single')({
                  from: path.resolve(__dirname, './public/robots.txt'), //防爬虫文件
                  to: './' //到根目录下
                })
                
            }
          )
      }
    }
6、package.json
  (1)vue create hello-world运行后的package.json文件
    {
      "name": "hello-world",
      "version": "0.1.0",
      "private": true,
      "scripts": {
        "serve": "vue-cli-service serve",
        "build": "vue-cli-service build",
        "lint": "vue-cli-service lint"
      },
      "dependencies": {
        "core-js": "^3.8.3",
        "vue": "^3.2.13"
      },
      "devDependencies": {
        "@babel/core": "^7.12.16",
        "@babel/eslint-parser": "^7.12.16",
        "@vue/cli-plugin-babel": "~5.0.0",
        "@vue/cli-plugin-eslint": "~5.0.0",
        "@vue/cli-service": "~5.0.0",
        "eslint": "^7.32.0",
        "eslint-plugin-vue": "^8.0.3"
        //"axios": "^0.18.0","vue-cli-plugin-axios": "0.0.4",运行 vue add axios 后新增这2项,若生产环境也需要该插件,则还需运行npm install axios@^0.18.0 -S
      },
      "eslintConfig": {
        "root": true,
        "env": {
          "node": true
        },
        "extends": [
          "plugin:vue/vue3-essential",
          "eslint:recommended"
        ],
        "parserOptions": {
          "parser": "@babel/eslint-parser"
        },
        "rules": {//原本为空,这是后来新增
          "no-unused-vars": "off",//关闭报错之变量声明没使用
          "no-undef": "off",//关闭报错之变量使用没声明
        }
      },
      "browserslist": [
        "> 1%",
        "last 2 versions",
        "not dead",
        "not ie 11"
      ]
    }
  (2)安装element-plus、vue-router、vuex后的package.json文件
    注:与UVE3对应的ElementUI为element-plus,
    {
      "name": "hello-world0",
      "version": "0.1.0",
      "private": true,
      "scripts": {
        "serve": "vue-cli-service serve",
        "build": "vue-cli-service build",
        "lint": "vue-cli-service lint"
      },
      "dependencies": {
        "core-js": "^3.8.3",
        "element-plus": "^2.2.14",
        "nprogress": "^0.2.0",
        "vue": "^3.2.13",
        "vue-router": "^4.0.3",
        "vuex": "^4.0.0"
      },
      "devDependencies": {
        "@babel/core": "^7.12.16",
        "@babel/eslint-parser": "^7.12.16",
        "@vue/cli-plugin-babel": "~5.0.0",
        "@vue/cli-plugin-eslint": "~5.0.0",
        "@vue/cli-plugin-router": "^5.0.8",
        "@vue/cli-plugin-vuex": "^5.0.8",
        "@vue/cli-service": "~5.0.0",
        "eslint": "^7.32.0",
        "eslint-plugin-vue": "^8.0.3"
      },
      "eslintConfig": {
        "root": true,
        "env": {
          "node": true
        },
        "extends": [
          "plugin:vue/vue3-essential",
          "eslint:recommended"
        ],
        "parserOptions": {
          "parser": "@babel/eslint-parser"
        },
        "rules": {
          "no-unused-vars": "off",
          "no-undef": "off"
        }
      },
      "browserslist": [
        "> 1%",
        "last 2 versions",
        "not dead",
        "not ie 11"
      ]
    }
7、vue create hello-world执行时出错
  (1)现象Failed to get response from https://registry.npmmirror.com/binary-mirror-config,解决方案如下
  (2).vuerc的放置位置,C:\Users\Haier\.vuerc
  (3)运行npm config get registry
  (4)结果为https://registry.npmjs.org/时,在.vuerc文件里写 “useTaobaoRegistry”: false
  (5)结果为https://registry.npm.taobao.org/时,在.vuerc文件里写 “useTaobaoRegistry”: true
8、@vue/cli-service 是一个npm包,执行vue create hello-world时,它被局部安装在每个@vue/cli创建的项目中,提供了一个开发环境依赖。
  (1)构建于 webpack 和 webpack-dev-server 之上
  (2)内部的 vue-cli-service 命令,提供 serve、build 和 inspect 命令
  (3)vue-cli-service serve,开启本地服务器,加载.env.development文件,把里面的键值对添加到process.env中
  (4)vue-cli-service build,打包压缩项目,加载.env.production文件,把里面的键值对添加到process.env中
  (5)vue-cli-service build --mode staging,项目测试,加载.env.staging文件,把里面的键值对添加到process.env中
9、cli插件,向我的vue项目提供可选功能的npm包
  (1)都会包含一个生成器和一个运行插件
  (2)用于,Babel/TypeScript 转译、ESLint 集成、单元测试和 end-to-end 测试等
  (3)内建插件,用vue add XXX 安装,以 @vue/cli-plugin- 开头,存放位置为hello-world\node_modules\@vue\cli-plugin-,内置依赖属性。
    比如执行vue add vuex后,package.json文件有如下新增,"dependencies": {"vuex": "^4.0.0"},"devDependencies": {"@vue/cli-plugin-vuex": "^5.0.8",},
  (4)社区插件,用npm install XXX --save安装,如果用vue add XXX 安装,存放位置为hello-world\node_modules\vue-cli-plugin-,使用时,会出现bug,且不易修改。
  (5)安装命令 vue add eslint,等价于之前的vue add cli-plugin-eslint,从npm安装它,调用它的生成器
  (6)升级命令 upgrade [options] [plugin-name]
  (7)在项目内部运行 vue-cli-service 命令时,它会自动解析并加载 package.json 中列出的所有 CLI 插件
10、引入插件
  (1)引入echarts:import * as echarts from 'echarts';
  (2)引入axios:import axios from 'axios';在plugins文件夹下进行配置
11、修改插件
  (1)用config.module.rule('svg').exclude.add(resolve('src/icons')).end()对vue-cli-5.0里内置的'svg'模块规则进行修改
  (2)用config.module.rule('icons').test(/\.svg$/).include.add(resolve('src/icons')).end()定义并向vue-cli-5.0里注入名为'icons'的模块规则
12、问题与解决
  (1)问题1
    现象:<router-view/>有下划红线
    问题:无法使用 JSX,除非提供了 "--jsx" 标志
    解决:在jsconfig.json(与package.json同级)里加"jsx": "preserve",
   
十二、mock拦截请求|模拟数据
1、说明
  (1)原理:通过覆盖和模拟原生XMLHttpRequest来拦截Ajax请求
  (2)拦截:拦截成功或失败,都不发出请求,network里无记录
  (3)参数:Mock.mock(rurl?,rtype?,template|function(options))
    来源,https://www.jianshu.com/p/b5c58ae144d9
    来源,https://blog.csdn.net/qq_51357960/article/details/127285388
    A、Mock.mock(template),根据模板,生成数据
    B、Mock.mock(rurl,template),监听路由,根据模板,生成数据
    C、Mock.mock(rurl,function(options)),监听路由,根据函数返回值,生成数据
    D、Mock.mock(rurl,rtype,template),监听路由,根据请求方式、模板,生成数据
    E、Mock.mock(rurl,rtype,function(options)),监听路由,根据请求方式、函数返回值,生成数据
    F、参数说明
      a、rurl:表示要拦截的接口,可以是绝对地址或者正则表达式
      b、rtype:表示请求类型 get/post 等等
      c、template|function(options):生成数据的模板
2、Mock.mock只有一个对象参数时的用法
  来源,http://mockjs.com/examples.html
  说明,对象参数就是数据模板,包含数据占位符
  <!DOCTYPE html>
  <html lang='en'>
    <head>
      <meta charset='UTF-8'>
      <meta name='viewport' content='width=device-width, initial-scale=1.0'>
      <title>数据展示</title>
      <style>
        span{
          color:red;
          font-size: 16px;
        }
        .fontSize16{
          font-size: 16px;
        }
        .result{
          margin-left: 40px;
          padding: 40px;
          font-size: 12px;
          background: rgb(231, 228, 228);
          max-width: 400px;
          white-space: pre-wrap;
        }
      </style>
      <script src='https://cdn.jsdelivr.net/npm/mockjs@1.1.0/dist/mock.min.js'></script>
    </head>
    <body>
      <pre>
        <div class="fontSize16">以下来源,http://mockjs.com/examples.html</div>
        <div style="display: flex;">
          <div class="fontSize16">
            //以下<span>数据占位符</span>
            //以下基本数据
            '@boolean'
            '@natural(min, max)'
            '@integer(min, max)'
            '@range(start, stop, step)'
            //上面,返回由数字构成的数组, 
            '@float(min,max, dmin, dmax)'
            //上面,dmin(小数最少位数), 
            //上面,dmax(小数最多位数)
            '@string'
            //以下日期时间
            '@date'
            '@time'
            '@datetime'
            '@now'
            //以下网络
            '@domain'
            '@email'
            '@guid'
            '@id'
            '@ip'
            '@url'
          </div>
          <div class="fontSize16">
            //以下<span>数据占位符</span>
            //以下英文数据
            '@paragraph'
            '@sentence'
            '@word'
            '@title'
            '@first'
            '@last'
            '@name'
            //以下中文数据
            '@cparagraph'
            '@csentence'
            '@cword'
            '@ctitle'
            '@cfirst'
            '@clast'
            '@cname'
            '@region'
            '@province'
            '@city'
            '@county'
          </div>
          <div class="fontSize16">
            //以下<span>数据模板</span>
            //以下是data的定义
            var data= Mock.mock({
              'aaa': 200,
              'bbb': /\d{4,7}/, 
              'ccc': '@boolean',
              'ddd|2': [{
                'title': '@cword(2, 4)',
                'status|1': ['成功', '失败', '其它-@cword(1, 2)'],
              }],
              'eee|2': {
                '省级1': '@province',
                '省级2': '浙江省',
                '省级3': {
                  '河北省': '@city',
                  '河南省': {
                    '信阳市': {
                      '固始县': ['A',[1,2],'B',['3','4']],
                      //把数组某项换成'省级3'对象,看效果
                    }
                  }
                }
              },
              'fff|2-5': '★',
            });
            <span>'ddd|2': 对象、数组、字符串,由2项构成</span>
            <span>数据模板-包含-数据占位符;刷新页面,可以更新右图数据</span>
            <span>2种JS方案-展示data,1、直接展示,2、格式化展示</span>
            <span>JSON格式在线验证,https://www.bejson.com/</span>
          </div>
          <div id='div' class="result"></div>
        </div>
      </pre>
    </body>
  </html>
  <script>
    var data= Mock.mock({
      'aaa': 200,
      'bbb': /\d{4,7}/, 
      'ccc': '@boolean',
      'ddd|2': [{
        'title': '@cword(2, 4)',
        'status|1': ['成功', '失败', '其它-@cword(1, 2)'],
      }],
      'eee|2': {
        '省级1': '@province',
        '省级2': '浙江省',
        '省级3': {
          '河北省': '@city',
          '河南省': {
            '信阳市': {
              '固始县': ['A',[1,2],'B',['3','4']],
              //把数组某项换成'省级3'对象,看效果
            }
          }
        }
      },
      'fff|2-5': '★',
    });
    //以下2种展示方式
    //1、以下直接展示
    // document.getElementById('div').innerText = JSON.stringify(data);
    //2、以下格式化展示
    function isArray(value) { return {}.toString.call(value) === "[object Array]"; }
    function isObject(value) { return {}.toString.call(value) === "[object Object]"; }
    function addLine(num) {//添加换行
      var str ='\n';
      for(var i = 0; i < num; i++) str +='\n'
      return str;
    }
    function addSpace(num) {//添加空格
      var str ='\xa0';
      for(var i = 0; i < num; i++) str +='\xa0'
      return str;
    }
    function addStr(data, num, isAddDot) {//添加字符串
      var str = '';
      if (isObject(data)) {
        str += '{' + addLine(1);
        var i = 0;
        var strIn = ',';
        var length = Object.keys(data).length;
        for(var attr in data){
          var value = data[attr];
          i++;
          if(i == length) strIn = '';
          str += addSpace(4*num) + JSON.stringify(attr) + ":" + addStr(value, num+1) + strIn + addLine(1);
        }
        str +=  addSpace(4*(num-1)) + '}';
        if(isAddDot) str += ',';
      } else if (isArray(data)) {
        str += '[';
        for(var i=0;i<data.length;i++){
          var value = data[i];
          var isHasDot = i<data.length-1;
          str += addStr(value, num, isHasDot);
        }
        str += ']';
        if(isAddDot) str += ',';
      } else {
        str += JSON.stringify(data);
        if(isAddDot) str += ',';
      }
      return str;
    }
    document.getElementById('div').innerText = addStr(data, 1);
  </script>                      
3、示例,案例来源,online-class-manage
  (1)添加依赖,package.json
    {
      "name": "vue-admin-template",
      "scripts": {
        "dev": "vue-cli-service serve",
      },
      "dependencies": {
        "axios": "0.18.1",
        "vuex": "3.1.0"
      },
      "devDependencies": {
        "mockjs": "1.0.1-beta3",
      },
    }
  (2)使用依赖,配置模拟,require
    A、定义分散的路由与返回值
      ./mock/table.js
      const Mock = require('mockjs')
      const data = Mock.mock({
        'items|30': [{
          id: '@id', //数据占位符
          title: '@sentence(10, 20)',
          'status|1': ['published', 'draft', 'deleted'],
          author: 'name',
          display_time: '@datetime',
          pageviews: '@integer(300, 5000)'
        }]
      })
      module.exports = [
        {
          url: '/vue-admin-template/table/list',
          type: 'get',
          response: config => {
            const items = data.items
            return {
              code: 20000,
              data: {
                total: items.length,
                items: items
              }
            }
          }
        }
      ]
    B、合并分散的路由与返回值,在生产环境中使用模拟数据
      // 附、withCredentials使用示例
      // //1、前端
      // var xhr = new XMLHttpRequest();
      // xhr.open("GET", "http://example.com/api/data", true);
      // xhr.withCredentials = true;//在跨域请求中发送凭据信息,如携带cookie
      // xhr.onreadystatechange = function () {
      //   if (xhr.readyState === 4 && xhr.status === 200) {
      //     console.log(xhr.responseText);
      //   }
      // };
      // xhr.send();
      // //2、后端
      // const cors = require('cors');
      // const app = express();
      // app.use(
      //   cors({
      //     credentials: true, // 允许 withCredentials 的请求
      //     origin: 'http://example.com' // 指定允许的跨域请求来源
      //   })
      // );
      ./mock/index.js 
      const Mock = require('mockjs') //覆盖和模拟原生XMLHttpRequest
      const { param2Obj } = require('./utils')
      const user = require('./user.js')
      const table = require('./table.js')
      const mock_ = [...user,...table] //合并分散的路由与返回值
      function mockXHR() { //在生产环境中使用模拟数据
        Mock.XHR.prototype.proxy_send = Mock.XHR.prototype.send; //存储旧send
        Mock.XHR.prototype.send = function() { //定义新send
          // 猜想:
          // 1、真实路由没有比中虚拟路由,就放到this.custom.xhr,执行send,然后mockjs会返回相应的报错信息
          // 2、右侧是真实请求中的相关数据,this.withCredentials、this.responseType
          if (this.custom.xhr) {//如果没有配中请求
            this.custom.xhr.withCredentials = this.withCredentials || false //让“没有配中的请求”也能携带cookie
            if (this.responseType) {
              this.custom.xhr.responseType = this.responseType //让“没有配中的请求”不返回乱码
            }
          }
          this.proxy_send(...arguments) //调用旧send
        }
        function XHR2ExpressReqWrap(respond) {
          return function(options) {
            let result = null
            if (respond instanceof Function) {
              const { body, type, url } = options // https://expressjs.com/en/4--x/api.html#req
              result = respond({// 这个respond就是分散定义中的response,此处传入的对象没有被利用
                method: type,
                body: JSON.parse(body),
                query: param2Obj(url)
              })
            } else {
              result = respond
            }
            return Mock.mock(result)
          }
        }
        for (const i of mock_) { 
          // --非常重要--在浏览器里,拦截并重新定义ajax请求,url可以用正则 
          Mock.mock(new RegExp(i.url), i.type || 'get', XHR2ExpressReqWrap(i.response)) //直接传入i.response,似乎也行
        }
      }
      //以下,导出数据
      module.exports = {
        mock_, 
        mockXHR 
      }
    C、把每个定义的路由与返回值绑定
      ./mock/mock-server.js, 
      const chokidar = require('chokidar')
      const bodyParser = require('body-parser')
      const chalk = require('chalk')
      const path = require('path')
      const Mock = require('mockjs') 
      const { mock_ } = require('./mock/index.js') 
      const mockDir = path.join(process.cwd(), 'mock') //process.cwd(),获取当前工作目录
      function registerRoutes(app) { 
        let mockLastIndex
        const mocksForServer = mock_.map(route => {
          return {
            url: new RegExp(`${process.env.VUE_APP_BASE_API}${route.url}`),
            // console.log(new RegExp("ab+c", "i"));// /ab+c/i
            // console.log(new RegExp(/ab+c/, "i"));// /ab+c/i
            type: route.type || 'get',
            response: function(req, res) { 
              res.json(Mock.mock(route.response instanceof Function ? route.respond(req, res) : route.response))
            }
          }
        })
        for (const mock of mocksForServer) {
          // --非常重要--在node里,拦截并重新定义ajax请求,url可以用正则 
          app[mock.type](mock.url, mock.response) 
          mockLastIndex = app._router.stack.length
        }
        const mockRoutesLength = Object.keys(mocksForServer).length
        return {
          mockRoutesLength: mockRoutesLength,
          //自己开始的索引:最后索引 - 路由长度;避免后来多删
          mockStartIndex: mockLastIndex - mockRoutesLength 
        }
      }
      function clearModuleCache() { //项目原名,unregisterRoutes
        Object.keys(require.cache).forEach(i => {
          if (i.includes(mockDir)) {
            delete require.cache[require.resolve(i)]//require.cache:引入的模块将被缓存在这个对象中 
          }
        })
      }
      module.exports = function(app){
        app.use(bodyParser.json()) //解析请求体中json数据
        app.use(bodyParser.urlencoded({ //解析请求体中form表单数据
          extended: true
        }))
        const mockRoutes = registerRoutes(app)
        var mockRoutesLength = mockRoutes.mockRoutesLength
        var mockStartIndex = mockRoutes.mockStartIndex
        chokidar.watch(mockDir, { //初始化时,监听mock目录的变化
          ignored: /mock-server/,
          ignoreInitial: true
        }).on('all', (event, path) => { //更新时,执行下列代码
          if (event === 'change' || event === 'add') {
            try {
              clearModuleCache(); //1/3--这里清除“模块缓存”,下面会再次执行require('./mock/index.js'),更新“模块缓存”
              app._router.stack.splice(mockStartIndex, mockRoutesLength) //2/3--清除旧的“路由栈”,以免与新的重复
              const mockRoutes = registerRoutes(app); //3/3--生成新的“路由栈”,存储匹配关系
              mockRoutesLength = mockRoutes.mockRoutesLength;
              mockStartIndex = mockRoutes.mockStartIndex;
              console.log(chalk.magentaBright(`\n > Mock Server hot reload success! changed  ${path}`))
            } catch (error) {
              console.log(chalk.redBright(error))
            }
          }
        })
      }
  (3)使用模拟,require
    A、开发环境中注入//vue.config.js
      module.exports = {
        devServer: { //开发环境中
          before: require('./mock/mock-server.js') //进而引入虚拟数据,const { mock_ } = require('./mock/index.js') 
          before: function(app, server) {
            //生成新的“路由栈”,存储匹配关系
            app.get('/some/path', function(req, res) { 
              res.json({ custom: 'response' });;
            });
          }
        }
      };
    B、生产环境中注入模拟数据//main.js 
      if (process.env.NODE_ENV === 'production') { //在生产环境中使用模拟数据,上线之前,请移除
        const { mockXHR } = require('./mock/index.js')
        mockXHR()
      } 

十三、element-plus之Form表单验证规则
1、验证类型
  (1)type:标志要使用的validator的数据类型
  (2)required:必填
  (3)message:提示内容
  (4)trigger:触发条件 (change||blur)
  (5)min:最小值
  (6)max:最大值
  (7)len:精准长度 (优先级高于min,max)
  (8)enum:枚举中存在该值 (type必须为enum类型)
  (9)whitespace:不能包含空白符
  (10)pattern:正则 (必须加 required: true)
2、单项验证
  <el-form-item 
    label="邮箱"
    prop="email"
    :rules="[
      { required: true, message: '请输入邮箱地址', trigger: 'blur' },
      { type: 'email', message: '请输入正确的邮箱地址', trigger: ['blur', 'change'] }
    ]">
    <el-input v-model="dynamicValidateForm.email"></el-input>
  </el-form-item>
3、整体验证(vue2)
  原文链接:https://blog.csdn.net/cplvfx/article/details/125329481
  <el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-width="100px" class="demo-ruleForm">
    <el-form-item label="活动名称" prop="name">
      <el-input v-model="ruleForm.name"></el-input>
    </el-form-item>
    <el-form-item label="活动区域" prop="region">
      <el-select v-model="ruleForm.region" placeholder="请选择活动区域">
        <el-option label="区域一" value="shanghai"></el-option>
        <el-option label="区域二" value="beijing"></el-option>
      </el-select>
    </el-form-item>
    <el-form-item>
      <el-button type="primary" @click="submitForm('ruleForm')">立即创建</el-button>
      <el-button @click="resetForm('ruleForm')">重置</el-button>
    </el-form-item>
  </el-form>
  <script>
    export default {
      data() {
        return {
          ruleForm: {
            name: '',
            region: ''
          },
          rules: {
            name: [
              { required: true, message: '请输入活动名称', trigger: 'blur' },
              { min: 3, max: 5, message: '长度在 3 到 5 个字符', trigger: 'blur' }
            ],
            region: [
              { required: true, message: '请选择活动区域', trigger: 'change' }
            ]
          }
        };
      },
      methods: {
        submitForm(formName) {
          this.$refs[formName].validate((valid) => {//使用验证
            if (valid) {
              alert('submit!');
            } else {
              console.log('error submit!!');
              return false;
            }
          });
        },
        resetForm(formName) {
          this.$refs[formName].resetFields();//把表单项重置为初始值,并移除验证结果
        }
      }
    }
  </script>
4、单项和整体验证(vue3,传值传参传数据)
   附、关闭弹窗的3种方式,要重置里面的表单项及其验证 
    A、点击右上方叉号 
    B、点击确认,发射事件,让父级根据情况决定 
    C、点击取消,发射事件,让父级改变v-model变量为false 
  (1)父子组件
    <script setup>
      import ModelAdd from  './model-add.vue'
      const dialogVisible = ref(false)
      const showCreateOrUpdate = function(){
        dialogVisible.value = true;
      }
      const child = ref(null);
      const handleClose = function(done){
        child.value.clearValidate();//调用子组件的方法,清除验证
        done()
      };
    </script>
    <template>
      <div>
        <el-button  @click="showCreateOrUpdate()" >创建模型</el-button>
        <el-dialog v-model="dialogVisible" :title="text+'模型'" :before-close="handleClose" >
          <ModelAdd @submit="confirmNew" :form="formModel" @cancel="dialogVisible = false" :text="text" ref="child"></ModelAdd>
        </el-dialog>
      </div>
    </template>
    <style lang="scss">
      .model{}
    </style>
  (2)子组件,ModelAdd,含表单事件  
    注、表单验证规则(整体与单个),表单验证范围(整体与单个),表单验证清除、重置
    注、props.form,父组件传来的属性经转换后,才能在子组件的表单里改变
    <script setup>
      const props = defineProps({
        form: {
          type: Object,
          required: false, 
          default: () => {
            return { 
              name: "",
              organization: "",
              template: "",
              path: "",
            }
          }
        },
        text: {
          type: String,
          required: true, 
          default: ''
        },
      });
      const formModel = reactive(props.form); 
      const formRef = ref(null)
      const rules = {
        name: [
          { required: true, trigger: "blur", message: "模型名称不能为空" }, //为空验证
          { min: 3, max: 5, message: 'Length should be 3 to 5', trigger: 'blur', informType: 'warning'},//长短验-证
        ],
      };
      const emit = defineEmits(['submit','cancel'])
      const cancel = function () {
        formRef.value.clearValidate()
        emit('cancel')
      };
      const confirm = function () {
        formRef.value.validate((valid, invalidFields) => { //2、表单验证范围(整体与单个)
          if (valid) { 
            ElMessage.success( props.text + '成功' );
            emit('submit', formModel)
          } else {
            console.log('Invalid fields:', invalidFields);
            ElMessage.error('表单验-证失败');
            return false;
          }
        });
        formRef.value.validateField('username', (valid) => { //2、表单验证范围(整体与单个)
          if (valid) {
            console.log('用户名校验成功');
          } else {
            console.log('用户名校验失败');
          }
        });
      };
      const clearValidate = function () {//3、clearValidate,移除验-证;resetField,移除验-证且重置为初始值
        formRef.value.clearValidate()
        formRef.value.clearValidate('date');
        formRef.value.resetField()
        formRef.value.resetField('date')
      };
      defineExpose({ clearValidate })//暴露过的子组件方法,能被父组件调用
      const handleValidate = function (valid, invalidFields) {
        console.log('验-证结果:', valid);
        console.log('无效字段:', invalidFields);
        //这里可以添加自定义的验-证后的处理逻辑
      }
    </script>
    <template>
      <div class="model-add">
        <el-form
          :model="formModel"
          :rules="rules"  //1、表单验证规则-整体
          ref="formRef"
          @validate="handleValidate" //2、表单验证范围(表单项被验证后触发),表单事件
        >
          <el-form-item prop="name" label="模型名称">
            <el-input
              v-model="formModel.name"
              placeholder="请输入模型名称"
            />
          </el-form-item>
          <el-form-item 	label-width="100px"  label="办理事由" 
            :rules="{required: true, trigger: 'change', message:'请输入办事理由'}"> //1、表单验证规则-单个
            <el-input
              v-model="item.reason"
              style="width: 400px"
              maxlength="50"
              placeholder="请输入办理事由"
              show-word-limit
            />
          </el-form-item>
        </el-form>
        <div>
          <el-button type="primary" @click="confirm">确定</el-button>
          <el-button @click="cancel">取消</el-button>
        </div>		
      </div>
    </template>
    <style lang="scss">
      .model-add{}
    </style>
  
  

  

posted @ 2020-07-28 17:49  WEB前端工程师_钱成  阅读(9127)  评论(0编辑  收藏  举报