赞
踩
scoped
只作用于本页面, 甚至不作用于页面引入的组件, 所以想让页面既不影响其他页面, 又能改变组件的样式, 可以使用 >>>
深度作用选择器;
iView 框架里的组件的样式无法在 scoped
里改, 但是用这个就可以, 当然, 自己写的其他组件也一样;
注意:
预处理器有可能不支持 >>>
, 可以用 /deep/
或 ::v-deep
代替 >>>
搜了下, 关于 /deep/
和 ::v-deep
有几种说法:
/deep/
在某些时候会报错, ::v-deep
更保险并且编译速度更快.::v-deep
针对的是 dart-sass 的 npm 包的, node-sass 用 /deep/
不知道哪种说法是对的, 我本地项目两个都可以(装的是 node-sass 4.13.1), 出错了的话就换一种试试吧
不要在预处理器中嵌套写 /deep/ , 本身这种写法是错误的, 嵌套中的 /deep/ 不会被解析, 大部分客户端做了兼容不会出错, 但苹果不支持 /deep/错误嵌套用法导致的ios移动端真机样式失效问题( >>>、 /deep/、::v-deep)
Chrome89 也不再兼容嵌套 /deep/ 了 /deep/的使用与导致样式失效问题处理
less 文件里写 /deep/ , @import 它时加上 scoped , 好像可以 scoped , 参见Vue style里面使用scoped属性并@import引入外部css, 作用域是全局的解决方案
使用 $nextTick()
方法,这个方法好像是让其内部的函数在 DOM 更新后再调用(具体没查);
有时想刷新一个页面, 常常用这个, 比如先把动态组件置空,然后在此方法中再把此组件还原回去;
使用 v-if
刷新组件和页面也是一样;
不要在 computed
中使用 ajax
请求数据
computed 属性为对象数组, 可以直接改对象的属性(v-model 绑定也可以)
watch
要监控对象属性的变化,需要使用 deep: true
, 详见 官方教程
watch
监控对象时, val
和 oldVal
一模一样,官网上关于这点的解释好像是在 迁移
那一部分;
变通方法: 可以直接 watch 对象的某个属性
watch
第一次绑定时是不会执行的, 加上这个 immediate: true
就可以了. 用法: 比如一个 Modal
使用了 v-if
绑定了父组件某个属性, 那么 ``immediate可以让
Modal一被创建就能
watch` 到外部传递给它的相关属性.
methods: {
methodName() {
//...
}
},
watch: {
watcher: {
handler: 'methodName'
}
}
<keep-alive>
标签包裹组件后,只有第一次加载组件才会触发 mounted
生命周期,后面再切换就是 activated
和 deactivated
v-on
事件监听器在 DOM 模板中会被自动转换为全小写 (因为 HTML 是大小写不敏感的),因此,推荐始终使用 kebab-case 的事件名。使用 eventBus
机制传递数据时,要注意监听事件的时机,不然有可能出现----跳转前的页面发送事件带参数过去时,对方还没来得及绑定监听事件,
举例: 我在跳转后的页面的 mounted
里加上了发送 "已 mounted 完成"
的 eventBus
事件, 当跳转前的页面接收到此事件才会传参数过去,因为两个页面被 <keep-alive>
标签包裹,所以也不用担心跳转后发不了事件
inheritAttrs
所有父组件传入子组件, 但未被子组件 props 注册的变量, 都会变成普通 html 元素属性, 作用到子组件根元素上; 在子组件声明时, 加上 inheritAttrs: false
, 就能避免此行为
<script>
export default {
name: 'authorityDrawerList',
inheritAttrs: false,
// ...
}
</script>
$attrs
所有未被子组件 props 注册的变量(非 props 属性)都会放入 $attrs 中 (class 和 style 除外), 子组件使用 this.$attrs.xxx
获取父组件传入变量, 也可以使用 v-bind="$attrs"
传给孙组件, 曾孙组件…
<template>
<Drawer v-bind="$attrs"></Drawer>
</template>
$listeners
$listeners
:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on=“$listeners” 传入内部组件
<template>
<Drawer v-on="$listeners"></Drawer>
</template>
应用
我在项目中实际中用到的地方, 是无限嵌套 Drawer — 用户点击表格行打开详情页, 在详情页又点击按钮打开新的详情页, 一层层嵌套;
此时, 如果使用 props 和 $emit , 就要一层层传递数据/数据, 这显然很麻烦; 此时使用 $attrs , 就能让最新的详情页直接获取最外层的数据, 使用 $listeners , 就能直接让最外层执行方法, 方便很多(虽然也要一层层 v-bind v-on)
这个只看了文档, 没咋用过, 略
// 这个我没弄成功,但是思路应该是这样,自己去官网搜 $options
Object.assign(this, this.$options.data.bind(this)())
场景: 做任务计划模块,计划有提醒模块,到时间后提醒
实现:
// 这里是发送请求完成的回调, 参数是外面定义的 if (response.data.code === '1') { // 创建任务成功 if (this.taskObj.remindTimeValue === -1) { // 不提醒 this.$store.commit('setTaskChangeObj', { // 设置 taskChangeObj (Vuex) id: JSON.parse(response.data.data).id, type: 'noNotice' }); } else { // 需要提醒 let time = this.taskObj.taskEndTime.getTime() - this.taskObj.remindTimeValue * 60000 - Date.now(); // 计算任务截止时间 - 任务提前多久提醒(准时, 提前多少分钟, 提前多少天) - 当前时间 if (time > 0) { // 如果计算得到的值大于 0 this.$store.commit('setTaskChangeObj', { id: JSON.parse(response.data.data).id, type: type === '新建' ? 'newTask' : type === '编辑' ? 'editTask' : '', taskName: this.taskObj.taskName, taskEndTime: this.taskObj.taskEndTime, taskDetails: this.taskObj.taskDetails, time: time }); // 存入相应的 taskChangeObj } else if (time === 0) { // 如果刚好需要提醒 this.$store.dispatch('taskNotice', { tip: this.taskObj.taskName, fromNick: this.taskEndTime.format('yyyy-MM-dd hh:mm'), // 这里的 format 是重写了 Date.prototype 上的方法 text: this.taskObj.taskDetails }); // 那么直接调用通知方法(这里是之前其他同事用插件做的, 我直接照着用的) } } // 其他代码 } else { this.$Message.error(`${type}任务失败!`); } // 除此之外, 还有"标记任务为已完成/未完成"可以改变任务状态
// 1. 定义, 用 Map 格式存储任务提醒, 格式为 任务id(字符串): 相应的 setTimeout 返回的 id taskNoticeTimeoutMap: new Map(), // 2. 定义, 进入最外层页面就要请求任务详情 getTaskNotice() { util.ajax({ url: '请求URL', method: 'post', data: { // 参数 } }).then(response => { if (response.data.code === '1') { // 请求成功 if (Array.isArray(response.data.data)) { // 如果返回数组 response.data.data.map(item => { // 遍历数组 if (item.remindTimeLatest < Date.now()) { // 如果提醒时间已过 // 相关处理 } else if (item.remindTimeLatest > Date.now()) { // 如果提醒时间还没到 let timeout = setTimeout(() => { // 设置 setTimeout this.taskNoticeTimeoutMap.delete(item.id + ''); // 到时间后从 map 中去掉此任务 this.$store.dispatch('taskNotice', { // 触发提醒 tip: item.taskName, fromNick: new Date(item.taskEndTime).format('yyyy-MM-dd hh:mm:ss '), text: item.taskDetails }); }, item.remindTimeLatest - Date.now()); // 时间设置为相差时间 this.taskNoticeTimeoutMap.set(item.id + '', timeout); // 加入此任务到 map } else { // 时间刚好到了, 那就直接提醒 this.$store.dispatch('taskNotice', { tip: item.taskName, fromNick: new Date(item.taskEndTime).format('yyyy-MM-dd hh:mm:ss '), text: item.taskDetails }); } }); } } else { this.$Message.error(response.data.data || '任务提醒功能故障'); } }); }, // 这个是调用上面的方法的, 此方法的作用主要是为了保证过 0 点了重新请求提醒列表 // 因为任务创建时可以设置重复(最低以天为单位), 然后请求是按天算的, 后天计算得到当天有哪些任务需要提醒, 传递给前端 updateTaskNotice() { this.getTaskNotice(); // 先调用上面的方法请求和加入相关任务到提醒map let tomorrowTimeStamp = new Date(new Date(Date.now() + 24 * 60 * 60 * 1000).toLocaleDateString()).getTime(); // 存入明天时间戳 if (Date.now() < tomorrowTimeStamp) { // 如果当前时间戳小于明天时间戳 setTimeout(() => { // 设置 setTimeout setInterval(() => { // 每天 0 点 for (let i of this.taskNoticeTimeoutMap) { // 遍历 map clearTimeout this.taskNoticeTimeoutMap.delete(i[0]) } this.getTaskNotice(); // 重新获取任务提醒 }, 24 * 60 * 60 * 1000) }, tomorrowTimeStamp - Date.now()); } else { // 否则从现在开始就执行上面的相关操作 setTimeout(() => { setInterval(() => { for (let i of this.taskNoticeTimeoutMap) { this.taskNoticeTimeoutMap.delete(i[0]) } this.getTaskNotice(); }, 24 * 60 * 60 * 1000) }, 24 * 60 * 60 * 1000); } }, // 3. computed 里取到 Vuex 的 taskChangeObj // 4. 然后在 watch 中监控 taskChangeObj taskChangeObj(obj) { if (obj) { switch (obj.type) { // 根据不同的任务类型处理相关的提醒 case 'newTask': // 新建 case 'editTask': // 编辑 case 'sign': // 标为未完成 if (obj.id) { // 如果当前 map 中已存在此任务, 那么 clearTimeout if (this.taskNoticeTimeoutMap.has(obj.id + '')) { clearTimeout(this.taskNoticeTimeoutMap.get(obj.id + '')); } // setTimeout 到时间提醒 + 从 map 删除 let timeout = setTimeout(() => { this.taskNoticeTimeoutMap.delete(obj.id + ''); this.$store.dispatch('taskNotice', { tip: obj.taskName, fromNick: obj.taskEndTime.format('yyyy-MM-dd hh:mm:ss '), text: obj.taskDetails }); }, obj.time); // 加入 clearTimeout 到 map this.taskNoticeTimeoutMap.set(obj.id + '', timeout); } break; case 'noNotice': // 不提醒 case 'unSign': // 标记为已完成 case 'delete': // 删除任务 // 清除相应的 setTimeout 和从 map 中删除 if (obj.id && this.taskNoticeTimeoutMap.has(obj.id + '')) { clearTimeout(this.taskNoticeTimeoutMap.get(obj.id + '')); this.taskNoticeTimeoutMap.delete(obj.id + ''); } break; } } this.$store.commit('setTaskChangeObj', null); }
// 想在 js 中使用 Vuex, 发现前辈在项目里是这么写的
// 之前 Vuex 定义时, 存放在 store/index.js 里, 生成了 Vuex 实例, 然后 export 了
// 只要直接引用这个变量就行了
const store = new Vuex.Store(Obj);
export default store;
/* 实际使用 */
import store from '../store'; // 引入实例
store.state.app.firstRedirect // 就这么用, 相当于把 this.$store 替换成 store
store.commit('changeFirstRedirect');
// 这个是根据上面来的, 大体一致
new VueRouter(RouterConfig); // router/index.js 中生成了实例并 export 了
/* 实际使用 */
import {router} from '../router/index'; //引入
router.push({
name: 'login'
}); // 使用
这个可以用 router.currentRoute 来获取当前路由信息对象
自己在 Vue.prototype 上定义一个方法(别用箭头函数), 在 Vue 文件中调用, 这时根据"一般情况下, 谁调用此函数, 函数里的 this 就指向谁"规则, this 和 Vue 文件里的 this 等价…
首先我们都知道 axios 中有拦截器, 在请求回来发现 token 过期时, 我们需要在 success拦截器函数中, 中断后续的请求处理逻辑, 并跳转到登录页重新登录, 并显示错误提示.
假定后台返回的数据格式如下:
{
code: '0'/'1'/'NEED_REDIRECT'/..., // 0 错误, 1 正常, NEED_REDIRECT 代表 token过期, 需要跳转到登录页重新登录
data: obj
}
可能有多个请求受到 token 过期的影响, 为了防止错误提示弹出多次, 在 Vuex 定义一个变量, 比如firstRedirect
;
它初始时为 true
, 当发现后台返回 code: 'NEED_REDIRECT'
时, 如果当前它为true
,
说明这是第一个被拦截下来的函数, 这时, 我们把它赋值为 false
;
这样, 设置一个 if (store.state.app.firstRedirect)
就能拦截下后面的请求(具体在 Vuex 里怎么存看自己的代码结构), 避免弹出多次错误提示.
跳转到登录页面后, 在 router.push
的回调函数中, 再把 firstRedirect
赋值回 true
但是实际发现, 本来会弹出多次的错误提示, 现在变成只弹两次, 而不是只弹一次, 猜想可能是这样:
前面几个都被拦截下来了, 但是到了某次请求返回结果时, 已经跳到了登录页面, firstRedirect
已经变回 true
了, 于是通过了 if
语句, 但是因为已经在登录页了, router.push
实际上没有跳转, 也就没有触发它的回调函数, firstRedirect
从此变为 false
, 还会影响到后续的拦截
所以, 需要加上判断——当前页面是否在登录页.
最后是发现过期后如何中断请求, 直接看下面的代码吧:
最终拦截器代码如下:
// 这里是在 js 文件中里引用 Vuex 和 Message(iView 组件) , 用法可见本页相关知识点 // 定义拦截器函数 let success_interceptor_func = response => { if (response.data.code === 'NEED_REDIRECT') { // 如果 token 过期 if (store.state.app.firstRedirect && router.currentRoute.name !== 'login') { // 如果是第一次拦截到过期请求, 且当前页面不是登录页 store.commit('changeFirstRedirect'); // Vuex 修改 firstRedirect 的值 Message.error(lang[Vue.config.lang].tokenExpired); // 弹出错误提示 Cookies.remove('token'); // 清除过期 token , 避免跳回登录页时被判断成"已有token, 不能再回登录页重复登录" (这里 Cookies 是引用的包) router.push({ name: 'login' // 跳转到登录页 }, () => { if (!store.state.app.firstRedirect) { store.commit('changeFirstRedirect'); // 改回 true } }); } throw new axios.Cancel('Token expired'); // 抛出错误, 中断请求 } return response; // 正常返回 response };
场景: 短时间内发送多个筛选表格数据请求(筛选条件不同), 想要的是最后一次请求得到的筛选数据, 然而可能最后一次请求完成时间反而比前面的更早, 最后一次请求写入的数据被更早的请求(但请求完成时间靠后)覆盖了, 导致筛选出来的数据(更早的)和筛选条件(当前的)不对应;
解决方案: 其他的当然也有, 但是看到 axios 有一个相关的设置, 就用这个了:
<script> export default { data() { return { cancelFunc: null, // 取消请求函数 ... }; }, methods: { get_table_datas() { // 如果之前有发送表格数据请求, 则取消之前的请求 if (typeof this.cancelFunc == 'function') { this.cancelFunc() } // 暂存 this let vm = this; // ajax 是 axios.create 创建的方法, 现在发送新的表格数据请求 ajax({ ... cancelToken: new axios.CancelToken(function executor (c) { vm.cancelFunc = c }) }).then(...) } } } </script>
转载: vue-cli3内存溢出,JavaScript heap out of memory
为啥小组里只有我一个人有这毛病…
在 render 函数体里把 h 改为 this.$createElement 例如:
let create = this.$createElement;
return create('div', ...)
大佬厉害!
let myFunc = function(params) {};
(function 形式, 不能是箭头函数)return this.myFunc.bind(this)
, 即可得到函数问题: 节流和防抖函数生成函数并返回, 想把这个直接设置为 watch 的 handler , 发现 handler: this.methodName
会报错 this undefined
解决方法: 在 created 中使用 $watch 注册监听函数, 并在 destroyed 时销毁
主要是vuex和组件通信
在模板中, 父组件通过 @hooks:created
这种形式监听
ps: 我看也有人写可以通过 vm.$on('hooks:created', cb)
或者 vm.$once('hooks:created', cb)
监听, 猜测是不是可以用 $parent , $children , $refs 来获取组件实例, 再调用 $on , 还没实际尝试
新建空白路由 refresh , 带参数 $router.replace 到 refresh 页后, 在 refresh 的 beforeRouteEnter 中再 replace 到指定 name
注意: 地址栏会有闪动, 另外, replace 时 title 记得别改 title , 避免 title 闪动
https://cn.vuejs.org/v2/guide/plugins.html
使用插件统一管理 vue.protoType.methods , vue.methods , mixins , filters , directives
https://cli.vuejs.org/zh/guide/mode-and-env.html
使用环境变量和模式来指定相关地址等数据
https://cn.vuejs.org/v2/guide/render-function.html#JSX
https://github.com/vuejs/jsx
可以考虑使用 jsx 来更方便地 render VNode
统一管理接口调用
本体
directives: { drag: { // 指令的定义 bind: function(el, binding) { // 阻止事件冒泡 // 不仅仅要stopPropagation,还要preventDefault function pauseEvent(e) { if (e.stopPropagation) e.stopPropagation(); if (e.preventDefault) e.preventDefault(); e.cancelBubble = true; e.returnValue = false; return false; } const oDiv = el; // 获取当前元素 oDiv.onmousedown = (e) => { // 在事件中 e = e || window.event; pauseEvent(e); // 算出鼠标按下元素时的初始位置 let disX = e.clientX; let disY = e.clientY; // 获取元素的 computedStyle const styleObj = window.getComputedStyle(oDiv); // 鼠标移动时设置元素的定位属性 document.onmousemove = e => {= // 用鼠标移动后的位置减去鼠标的初始位置得到鼠标位移 const left = e.clientX - disX; const top = e.clientY - disY; if (left || top) { // 确保移动时不触发点击内部点击事件 binding.value.changeDragStatus(true); // 算出鼠标相对元素的位置 disX = e.clientX; disY = e.clientY; // 移动当前元素 oDiv.style.right = `${Number(styleObj.right.replace('px', '')) - left}px`; oDiv.style.bottom = `${Number(styleObj.bottom.replace('px', '')) - top}px`; } }; // 鼠标松开时清除掉鼠标移动监听 document.onmouseup = (e) => { document.onmousemove = null; document.onmouseup = null; binding.value.changeDragStatus(false); }; }; } } }
避免触发子元素 click 事件
// 插件绑定方法 v-drag="{changeDragStatus}" // 方法定义 changeDragStatus(val) { this.dragging = val; } // html 部分, 拖拽时给不想触发 click 的元素加上 disableClick 类 :class="{'disableClick': dragging}" // css 部分, 使用 point-event 避免触发鼠标事件 .disableClick { pointer-events: none; } // 插件内部使用 // 搜索 changeDragStatus 部分
避免 onmouseup 失效
pauseEvent
备注:
onmousemove
, 很奇怪的是, 第一次 oDiv.onmousedown
时, 哪怕没有移动鼠标, 也会触发其内部定义的 document.onmousemove
, 这有可能导致 BUG , 因此, 加上了 if (left || top)
来确保鼠标真的是有位移问题定位:
axios.created 创建的 ajax 配置底下加了个莫名其妙的 cancelToken , 用的同一个 axios.cancelToken , 传的cancelToken.source().token
逐个代码块注释解注, 发现在某个请求之后出现内存问题, 控制台打快照对比切换前后变化(有个选项可以看到在快照 2 和快照 1 之间被分配的数据), 发现就一个 cancelToken 看着眼熟
跳到该请求 ajax 配置定义处, 发现比之前多了一个 cancelToken (其他同事加的), 上 Google 搜索这个是否会导致内存泄漏, 搜索栏自动联想"axios canceltoken memory leak", 应该就是这个了
频繁切换组件后, 使用 vue-i18n, this.$t 可能取不到 this 导致报错
定位问题时, 我是注释大块代码后在左侧目录快速切换子组件 50 次看看内存上升多少, 发现在这种情况下控制台经常报错"TypeError: Cannot read property ‘_t’ of null", 这时内存有变化
搜了下, 找到一个回答
https://stackoverflow.com/questions/54666293/vue-i18n-cannot-read-property-t-of-undefined-at-proxy-vue-t
照着这个, 在 new Vue 之后加上了 ‘Vue.prototype._i18n = i18n;’ , 提示语消失了, 不过之后又有出现, 只是频率降低很多, 有兴趣的可以继续看看
某组件绑定事件后未解绑事件
最后一路解注释到这块儿, 加上解绑后整体解决
可以使用 {{ ‘\xa0’ }} 代替
背景:
登录页加载了两个 1m 以上的 js , 太大了, 打开非常简单的登录页需要 7 - 9s
优化方向:
webpack 配置优化
主要是打包增加 gzip 压缩 — 前端加上 compress-webpack-plugin 设置好, 也需要后端配合设置好
效果, 两个 js 文件大小瞬间减少了一半还多
加上 webpack-bundle-analyzer 插件, 打包后看看各文件里还有哪些模块占大头的, 能不能拆分之类的
其他…网上搜相关文章 + 对照着 webpack 官方文档慢慢尝试
之前没有走按需加载的路由组件统一改为走按需加载
非常坑的是, 之前做过按需加载, 但这次排查时, 发现实际上只有初始化二级路由时才用了按需加载, 根据目录权限接口的返回生成一级路由时, 一级路由的 component (按需加载 Main.vue)不是走的按需加载, 还有个别新增的不从属于 Main.vue 的单页也没走按需加载
改为按需加载, 两个 js 文件大小再次降低一半多
将 main.js 内大部分不需要在登录页加载的引入挪到 Main.vue 中, 减轻首屏 js 文件大小(相应地, main.vue 对应的 js 文件变大)
其他非 Main.vue 界面(登录页, 忘记密码页, 中转页, 邮件单页)中使用的全局 api 可能有缺失, 需要调整引入方式
将 index.html 里没必要第一时间引入的资源, 往后移动, 或者加上 async 或者 defer , fetchPriority="low"之类的
去掉冗余的引入, 各种 import 了没使用的都注释掉
其他非主体框架页面(Main.vue), 比如 login 页面, 忘记密码页面, 各类单页等, 如果有用到全局 component , api 等, 需要检查一遍是否还有效, 无效了最好自己单独引入
结果:
首页加载的最大的两个文件 , appXXXX.js 和 chunk-vendorXXXX.js , 降到了 200 多 K , 300 多 K
google 网址测速, pc 首屏显示时间为 1.5s 左右, 移动端首屏显示时间为 6s 左右
未来方向:
参考资料 1 中提出的几条路线(服务端渲染, 打包预渲染, ), 因为是改造现有应用, 所以综合下来选了 prerender-spa-plugin 路线;
此路线大意为: 此插件可以在页面渲染后再打包生成静态页面, 生成的页面因为有了内容, 可以被 Google 收录
安装 prerender-spa-plugin 插件(当前 ^3.4.0)
修改 Vue.config.js , 应用上述 plugin
// 引入 const PrerenderSPAPlugin = require('prerender-spa-plugin'); const Renderer = PrerenderSPAPlugin.PuppeteerRenderer; // 配置变量新增内容 const VueConfig = { publicPath: '/Front-Vue', // 原 publicPath , 不用改 outputDir: 'dist/Front-Vue', // 新增 outputDir 属性(之前没有), 指向打包后的文件夹 + publicPath /* ... */ // configureWebpack 新增 plugin 配置 configureWebpack: { plugins: [ new PrerenderSPAPlugin({ staticDir: resolve('dist'), outputDir: resolve('/dist/Front-Vue/'), indexPath: resolve('dist/Front-Vue/index.html'), routes: ['/', '/login'], // 需要渲染的页面, 与路由对应 postProcess(renderedRoute) { return renderedRoute; }, renderer: new Renderer({ inject: { foo: 'bar' }, headless: false, renderAfterDocumentEvent: 'render-event' // 自定义的渲染完成事件, 触发后开始抓取 // 还有些"等待xx元素加载完毕", "延时等待渲染完成"之类的参数, 详见 github 上 prerender-spa-plugin 的 README 文档 }) }) ] } }
在 new Vue 的 mounted 函数中加上触发自定义的加载完成事件
该事件通知到 plugin : 页面渲染完毕, 可开始生成静态页; 也可以换成在其他处触发此事件 — 但有文章提到:“不是在根组件中设置此事件无效”
document.dispatch(new Event('render-event'))
修改 Vue-router 配置
{
// before...
mode: 'history', // 此插件要求, 必须为 history 模式
base: '/Front-Vue' // 与 Vue.config.js 的 publicPath 相同; --- 我之前是 hash 模式, 对 history 的配置不熟, 试了下这里保持一致才能正常加载页面
// after...
}
打包, 上传页面到 Google 收录, 确定当前可以收录
访问 请求(重新)编入索引, 按操作将要收录的页面收录到 Google , 已收录后, 去 Google 搜索下页面标题/网址, 看能否搜索到
可使用 Vue-meta-info 组件添加 meta 信息
iView 因为多个组件有内存泄漏问题, 且官方不处理问题直接关闭 issue , 现已放弃, 改为使用 Element-UI
iView 框架的 DatePicker
组件直接使用 v-model
有问题(国际时间和本地时间的问题),可以使用 value
和 @on-change
,手动赋值,这样显示就完全正常;
Datepicker
组件,可以使用 @on-open-change
,在其中设置好唯一标志本次打开的 Datepicker
的属性,然后再在 @on-change
中处理value
并不会让 Datepicker
组件的显示也跟着改iView 中有些组件可以绑定的一些事件,其本身不需要传入参数,只需要在定义事件方法时写入形参就能取到,但如果在传入时附加参数,就取不到默认参数了
此时可以使用 @on-change="setOption($event, 其他参数)"
, 此时 $event
就是默认参数
某些函数默认带有两个参数, 此时用 $event
只能取到第一个(**查了一下好像是原生事件
e
v
e
n
t
获取事件对象
,
非原生事件只取第一个参数
∗
∗
)
,
搜索后发现可以通过
‘
a
r
g
u
m
e
n
t
s
‘
代替
‘
event 获取事件对象, 非原生事件只取第一个参数**), 搜索后发现可以通过 `arguments` 代替 `
event获取事件对象,非原生事件只取第一个参数∗∗),搜索后发现可以通过‘arguments‘代替‘event`
就 Poptip
组件而言(其他的没观察), 它的 transfer
属性, 是把组件的气泡放到全局 body
中.
这种情况下, 在本页面的样式中操作无用, 只有在全局中调整才可以, 但是要使用 popper-class
绑定类名到 Poptip
组件才行
但是也要注意, 在这种情况下, 触发气泡的元素(trigger)还在组件中, 使用深度选择器在 scoped
中选择 ivu-poptip-rel
即可
场景: iView , 三个 Select
组件,第一个的 @on-change
会动态改变第二个的 option
数组,以及第三个的类型( input
或 DatePicker
), 它的使用场景是填写筛选条件, 比如第一项选 最近更新时间,那第二项就会是 ['早于', '晚于', '时间段']
, 第三项变为 Datepicker
, 第一项选 客户星级,那第二项就是 ['大于', '小于', '等于']
, 第三项变为 InputNumber
问题: 改变首项筛选条件时, change
前后若根据首项获取的二项的 option
数组前后 length
相等(如,都有三个选项), 那么选择二项时,虽然实际上可以正确筛选,但文字总是显示成 change 前的选项.
解决:直接用 ref
取数据,当 Select
组件的 data
上的 selectedSingle
不等于 model
时, @on-change
的方法直接把 model
赋给前者
需要注意: v-for
中写的 ref
取到的值是个数组,具体到我当时的实例中,是个只有一项的数组,用 [0]
取组件数据
场景: 做邮件系统,选择收件人,需要既可以显示候选账号进行下拉选择,又可以直接手动输入账号,但是 iView 本身没有符合需求的组件
思路:
div
,在其中加入 Tag
系列,后面追加一个 input
, 再用隐藏的 span
实现 input
的动态变长(在这过程中我还找到了 contentEditable
这个可以让 div
可编辑的属性),这一步我是直接用的 filterable
+ multiple
的 Select
组件生成的 HTML
代码input
前再加一个 div
, 再把 input
长度限制到只能显示一个光标, 动态填入输入内容到前面的 div
,这样就不用变长,也不用担心溢出和让 input
换行filterable
+ multiple
的 Select
组件已经可以了,稍作调整就行.实现:
filterable
+ multiple
的 Select
组件生成的代码结构, mounted
时,为输入框绑定 blur
事件,根据 e.target.value
判断输入值是否已在下拉列表(v-for
Option
数组),是否已被选中,如果没有,则 push
进去Select
的 @on-change
中,询盘判断当前数组各项是否正则校验邮箱(推荐 Regulex ----正则可视化工具+邮箱正则表达式)通过,如果通过,则通过 DOM
绑定对应位置的 .ivu-tag-text
字体标黑(通过的情况也要显式操作,不然也可能变红),不通过标红场景: 使用 Cascader
级联组件,要求动态加入一级目录,点击一级目录后的请求二级目录还是用组件自带的搜索+动态加载功能
实现:
首先想的是直接请求一级目录动态添加,但是组件本身的机制是,使用筛选后得到的值点击后就直接被认定为完成选择,跟我想的点击动态生成的一级目录就会请求二级目录不同
于是我使用 slot ,在其中定义一个 Input 组件,这样看上去和原来一样,也能运行,但是会报错
在 Input 组件上绑定 @input.native ,
Cascader 组件上 @on-change 做回填到 Input 用,
@on-visible-change , 本意是想要让它在关闭 panel 时检测是否已经有选中的选项,有的话就回填
避免用户已选择选项后又输入字符,没有匹配到就关闭了 cascader ,这时没有触发 cascader 的 on-change 函数,所以没有回填,这时其实 cascader 是有数据的,但是 input 里却仍然显示之前没匹配到时输入的字符
结果发现 on-visible-change 好像只检测到了 visible 为 true 的情况,
控制台报错,因为组件源码里定义的 slot 默认内容中的 input 被 slot 中的 input 组件替换了,后面用到默认 input 时就会报错,不知道是不是因为这个原因才没检测到 visible 为 false 的情况,报错的时机和 panel 关闭时间相合
不得已只能在展开时清空 cascader 的 v-model 了,毕竟展开就说明是要搜索,这也说的过去
<!-- 思路: 框架里用了 include 但是没用 exclude, 动态检测当前页面的 name , 把它加入到 exclude 中然后清掉 router-view , 再把它从 exclude 中去掉, 再显示 router-view 注意: 这种方式没有从服务器重新请求文件 --> <!-- 框架原代码 --> <keep-alive :include="cachePage"> <router-view></router-view> </keep-alive> <!-- 变动后 --> <keep-alive :include="cachePage" :exclude="excludePage"> <router-view v-if="update"></router-view> </keep-alive> <!-- 在 data 中定义 --> update: true, excludePage: '' <!-- 更新方法 --> updateCurrent() { this.excludePage = this.$route.name; this.update = false; this.$nextTick(() => { this.update = true; this.excludePage = ''; }); }
直接把 iView 的 GitHub 仓库下载下来
改动代码后,可以 npm run dev 查看效果, npm run dist 重新编译
编译完成之后,用新的 dist 文件把原来的框架中的 node_modules 中的 iView 文件夹的 dist 代码覆盖掉, 如果需要引用 iview 里的组件, 把 src 也替换掉(一般也只会改 src 吧?)
如果不想每次编译那么麻烦, 可以直接把下载下来的 iview src 文件夹复制到项目中, 引用的时候直接按路径引它而不是安装好的 iview 包;
但是这样也有麻烦:
所以最后还是放弃直接引入了, 只在早期开发阶段不急着上线时用
多选:
使用 Cascader 的 自定义显示 功能, 用带 multiple 的 Select 替换掉组件自身的文本框
修改 Select 相关样式
.select {
/* 让 select 框内的文字能正常换行 */
white-space: normal;
/* 深度作用选择器, 让 scoped 的样式能影响到子组件 */
/* 让已选项 tag 的高度自适应 */
/deep/ .ivu-tag.ivu-tag-checked {
height: auto;
}
/* 隐藏掉 Select 的下拉框 */
/deep/ .ivu-select-dropdown {
display: none;
}
}
为 Cascader 组件绑定 on-change 事件
当事件传入的已选项数据不为空时, 取出已选项数据, 格式化后填入 Select 的 option 列表和 v-model 绑定的数组
为 Cascader 组件绑定 on-visible-change 事件
每次 visible-change 时, 都通过给 Cascader 设置的 ref 来清空 Cascader 的已选项
/* 通过查看 iview 源码可知点击清空图标时组件执行的操作, 从而得到下面的代码 */
this.$refs.cas.currentValue = this.$refs.cas.selected = this.$refs.cas.tmpSelected = [];
直接 Ctrl + B (webstorm 下)跳转到组件的定义处, 就能发现源码中也是 export 组件在引用使用的, 于是照着来:
// 以 this.$Message 为例
import Message from 'iview/src/components/message'
Message.error('出错信息');
=
开头, 则会被 Excel 识别为公式, 错误显示."\t"
(注意双引号) 就行了,
的内容被分割成多列的情况, 用 iView 文档中说的 quoted: true
给数据统一包裹上""
就行了<DatePicker open ...>
<span></span>
</DatePicker>
但是注意, 这样展示的 Datepicker 仍然会触发 on-open-change 事件, 造成后果和解决方案参看下方
上面 Datepicker 的 open 始终打开, 但仍会触发 on-open-change 事件造成阻塞, 导致: 点击其它地方时会先触发 on-open-change(false) 随后不再执行, 导致第一次点击无效
另外, 上 GitHub 看了下, iview 项目有人提 issue 表示, Select 展开情况下, 点击别处想执行操作不会生效, 因为跟上面一样要先关闭 Select , 官方表示就是这么设计的
解决方法: 在第一次渲染(open 变为 true)时就点击一下(选不会影响页面行为的地方点击, 比如这里选的是 Datepicker 的父级 Poptip 的 $el)提前触发这个 on-open-change(false), 之后就不会阻塞了
// Datepicker 的 @on-open-change
change_date_popper(val) {
if (val) {
this.$refs.poptip.$el.click();
}
}
以后这类情况(Datepicker, open 始终打开)都要这么弄, 类似的弹出型出现同样的问题应该也可以这么弄
场景: 表格页点击进入详情页, 详情页又有一些的关联的详情页, 比如 商品列表 => 商品详情页 => 商品厂家详情页 => 厂家下别的商品的详情页 => …
实现:
详情页分开写成组件, 用一个中间层总体处理 Drawer 和 详情页, 外部直接引用这个中间层.
中间层大致代码:
<template> <div> <!-- 最外层 Drawer --> <Drawer :value="visible" @on-visible-change="toggle_drawer($event, 'main')" ...> <template v-if="type === 'customer'"> <CustomerDetail ...></CustomerDetail> </template> <template v-else-if="type === 'contact'"> <ContactDetail ...></ContactDetail> </template> .... </Drawer> <!-- 后续连环 Drawer --> <Drawer v-for="(item, index) in drawer_list" v-model="item.visible" @on-visible-change="toggle_drawer($event, index)" ...> <template v-if="type === 'customer'"> <CustomerDetail ...></CustomerDetail> </template> <template v-else-if="type === 'contact'"> <ContactDetail ...></ContactDetail> </template> .... </Drawer> </div> </template> <script> import ... export default { ... props: [ 'visible', // 最外层 Drawer 显隐, 外部引用时加上 .sync 'type', // 最外层 Drawer 类型 'id', // 最外层 Drawer 唯一标志 ... ], data() { return { drawer_list: [], ... }; }, methods: { // 切换 drawer 可视状态 toggle_drawer(val, index) { // 如果更改最外层 Drawer 的可视状态 if (index === 'main') { // 先把 drawer_list 清空 // 如果直接更改数组, 会导致关闭所有 drawer 后数组为空但仍存在不可点击的 drawer 的 DOM , 所以先调整 DOM 可视状态为 false , 再在 setTimeout 中更改数组, 时间间隔自己调, 以 console 不报错为准 if (this.drawer_list.length) { this.drawer_list.forEach(item => item.visible = false); setTimeout(() => { this.drawer_list = []; }, 400); } // 更新外部 prop: visible if (!val) { this.$emit('update:visible', false); } } else { // 如果是隐藏掉 drawer_list 的某一项 if (!val) { this.drawer_list[index].visible = false; setTimeout(() => { this.drawer_list.splice(index, 1); }, 400); } } }, // 预备绑定给当前所处的最外层 DOM (此时以 .single-page 为例)的点击事件, 点击表格条目时打开相应的详情页, 点击其它位置关闭最上一层 Drawer click_blank(e) { let boo = true; let temp_el = e.target; // 以 e.target (触发点击事件的元素)一路向上搜索直到搜索到最外层 while (!temp_el.classList.contains('single-page')) { if (temp_el.classList.contains('ivu-table-row')) { boo = false; break; } else { temp_el = temp_el.parentElement; } } // 如果确定不是点击的不是表格行 (Table 组件生成的 ivu-table-row) if (boo) { if (this.drawer_list.length && this.drawer_list.some(item => item.visible)) { let temp = this.drawer_list.slice().reverse(); let index = this.drawer_list.length - 1 - temp.findIndex(item => item.visible); this.drawer_list[index].visible = false; setTimeout(() => { this.drawer_list.splice(index, 1); }, 400); } else { this.$emit('update:visible', false); } } } }, watch: { id(val) { // 外部 id 不传入时, 把详情页关掉 if (!val) { this.$emit('update:visible', false); } // id 发生变化时, 把 drawer_list 清空(比如我点击第一行查看该行详情, 不管我后续在详情页里又点了多少层 Drawer, 我点击第二行时, id 发生变化, 清空后续的 Drawer , 只保留最外层 Drawer 显示新的表格行详情信息) if (this.drawer_list.length) { this.drawer_list.forEach(item => item.visible = false); setTimeout(() => { this.drawer_list = []; }, 400); } }, visible: { handler(val) { // 切换最外层 Drawer 显隐 this.toggle_drawer(val, 'main'); // 加上点击清除 Drawer 方法 if (document.getElementsByClassName('single-page')[0]) { if (val) { document.getElementsByClassName('single-page')[0].addEventListener('click', this.click_blank); } else { document.getElementsByClassName('single-page')[0].removeEventListener('click', this.click_blank); } } } } } } </script>
如果发现 Select 高度消失或者选项不显示, 看看是不是 placeholder 为空或者没写 label
自定义滚动条样式后, 若水平和数值滚动条宽度不一致, 可能会有样式上的错误, 原因是 Table 组件只计算一边(忘了哪边了)的宽度, 随后拿这个宽度来调整宽高.
自己定制 iview 框架, 找到地方(Table.vue 里面的 scrollBarWidth 之类的)修改一路"跳转到定义", 修改成各按各的来.
Table 的 columns 配置 tooltip 属性后, 鼠标不能移上去 tooltip 复制上面的文字
另外, 表格有些时候需要 render , 这时候想要兼顾 render 和 tooltip 配置, 只能自定义一个 Tooltip 组件(先按上面的修改方案改好 Table 组件了才能用这个), 把要 render 的元素放到新组件的 slot 里, 代码如下:
<!-- 基本照搬 tooltip 组件, 加上了处理上面修改的代码传递过来的事件的代码 --> <template> <Tooltip transfer :placement="placement" :content="content" :delay="delay" :controlled="controlled" :always="always" :theme="theme" :maxWidth="maxWidth" :disabled="!showTooltip" @mouseIn="handleMouseInTooltip" class="ivu-table-cell-tooltip"> <!-- 这里的 slot 方便把 render 要渲染的元素放进来 --> <span ref="content" class="ivu-table-cell-tooltip-content"><slot>{{ content }}</slot></span> <div slot="content"> <slot name="content">{{ content }}</slot> </div> </Tooltip> </template> <script> import util from '@/libs/util'; // 这里用到的 util 的方法也是复制的 tooltip 组件的 export default { name: 'tooltipAuto', props: { placement: { validator(value) { return util.oneOf(value, ['top', 'top-start', 'top-end', 'bottom', 'bottom-start', 'bottom-end', 'left', 'left-start', 'left-end', 'right', 'right-start', 'right-end']); }, default: 'bottom' }, content: { type: [String, Number], default: '' }, delay: { type: Number, default: 100 }, controlled: { // under this prop,Tooltip will not close when mouseleave type: Boolean, default: false }, always: { type: Boolean, default: false }, theme: { validator(value) { return util.oneOf(value, ['dark', 'light']); }, default: 'dark' }, maxWidth: { type: [String, Number], default: 300 } }, data() { return { showTooltip: false, // 鼠标滑过overflow文本时,再检查是否需要显示 }; }, methods: { handleTooltipIn () { const $content = this.$refs.content; this.showTooltip = $content.scrollWidth > $content.offsetWidth; }, handleTooltipOut () { this.showTooltip = false; }, // 新增的事件处理方法 handleMouseInTooltip(boo) { if (boo) { this.handleTooltipIn(); } else { this.handleTooltipOut(); } } } }; </script>
复杂筛选的拼合
@click.native
, 以及如何避免触发默认事件原因
内部 transfer 了, 点击其浮层, 就相当于点击 body 中其他元素, 所以会关闭 Poptip
解决方案
简单行为, 如: Poptip 里又有一个 transfer 的 Poptip, 点击其内部, 最外的 Poptip 消失
// 在内部 Poptip 的 on-popper-show 里加上
this.refs.poptip.handleTransferClick();
// 或
this.refs.poptip.disableCloseUnderTransfer = true;
// 总之, 就是修改外部 Poptip 的 disableCloseUnderTransfer 值
复杂行为, 如: Poptip 里有 DatePicker 组件, 点击 DatePicker 内部进行选择
// 因为 DatePicker 选择时间/日期可能会点击多次,
// 所以像上面一样在类似 on-popper-show 的 on-visible-change 只修改一次 disableCloseUnderTransfer 是没用的,
// 而且 DatePicker 本身也不提供监测内部点击事件的 events ,
// 那么, 就只能在 DatePicker 的组件选择面板上绑定原生 click 事件, 监控到点击事件后更改 disableCloseUnderTransfer
// refName 是 DatePicker 上的 ref 属性, 如果是在循环里还要加上 index
bind_onclick_for_pickerPanel(refName, index) {
if (this.$refs[refName] && this.$refs[refName][index]) {
this.$refs[refName][index].$refs.pickerPanel.$el.onclick = () => {
this.$emit('prevent_close_popper'); // 外部传入的修改 disableCloseUnderTransfer 的方法
}
}
},
@on-visible-change="bind_onclick_for_pickerPanel('dateRange', index)"
不定制 iview 组件的理由:
本来是想着干脆改了 Poptip 组件, 弄一个类似 disableCloseUnderTransfer 的 prop 在 Poptip handleClose 时阻止其关闭, 但是这样一来, 正常点击空白页面, 也不能关闭 Poptip 了, 所以还是一个个地在内部 transfer 的组件上加上修改 disableCloseUnderTransfer 的相应代码
背景:
之前已有了单独的表格筛选组件, 公司要求直接对表格顶部进行筛选, 方便用户操作
思路:
使用 element 表格 tableColumn 的 renderHeader 属性, 其内部绑定全局注册的筛选框组件, 传 column 的值给筛选框组件, 筛选框组件根据条件渲染不同的下拉框
代码实现:
renderHeader_filter 函数
// 返回字段筛选渲染函数 renderHeader_filter(colIndex) { return h => { // 获取字段属性 const column = this.allColumns[colIndex]; /* DOM */ // 标题 const title = h('div', { attrs: { title: column.title }, class: 'table-filter-title' }, column.title); // 排序组件, 显示当前字段是升序还是降序 let order_column = column.key; // 以下结构参照 element 表格排序的样式, Icon 为引入的 iview Icon 组件 const sortIcons = h('span', { class: { 'table-sort-icon-container': true } }, [ h('Icon', { class: { 'icon-custom-on': true, 'icon-custom-on-active': this.order_column === order_column && this.order_type === 'asc' }, props: { type: 'md-arrow-dropup' } }), h('Icon', { class: { 'icon-custom-on': true, 'icon-custom-on-active': this.order_column === order_column && this.order_type === 'desc' }, props: { type: 'md-arrow-dropdown' } }) ]); // 筛选框组件 const filterPop = h('FilterBox', { props: Object.assign({ // 是否筛选中 filtering: this.allColumns[colIndex].filtering, // 字段 key columnKey: column.key, // 字段排序升序降序 order_type: this.order_type, // 当前排序中的字段 order_column: this.order_column, // 字段类型(文本, 多选, 单选) columnType: this.columnType }, this.config_tableFilter[column.key]), // config_tableFilter 为字段筛选相关配置, 格式如下 /* { nickName: { sort: true, // 是否允许排序 filter: true, // 是否允许筛选 filterType: 'string', // 筛选类型(文本, 多选, 单选...) query: '', // 筛选值 allList: null // 所有可能的筛选条件 } } */ on: { // 触发筛选 updateList: () => { const temp = this.config_tableFilter[column.key]; if (temp) { /* 处理筛选 */ // 判断是否有值, 调整筛选条件对象 const con = column.key if (temp.query.length) { // 根据字段筛选类型的不同进行不同处理 switch (temp.filterType) { case 'string': // 根据后端要求的格式, 处理筛选条件, 赋值到筛选条件对象中 this.$set(this.sqlList_tableFilter, con, { $regex: temp.query }); break; // ... } } else { // 无筛选值, 删除筛选条件 this.$delete(this.sqlList_tableFilter, con); } /* 刷新列表 */ this.refreshData_filter(); } }, // sync 'update:query': (val) => { // 更新 query 数据 if (this.config_tableFilter[column.key]) { this.$set(this.config_tableFilter[column.key], 'query', val); } }, 'update:order_column': (val) => { this.order_column = val; }, 'update:order_type': (val) => { this.order_type = val; } } }); /* 返回 DOM */ return h('div', { class: 'table-filter-container' }, [ title, (this.order_column === order_column && this.order_type) ? sortIcons : undefined, filterPop ]); }; }
筛选组件
// 1. 全局注册筛选组件, 便于 render 函数引用 import FilterBox from './filter-box.vue'; Vue.component('FilterBox', FilterBox); // filter-box.vue <template> <el-poppover :width="width_popper" popper-class="filter-popper" ref="poptip" @hide="hide" @show="show" > <!-- 按钮, Poptip 触发器(默认隐藏, 鼠标 hover 时显示) --> <Icon slot="reference" custom="custom-table-head-filter" :color="filtering ? '#2d8cf0' : undefined" @mouseenter.native="hover_poptip_icon" @click.stop class="down-icon" ref="reference" ></Icon> <!-- Poptip 内容 --> <div v-if="!isHidden" style="font-weight: 400"> <!-- 升序降序 --> <ul v-if="sort" class="filter-seq"> <li @click="sortBy('asc')"> <Icon custom="custom-ascending"></Icon> <span>升序</span> </li> <li @click="sortBy('desc')"> <Icon custom="custom-descending"></Icon> <span>降序</span> </li> </ul> <!-- 内容筛选 --> <div v-if="filter" class="filter-content clearfix"> <!-- 文本型 --> <template v-else-if="filterType === 'string'"> <el-input v-model="keywords" @keyup.enter.native="confirm" /> </template> <!-- 其他略 --> <!-- 选项型 --> <!-- 单选型 --> <!-- 数字型 --> <!-- 国家地区型 --> <!-- 日期时间型 --> <!-- 用户型 --> <!-- 树形筛选 --> <!-- 底部按钮 --> <div class="search-btns"> <el-button @click="cancel" type="minor" size="small">取消</el-button> <el-button type="success" size="small" @click="confirm" :class="class_headerscreening" >确认 </el-button> </div> </div> </div> </el-poppover> </template> export default { name: 'filter_box', props: [ 'columnKey', // 当前 column.key 'sort', // 是否允许排序 'filter', // 是否允许筛选 'filterType', // 筛选类型 'query', // 筛选值 'filtering', // 是否正在筛选 'order_column', // 表格当前的筛选字段在数据库中的字段名 'order_type', // 表格当前的筛选类型 // 不同类型筛选可能需要的值 'allList', // 单选多选所有可能的筛选选项 'blank', // 是否显示选项 空(未填写) 'dateOptions' // 日期类筛选的 dateOptions 配置 ], computed: { ...mapState({ enterpriseId: 'enterpriseId', userId: 'userId', userName: 'userName', fullName: 'fullName' }), // 根据类型的不同设置不同的宽度 width_popper() { switch (this.filterType) { case 'date': return 682 - (this.dateOptions ? 0 : 92); case 'users': return 320; default: return 230; } } }, data() { return { /* 筛选 */ // 所有类型通用 showing: false, // 当前面板是否显示中 // 文本型 keywords: '', // 搜索词 // 其他略 // 选择型 // 负责人型 // 单选型 // 日期型 // 国家地区型 // 用户型 }; }, methods: { /* Poptip 显隐 */ // 按钮默认隐藏, 鼠标移上去时 click 触发筛选框的显示 hover_poptip_icon() { if (!this.showing) { this.$refs.reference.$el.click(); } }, show() { this.showing = true; switch (this.filterType) { case 'string': this.keywords = this.query; break; // ... } }, hide() { this.showing = false; // 重置各种筛选条件 this.keywords = ''; this.closePoptip(); }, /* 排序 */ sortBy(type) { this.$emit('update:order_type', type); this.$emit('update:order_column', this.columnCon); this.$emit('update:storageName_orderColumn', this.columnKey); this.$emit('updateList'); this.closePoptip(); }, // 清空筛选条件 clear_filter_data() { switch (this.filterType) { case 'string': this.keywords = ''; this.$emit('update:query', this.keywords); break; // ... } }, // 取消筛选 clear_filter() { switch (this.filterType) { case 'string': case 'number': this.keywords = ''; this.$emit('update:query', this.keywords); break; // ... } this.$emit('updateList'); }, /* 确定取消 */ confirm() { switch (this.filterType) { case 'string': this.keywords = typeof this.keywords === 'string' ? this.keywords.trim() : this.keywords; this.$emit('update:query', this.keywords); break; // .... } this.$emit('updateList'); this.closePoptip(); }, cancel() { this.closePoptip(); }, closePoptip() { // 此处查看 element popover 组件源码, 找到此方法, 忘了为啥要这么弄了 this.$refs.poptip.doClose(); } }, mounted() { // 不确定当前版本是否存在这个问题, 所以上面没写 isHidden 相关逻辑 /* 页面有可能会渲染两个 thead, 一个显示出来让用户操作, 一个隐藏 所以页面中有多个相同的 filterBox, props 变化使 poptip 显示弹窗时, 第二个看不见的 poptip 也会弹出 这个属性就是为了去掉第二个显示出来的 poptip */ this.isHidden = this.$refs.container.parentElement.parentElement.parentElement.classList.contains('is-hidden'); } }; // style ....
其他
其他还有表格页对排序和筛选的各种处理, 筛选框的隐藏显示问题, 样式问题, 筛选完成后显示筛选标签在顶部方便用户查看筛选条件和快速删除筛选条件, 这里略过
pl-table el-bigdata-table
问题背景:
客户报 BUG : 多选 select (比如产品分类)选中三个或四个以上选项(每个选项只占都只有一行, 未换行)时, select 会不停抖动;
重现:
正常情况下未重现, 缩放浏览器(我是缩放到 110%)后重现
解决:
网上搜了下说可能是 tag 的问题, 于是定位;
发现之前为了让标签换行, 给 tag 加上了自定义样式 height: auto
, 去掉了 height: auto
后问题解决; 但是去掉之后又会换行, 于是加回来了, 再加了一句 min-height: 24px
, 问题解决;(24px 是不加 height: auto 时 tag 的固定高度)
通过 vue 调试工具查看, 发现触发了一个 input 事件, 其 payload 数据正常, 于是改 change
为 input
, v-model
为 :value
(在 input 时间 handler 中修改 value 值)
看同事的代码, 直接在渲染时返回 template 格式的代码, 而不是用 h , 问了下同事, 说这么弄没问题, 不过自己还没试20
背景:
需要做一个多选下拉组件, 与多选 select 类似, 但下拉框中是 tree 而不是 option
解决:
其他:
本来看 elemnet-ui 文档后, 想从它说的 popover.js 找解决方法, 但没找到, 直接看 element-ui 源码, 看了 popover.js 和 vue-popper.js , 没啥头绪, 干脆去看 select , 发现 select-dropdown 文件很简单很直观, 发现了这个 updatePopover 方法, 试了下确实可以
以后遇到类似的, 其他用了 mixin: [import 的 vue-popper] 的组件, 应该也可以如此解决
背景:
需要做一个多选下拉组件, 与多选 select 类似, 但下拉框中是 tree 而不是 option
解决:
从源码可以看出, trigger=“click” 时, 组件是在监控 document 的点击事件, 判断点击事件是否发生在 popover 之外
于是想到, 给相关弹框加上 @click.native.stop , 阻止它向上传递 click 到 document , 问题就解决了
ps: 要注意下会不会有其他问题, 比如全局在追踪一些按钮的点击事件, 不传递了会不会追踪失败这之类的, 要结合具体功能看
背景:
请求接口获取会话列表, 每个会话有一个"客户最后回复时间", 是以"距今时间差"形式返回毫秒数, 前端定时每秒给时间差 + 1000ms , 更新这个时间差, 做到实时显示哪些会话是活跃会话(距今 x 小时内), 是的话就加上相关样式
结果给会话头像加上 :class="getMethod(lastReplyTime)"
样式计算逻辑之后, 与头像无关的"会话最后一条消息"等其他数据, 也会每秒就重新计算一次
解决:
不知道原因
思路上, 认为这是会话数据对象被更新导致重新计算, 于是, 转变为"给 class 绑上不会经常变的变量"
改为: 在定时器中, 执行计算逻辑, 结果单独存放到一个对象中, 各个会话以其唯一 id 从此对象中取 class :class="classObj[item.id]"
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。