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); const loading = ref(false) const handleSave = () => { loading.value = true const imageData = canRefVas.value.toDataURL('image/png'); //将canvas转化为base64图片 emit('watermarkUpdate', imageData, config.content); loading.value = false }; let dataURItoBlob = (dataURI, type) => { let i, _i, _ref; let binStr = atob((dataURI.split(','))[1]); let len = binStr.length; let arr = new Uint8Array(len); for (i = _i = 0, _ref = len - 1; 0 <= _ref ? _i <= _ref : _i >= _ref; i = 0 <= _ref ? ++_i : --_i) { arr[i] = binStr.charCodeAt(i); } return new Blob([arr], { type: type }); }; 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 }) } 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 附、vue3.0.0,2020年01月04日,预发布 (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”函数与属性的区别 A、前者用props、context,接收属性、定义事件,如 a、props const { title } = toRefs(props); const title = toRef(props, 'title'); b、context context.attrs context.slots context.emit context.expose B、后者用defineProps、defineEmits、defineExpose,接收属性、定义事件, 直接使用,无需引入,如不能有import { defineProps } 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函数 附、2020年07月08日,Vue3.0.0-beta.20之前,setup(props, context)函数作为组件的一个配置项 附、不排斥vue2的配置和生命周期 a、setup里面的vue3配置覆盖外面的vue2配置,见“nameIcon:” b、setup里面的vue3生命周期与外面的vue2生命周期共存 A、setup(代替vue2的data、computed、methods、watch,没有实例,this指向undefined,创建实例,onbeforeCreate、oncreated没必要存在) B、onBeforeMount、onMounted、onBeforeUpdate、onUpdated、onBeforeUnmount、onUnmounted、onActivated、onDeactivated、onErrorCaptured C、context,非响应式对象,包含4个属性,attrs、slots、emit、expose,其中attrs是组件的props配置中没有声明的属性 a、attrs,获取透传过来的值 //传递给组件,却没有被该组件声明为props或emits的attribute或者v-on事件监听器 //最常见的例子就是class、style和id b、slots,插槽 //父组件调用了本组件,并添加了插槽内容 c、emit,子组件给父组件传值 d、expose,子组件暴露给父组件可以调用的属性和方法 D、数据需要return,才能供外面使用,返回对象中的数据和data中的数据同名时,setup优先级更高 E、vue3中,setup函数定义组件示例,不用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> F、vue3中,setup函数定义组件示例,使用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属性 附、2020年07月14日,Vue3.0.0-beta.21之后,增加了<script setup>的实验特性 附、2021年08月09日,Vue3.2.0之后,<script setup>正式使用 A、数据不需要return B、排斥vue2的配置和生命周期 C、新增api,defineProps、defineEmits、defineExpose, D、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、直接发出(浏览器跨域) 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,含表单事件 <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) => { if (valid) { //通过验-证后,才提交 ElMessage.success( props.text + '成功' ); emit('submit', formModel) } else { ElMessage.error('表单验-证失败'); return false; } }); formRef.value.validate((valid, invalidFields) => {//1、全面验-证 if (valid) { console.log('Form is valid!'); } else { console.log('Invalid fields:', invalidFields); } }); formRef.value.validateField('username', (valid) => {//2、单个验-证 if (valid) { console.log('用户名校验成功'); } else { console.log('用户名校验失败'); } }); }; const clearValidate = function () { formRef.value.clearValidate()//3、移除全面验-证;移除该表单所有项的验-证结果 formRef.value.clearValidate('date');//4、移除单个验-证;移除该表单date项的验-证结果 formRef.value.resetField()//5、全面重置;把表单所有项重置为初始值,并移除验-证结果 formRef.value.resetField('date')//6、单个重置;把表单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" ref="formRef" @validate="handleValidate" //表单事件:任一表单项被验证后触发 > <el-form-item prop="name" label="模型名称"> <el-input v-model="formModel.name" placeholder="请输入模型名称" /> </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>