elementui-表格打印

更多文章打印需要用到的组件为 print-js 普通表格打印一般的表格打印直接仿照组件提供的例子就可以了。 printJS({ printable: id, // DOM id type: 'html', scanStyles: false,})element-ui 表格打印element-ui 的表格,表面上看起来是一个表格,实际上是由两个表格组成的。 表头为一个表格,表体又是个表格,这就导致了一个问题:打印的时候表体和表头错位。 另外,在表格出现滚动条的时候,也会造成错位。 解决方案我的思路是将两个表格合成一个表格,print-js 组件打印的时候,实际上是把 id 对应的 DOM 里的内容提取出来打印。所以,在传入 id 之前,可以先把表头所在的表格内容提取出来,插入到第二个表格里,从而将两个表格合并,这时候打印就不会有错位的问题了。 function printHTML(id) { const html = document.querySelector('#' + id).innerHTML // 新建一个 DOM const div = document.createElement('div') const printDOMID = 'printDOMElement' div.id = printDOMID div.innerHTML = html // 提取第一个表格的内容 即表头 const ths = div.querySelectorAll('.el-table__header-wrapper th') const ThsTextArry = [] for (let i = 0, len = ths.length; i < len; i++) { if (ths[i].innerText !== '') ThsTextArry.push(ths[i].innerText) } // 删除多余的表头 div.querySelector('.hidden-columns').remove() // 第一个表格的内容提取出来后已经没用了 删掉 div.querySelector('.el-table__header-wrapper').remove() // 将第一个表格的内容插入到第二个表格 let newHTML = '<tr>' for (let i = 0, len = ThsTextArry.length; i < len; i++) { newHTML += '<td style="text-align: center; font-weight: bold">' + ThsTextArry[i] + '</td>' } newHTML += '</tr>' div.querySelector('.el-table__body-wrapper table').insertAdjacentHTML('afterbegin', newHTML) // 将新的 DIV 添加到页面 打印后再删掉 document.querySelector('body').appendChild(div) printJS({ printable: printDOMID, type: 'html', scanStyles: false, style: generatePrintStyle() }) div.remove()}

October 17, 2019 · 1 min · jiezi

vuei18n和ElementUI国际化使用总结

项目中需要自定义切换中/英文,基于vue.js,结合vue-i18n,ElementUI,以下是使用方法。示例代码地址: https://github.com/lilywang71... ElementUI国际化链接: http://element-cn.eleme.io/#/...vue-i18n:https://github.com/kazupon/vu...安装: npm install vue-i18n vue.js+vue-i18n国际化在main.js同级建i18n文件夹,并里面建i18n.js、langs文件夹,langs文件夹下建en.js、cn.js目录如下: .├── App.vue├── assets│ └── logo.png├── components│ └── HelloWorld.vue├── i18n│ ├── i18n.js│ └── langs│ ├── cn.js│ ├── en.js│ └── index.js├── main.js└── store.js //i18n.jsimport Vue from 'vue'import VueI18n from 'vue-i18n'import messages from './langs'Vue.use(VueI18n)const i18n = new VueI18n({ locale: localStorage.lang || 'cn', messages})export default i18n//langs/index.jsimport en from './en'import cn from './cn'export default { en, cn}//en.jsconst en = { message: { 'hello': 'hello, world', }}export default en//cn.jsconst cn = { message: { 'hello': '你好,世界', }}export default cn//main.jsimport Vue from 'vue'import App from './App'import store from './store'import i18n from './i18n/i18n'Vue.config.productionTip = falsewindow.app = new Vue({ store, i18n, render: h => h(App)}).$mount('#app')接下来是在页面中使用、切换语言。 ...

October 16, 2019 · 1 min · jiezi

使用jsx配置elementui的table

背景由于表格数据比较同构,有时候列配置是动态的,此时希望使用v-for来配置column <template><el-table :data="tableData"> <el-table-column v-for="col in columns" :key="col.dataIndex" :label="col.title" :prop="col.dataIndex" :formatter="col.render" /></el-table></template><script>const PHONE_COLS = [ { title: '录音id', dataIndex: 'attach_id' }, { title: '接听客服', dataIndex: 'handle_user_info' }, { title: '呼叫类型', dataIndex: 'type_desc' }, { title: '通话时长', dataIndex: 'duration' },];const WORK_COLS = [ { title: '工单id', dataIndex: 'attach_id' }, { title: '创建客服', dataIndex: 'handle_user_info' }, { title: '创建时间', dataIndex: 'c_t' }, { title: '工单来源', dataIndex: 'source' }, { title: '工单分类', dataIndex: 'ws_type', render: (row, column, cell) => (cell || []).join(',') }];export default { data () { return { columns: PHONE_COLS, tableData: [], tableTotal: 0, } },}</script>可以看到代码比直接使用template更加简洁,另外也可以利用formatter函数处理简单的自定义字符串 ...

October 15, 2019 · 2 min · jiezi

elementui的table表格表头与内容列不对齐问题

element-ui的table表格表头与内容列不对齐问题 解决办法:增加全局样式 .el-table th.gutter{ display: table-cell!important;}

September 10, 2019 · 1 min · jiezi

ReactNative分布式热更新系统

热更新是一个非常方便的方案。在应对大量用户和深度定制的时候一定不能使用开源的方案。一般第三方的这种方案,服务器带宽较小,或者不够灵活,不能满足自己的想法。这里推荐自己实现对应的热更新方案。只需要少量代码即可支持。下面推荐一种灵活的热更新方案。包括客户端的改造、接口设计、界面开发,同时是开源的!可以自由改造。体验地址:demo 用户名密码都是:admin 基础数据的准备和实现首先第一点,一个APP如果要支持热更新,需要在打开APP(或者其他进入RN页面之前)就要判断是否需要更新bundle文件。这里就是我们实现热更新的节点。一旦需要热更新就开始下载文件,而判断的接口就是我们这次文章的核心内容。这里简单贴出安卓和ios两端的下载逻辑。 请求之前需要在head中附带上客户端的几个重要信息。客户端版本号version、客户端唯一id:clientid、客户端类型platform、客户端品牌brand。ios下载的例子 -(void)doCheckUpdate{ self.upView.viewButtonStart.hidden = YES; if ([XCUploadManager isFileExist:[XCUploadManager bundlePathUrl].path]) {//沙盒里已经有了下载好的jsbundle,以沙盒文件优先 self.oldSign = [FileHash md5HashOfFileAtPath:[XCUploadManager bundlePathUrl].path]; }else {//真机计算出的包内bundlemd5有变化,可能是压缩了,所以这里写死初始化的md5 // NSString *ipPath = [[NSBundle mainBundle] pathForResource:@"main" ofType:@"jsbundle"]; // self.oldSign = [FileHash md5HashOfFileAtPath:ipPath]; self.oldSign = projectBundleMd5; } AFHTTPSessionManager *_sharedClient = [[AFHTTPSessionManager alloc] initWithBaseURL:[NSURL URLWithString:@"http://test.com"]]; [self initAFNetClient:_sharedClient]; [_sharedClient GET:@"api/check" parameters:nil progress:nil success:^(NSURLSessionDataTask * __unused task, id JSON) { NSDictionary *dic = [JSON valueForKeyPath:@"data"]; BOOL isNeedLoadBundle = YES; if ([dic isKindOfClass:[NSDictionary class]]) { self.updateSign = [dic stringForKey:@"sign"]; self.downLoadUrl = [dic stringForKey:@"downloadUrl"]; if(self.updateSign.length && self.oldSign.length && (![self.updateSign isEqualToString:self.oldSign])) { //需要更新bundle文件了 self.upView.viewUpdate.hidden = NO; [self updateBundleNow]; isNeedLoadBundle = NO; }else { //不需要更新bundle文件,再处理跳过按钮显示逻辑 [self.upView showSkipButtonOrNot]; } } if (isNeedLoadBundle) { [self loadBundle]; } } failure:^(NSURLSessionDataTask *__unused task, NSError *error) { [self loadBundle]; }];}安卓下载的例子 ...

August 19, 2019 · 2 min · jiezi

ElementUI嵌套Form的Dialog如何重置Form

场景第一次打开页面时,先点添加按钮选中第一行先点击添加按钮,执行resetFields(),重置form,正常。再点击修改按钮,回显用户信息,正常。 第一次打开页面时,先点修改按钮选中第一行先点击修改按钮,回显用户信息,正常。再点击添加按钮,执行resetFields(),没有重置form,异常。 分析官方文档的描述是重置为初始值。在创建form对象时,使用model绑定的表单数据对象,在form对象的mounted时期记录了下来第一次使用的表单数据对象,这个表单数据对象就是初始值,每次调用resetFields()重置表单,都是使用这个初始值,所以重置并不是清空。 <el-form ref="form" :model="form" label-width="80px">... ...</el-form>解决先调用resetFields()重置表单并移除校验结果,然后将表单数据对象置空。 resetEntityForm: function () { this.$nextTick(function () { this.$refs.entityForm.resetFields(); for (var key in this.entity) { if (this.entity.hasOwnProperty(key)) { this.entity[key] = ''; } } });}参考:解决 ElementUI form表单在dialog中重置表单,无法正确重置的问题elementui-清除弹框中表单的默认值-resetFields()element-ui 表单清空 resetFields 方法清空表单中的坑

July 11, 2019 · 1 min · jiezi

elementUI-动态生成几行几列-table2

elementUI 动态生成几行几列 table现在碰到一个需求:就是根据用户选择的行列,来自动生成相应大小的 table,如下这个实现还不完善,因为数据不对,只是实现了动态的效果,仅是提供一种实现思路吧,后续我会再想想看怎么实现为好,先记录一下吧直接看代码吧<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>elementUI table 动态生成列</title> <script src="https://cdn.jsdelivr.net/npm/vue"></script> <script src="https://unpkg.com/element-ui/lib/index.js"></script> <style type="text/css"> @import url("https://unpkg.com/element-ui/lib/theme-chalk/index.css"); </style></head><body><div id="app"> <el-form inline> <!--先选择 排数--> <el-form-item label="请选择排" style="margin-left: 50px;"> <el-select style="width: 100% ;" v-model="row1" placeholder="请选择排" @change="row1Change"> <el-option v-for="item in floorNumList" :key="item.floorId" :value="item.floorId"></el-option> </el-select> </el-form-item> <!--再选择 列数--> <el-form-item label="请选择列"> <el-select style="width: 100% ;" v-model="col1" placeholder="请选择列" @change="col1Change"> <el-option v-for="item in floorNumList" :key="item.floorId" :value="item.floorId"></el-option> </el-select> </el-form-item> <el-table ref="multipleTable" :data="rowDataList1" style="width:80%; border: 2px solid red; max-height: 500px; margin-left: 30px;" highlight-current-row :cell-style="cellStyle"> <el-table-column fixed type="selection" align="center" width="50" label="列"></el-table-column><!-- <el-table-column type="index" align="center" width="50" label="索引"></el-table-column>--> <el-table-column v-for="col in colDataList1" :prop="col.id" :label="col.id" align="center" > <el-table-column prop="id" align="center" > <template slot-scope="scope"> <el-button @click="handleClick(scope.row, col.id, scope.$index)" class="el-icon-cherry" v-bind:style="{ color: activeColor}">></el-button> </template> </el-table-column> </el-table-column> </el-table> </el-form> </div></div><script> let vm = new Vue({ el: '#app', data(){ return{ floorNumList: [ {floorId: 1}, {floorId: 2}, {floorId: 3}, {floorId: 4}, {floorId: 5}, {floorId: 6}, {floorId: 7}, {floorId: 8}, {floorId: 9}, {floorId: 10} ], floorNum: '', // 第1层 默认选择的排数 和 列数 row1: 1, col1: 1, // 第2层 默认选择的排数 和 列数 row2: 1, col2: 1, // 第3层 默认选择的排数 和 列数 row3: 1, col3: 1, // 第4层 默认选择的排数 和 列数 row4: 1, col4: 1, // 第5层 默认选择的排数 和 列数 row5: 1, col5: 1, activeColor: 'green', colPos: '', rowPos: '', rowDataList1: [{ // 默认给一个对象,即默认状态是 1行数据 id: Math.ceil(Math.random()*100) }], colDataList1: [ {id: '1'} ], } }, methods:{ col1Change(){ // 每触发一次,清空数据 this.colDataList1 = [{id: '1'}]; // 取得 选中的第一层的第一排的数值 let len = this.col1; if(len > 1){ for (let i = 2; i <= len; i++){ this.colDataList1.push({id: i + ''}); } return this.colDataList1; }else{ return this.colDataList1; } }, row1Change(){ // 每触发一次,清空数据 this.rowDataList1 = [{ id: Math.ceil(Math.random()*100)}]; let len = this.row1; if (len > 1){ for (let i = 2; i <= len ; i++){ this.rowDataList1.push({id: Math.ceil(Math.random()*100) + i}); } return this.rowDataList1; }else { return this.rowDataList1; } }, handleClick(row, col, index) { // console.log(JSON.stringify(row)); // console.log(JSON.stringify(col)); // console.log("点击的cell 行数: " + JSON.stringify(index)); // index 是 行数,0 表示第一行,从 0 开始 // 一点击获取 行纵坐标 this.colPos = col; this.rowPos = index; }, cellStyle({row, column, rowIndex, columnIndex}){ // console.log(JSON.stringify(row)) // console.log(JSON.stringify(column)) // console.log("要渲染的行数: " + JSON.stringify(rowIndex)) // console.log(JSON.stringify(columnIndex)) if(rowIndex == 0 && columnIndex == 0){ return ''; }else { if(rowIndex == this.rowPos && columnIndex == this.colPos){ //指定坐标 return 'background: pink'; }else{ return ''; } } }, } });</script></body></html>为了方便大家直接使用理解,我这里使用的脚本等都是在线链接,确保大家直接 down 下来就能运行处效果的。效果图 ...

July 10, 2019 · 2 min · jiezi

elementUI-动态生成几行几列-table

elementUI 动态生成几行几列 table现在碰到一个需求:就是根据用户选择的行列,来自动生成相应大小的 table,如下这个实现还不完善,因为数据不对,只是实现了动态的效果,仅是提供一种实现思路吧,后续我会再想想看怎么实现为好,先记录一下吧直接看代码吧<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>elementUI table 动态生成列</title> <script src="https://cdn.jsdelivr.net/npm/vue"></script> <script src="https://unpkg.com/element-ui/lib/index.js"></script> <style type="text/css"> @import url("https://unpkg.com/element-ui/lib/theme-chalk/index.css"); </style></head><body><div id="app"> <el-form inline> <!--先选择 排数--> <el-form-item label="请选择排" style="margin-left: 50px;"> <el-select style="width: 100% ;" v-model="row1" placeholder="请选择排" @change="row1Change"> <el-option v-for="item in floorNumList" :key="item.floorId" :value="item.floorId"></el-option> </el-select> </el-form-item> <!--再选择 列数--> <el-form-item label="请选择列"> <el-select style="width: 100% ;" v-model="col1" placeholder="请选择列" @change="col1Change"> <el-option v-for="item in floorNumList" :key="item.floorId" :value="item.floorId"></el-option> </el-select> </el-form-item> <el-table ref="multipleTable" :data="rowDataList1" style="width:80%; border: 2px solid red; max-height: 500px; margin-left: 30px;" highlight-current-row> <el-table-column fixed type="selection" align="center" width="50" label="列"></el-table-column> <el-table-column type="index" align="center" width="50" label="索引"></el-table-column> <el-table-column v-for="col in colDataList1" :prop="col.id" :label="col.id" align="center" > <el-table-column prop="id" align="center" ></el-table-column> </el-table-column> </el-table> </el-form> </div></div><script> let vm = new Vue({ el: '#app', data(){ return{ floorNumList: [ {floorId: 1}, {floorId: 2}, {floorId: 3}, {floorId: 4}, {floorId: 5}, {floorId: 6}, {floorId: 7}, {floorId: 8}, {floorId: 9}, {floorId: 10} ], floorNum: '', // 第1层 默认选择的排数 和 列数 row1: 1, col1: 1, // 第2层 默认选择的排数 和 列数 row2: 1, col2: 1, // 第3层 默认选择的排数 和 列数 row3: 1, col3: 1, // 第4层 默认选择的排数 和 列数 row4: 1, col4: 1, // 第5层 默认选择的排数 和 列数 row5: 1, col5: 1, rowDataList1: [{ // 默认给一个对象,即默认状态是 1行数据 id: Math.ceil(Math.random()*100) }], colDataList1: [ {id: '1列'} ], } }, methods:{ col1Change(){ // 每触发一次,清空数据 this.colDataList1 = [{id: '1列'}]; // 取得 选中的第一层的第一排的数值 let len = this.col1; if(len > 1){ for (let i = 2; i <= len; i++){ this.colDataList1.push({id: i + '列'}); } return this.colDataList1; }else{ return this.colDataList1; } }, row1Change(){ // 每触发一次,清空数据 this.rowDataList1 = [{ id: Math.ceil(Math.random()*100)}]; let len = this.row1; if (len > 1){ for (let i = 2; i <= len ; i++){ this.rowDataList1.push({id: Math.ceil(Math.random()*100) + i}); } return this.rowDataList1; }else { return this.rowDataList1; } }, } });</script></body></html>为了方便大家直接使用理解,我这里使用的脚本等都是在线链接,确保大家直接 down 下来就能运行处效果的。效果图 ...

July 10, 2019 · 2 min · jiezi

elementUI系列elementUI中表格的筛选功能和排序功能同时使用

一、前言最近在写项目的时候,发现自己对elementUI的表格,自己想吐槽一下table的组件,可能是没有理解透文档中的说明使用。 二、需求要做成这样的: 然后左侧还有一个类似于导航的切换,也可以是tab切换。左侧是查数据库的,右侧也是查数据库的。 三、分析写代码前分析一下: (1)我打算左侧使用tab切换。原因:这个数据之间有关联,分隔内容。用导航有点大材小用。 (2)右侧的部分打算封装成组件,因为每一个tab的内容和数据很相似。这样也是前端的组件高复用,模块化开发,而且左侧导航是后端控制的,多少条不知道。 四、封装组件,父组件传值给子组件子组件和父组件之间的通信,记住父传子:props属性,子传父:this.$emit()方法,兄弟传兄弟,用vuex。 组件的布局分成三部分:介绍,表格,分页。 1、子组件 父组件给子组件传值,在组件上使用props属性,接受父组件传递的值。 2、父组件那么父组件是怎么传的呢?看下图 父组件的布局 这个是父组件,红框内容就是父组件传值方式,父组件在自己本地需要定义后边的parentData,后边的tableData。前边是传给子组件的接受名。 父组件的data 五、封装组件,子组件传值给父组件为啥子组件还需要给父组件传值,因为子组件的操作改变了数据,对父组件造成了影响。 咱么这个需求中,也是可以选择子组件不给父组件传值的,直接改变子组件中改变父组件传递过来的值。我们就当复习一下父子组件传值。 比如分页这个功能,子组件需要将第几页传给父组件。 子组件的pageChange方法: pageChange(val) { this.middleData.pageNum = val; this.$emit('childData', this.middleData) },那父组件是怎么接受这个页码的呢? childData (val) { this.paramsData.orderBy = val.orderBy; this.paramsData.dir = val.dir; this.paramsData.dimension = val.dimension; this.queryData(); }上述childData方法中父组件方法,参数就是子组件传递的数据。 六、子组件的表格的【筛选功能】和【排序功能】1、我们先看【排序功能】看一下官方文档: 我是需要后端配合使用,远程排序。所以必须设置custom。然后配合sort-change方法。不然就是前端排序 我们来看一下sortChang方法: sortChange (column, event) { this.middleData.orderBy = column.prop; if(column.order =='ascending'){this.middleData.dir = 'asc'}else if(column.order =='descending'){this.middleData.dir = 'desc'}else{this.middleData.dir = ''}; this.$emit('childData', this.middleData) },将查询的参数传递给父组件。 ...

July 5, 2019 · 1 min · jiezi

elementUI-table-表格-column-样式

前言好长时间没更新了,最近这段时间状态不佳,感觉整个人的精神状态不太好,总是回想起以前的某个人。。。 好了,废话不多说了;今天我遇到一个有趣的 如题 table 表格的样式显示问题,然后我 google 了半天,发现别人写的都是 table 表头等这些不着边际的问题,与我的目的相差甚远,然后只好自己思考着怎么写了,下面看具体要求吧 功能截图如下: 要达到的要求:根据状态那一列的数据(未到账,已取消,已到账)显示相应的颜色标识,并且只有在 状态那一列是 未到账 状态时,操作才会显示 到账,最终显示呢,如上图所示。 实现其实,实现相对来说很简单,亮点在于 elementui 对于 table 每一行数据的处理上实现代码如下: <!-- table 数据 start --> <el-table ref="multipleTable" :data="orderList" v-loading="listLoading" element-loading-text="拼命加载中" border style="width:100%;" height="536" highlight-current-row @selection-change="chooseSelectionChange" :default-sort = "{prop: 'order_time', order: 'descending'}"> <el-table-column fixed type="selection" align="center" width="50"></el-table-column> <el-table-column prop="order_time" align="center" label="下单时间" show-overflow-tooltip min-width="140" sortable></el-table-column> <el-table-column prop="type_name" align="center" label="状态" show-overflow-tooltip min-width="140"> <template slot-scope="scope"> <span v-if="scope.row.type_name == '未到账'" style="color:black;">未到账</span> <span v-if="scope.row.type_name == '已到账'" style="color:rgb(84, 195, 29);">已到账</span> <span v-if="scope.row.type_name == '已取消'" style="color:red;">已取消</span> </template> </el-table-column> <el-table-column fixed="right" align="center" label="操作" show-overflow-tooltip min-width="140"> <template slot-scope="scope"> <span class="option-item option-edit" @click.stop="tableOption('到账', scope.$index, scope.row)" style="color: #54c31d;" v-show="getButtonPermit('sys:user:operate')" v-if="scope.row.type_name == '未到账'">到账</span> </template> </el-table-column> </el-table> <!-- table 数据 end -->解释: ...

July 2, 2019 · 1 min · jiezi

ElementUI-Collapse折叠面板更改展开icon位置

icon在右侧我们先来看官网中展开icon的位置,如下图 我们会因为ui的设计,将icon放置在文本的左侧,我们先看下element是否在该组件定义了相关的属性,找了一遍发现并没有。那么我们如果实现如下图中的布局呢? 接着我们通过观察element该组件中title的样式 发现了一种样式属性,flex。有关flex的简单教程请参考阮一峰老师的这一篇flex教程 我们发现可以通过flex中的项目属性,对文本和icon进行排序 常规为了避免修改到通用的组件(我们可能只是修改部分的折叠面板collapse样式),需要添加一个外部的class,这里的事例就不添加了 // html <script src="//unpkg.com/vue/dist/vue.js"></script> <script src="//unpkg.com/element-ui@2.10.0/lib/index.js"></script> <div id="app"> <el-collapse v-model="activeNames" @change="handleChange"> <el-collapse-item name="1"> <span class="collapse-title" slot="title">一致性 Consistency</span> <div>与现实生活一致:与现实生活的流程、逻辑保持一致,遵循用户习惯的语言和概念;</div> <div>在界面中一致:所有的元素和结构需保持一致,比如:设计样式、图标和文本、元素的位置等。</div> </el-collapse-item> </el-collapse> </div> // css @import url("//unpkg.com/element-ui@2.10.0/lib/theme-chalk/index.css"); .collapse-title { flex: 1 0 90%; order: 1; } .el-collapse-item__header { flex: 1 0 auto; order: -1; } // js var Main = { data() { return { activeNames: ['1'] }; }, methods: { handleChange(val) { console.log(val); } } } var Ctor = Vue.extend(Main) new Ctor().$mount('#app')这里的文本需要用到slot,因为我们需要给文本添加class,这里类名为collapse-title。具体可以通过粘贴到codepen查看效果。 ...

June 29, 2019 · 1 min · jiezi

从设计页面逻辑走一走权限管理的完整流程

本文主要想对前端权限管理功能实现做一个分享,所以并不会对后台管理的框架结构做太详细介绍,如果有朋友对其他有兴趣可以留言。 基本设计和分析前端 vue + elementui服务端: node + mysql + nginx主要功能打开思否页面,根据页面的功能点,设计出相关的数据表,和管理系统需要的相关页面。计划后台管理需要完成的功能: 权限管理(菜单权限到数据权限) -- 已完成工作流 (问答和文章在某个条件内,提交需要走流程)-- 未完成socket (对用户点赞,评论,系统通知等消息进行实时推送)-- 未完成文件管理(将页面需要用到的文件上传管理,其他页面都统一访问文件库资源)-- 已完成基本业务 (业务页面)-- 部分完成模块相关介绍模块功能页面编码描述登录登录login菜单中不显示401401401角色无访问权限时进入这个页面404404404访问菜单不存在时进入这个页面首页首页home 运维中心 opsCenter -问答管理questionMan -专栏管理blogMan -文章管理articleMan -讲堂管理liveMan -活动管理activityMan -广告位advertising 工作流 workflow -流程设计processDesign -业务管理businessMan -已办事项finishedItems -未办事项unfinishedItems 文件库 library -图片管理imgMan -文件管理fileMan 论坛配置 bbsConfig -轮播carousel -技术频道techSquare -通知notices -标签类型管理tagTypeMan -标签管理tagMan 系统管理 sysMan -用户管理userMan -角色管理roleMan -菜单管理menuMan -区域管理areaMan -图表配置chartConfig -系统日志log 代码结构├── admin // 打包产出文件├── node_module // npm加载所需的项目依赖模块├── public // 静态入口├── src // 源代码│   ├── api // 所有请求│   ├── assets // 主题 字体 图片等静态资源│   ├── common // 全局公用配置│ │ ├── config // 配置全局路由权限和错误捕获│ │ ├── mixin // 一些vue公用的mixin│ │ ├── js // 编写公有的方法│ │ └── style // 编写公有的样式│   ├── components // 全局公用组件│   ├── directive // 自定义指令│   ├── router // 路由│   ├── store // 全局 store管理│   ├── views // view│   ├── App.vue // 入口页面│   └── main.js // 入口 加载组件 初始化等├── static // 第三方不打包资源├── .babelrc // babel-loader 配置├── eslintrc.js // eslint 配置项├── .gitignore // git 忽略项├── vue.config.js // vue-cli@3.0+ 配置文件└── package.json // package.json权限设计进入正文,关于权限设计,围绕的是前端页面,但是会将前端和后端的逻辑都讲出来。 ...

June 27, 2019 · 3 min · jiezi

vue项目使用百度富文本UEdtior

一、下载UEditor我下载的是1.4.3.3 utf-8 jsp 版本 下载链接:https://ueditor.baidu.com/web... 将下载好的文件解压到 /static/urditor 目录,如图: 二、创建组件 UEditor组件内容如下 <template> <div class="ueditor" ref="ueditor"> <script :id="id" type="text/plain"></script> </div></template><script>import '../../../static/ueditor/ueditor.config.js'import '../../../static/ueditor/ueditor.all.min.js'import '../../../static/ueditor/lang/zh-cn/zh-cn.js'import '../../../static/ueditor/ueditor.parse.min.js'import { baseURL } from '@/config/const'import { getToken } from '@/config/auth'export default { name: 'UEditor', data () { return { editor: null, flag: true, baseURL: baseURL } }, props: { value: {//文本内容 type: String }, config: {//单独设置 type: Object, default: ()=> { return { initialFrameWidth: null, initialFrameHeight: 350, UEDITOR_HOME_URL: 'static/ueditor/' } } }, id: { type: String, default: ()=> { return 'editor' + new Date().getTime(); } } }, computed: { DefaultConfig() { //默认设置 let obj = this.config; let serverUrl = this.baseURL + '/file-service/v1/ueditorUpload?' + 'token=' + getToken(); //服务器地址 return { serverUrl, ...obj } } }, created() { }, mounted() { this.initUEditor() }, watch: { 'value': function (val) { if(this.flag) { this.editor.setContent(val) } this.flag = true } }, methods: { initUEditor() { this.$nextTick(() => { this.editor = UE.getEditor(this.id, this.DefaultConfig); // 初始化UE this.editor.addListener("ready", () => { if (this.value == null) { this.editor.setContent(''); } else { this.editor.setContent(this.value); } }); this.editor.addListener("contentChange", () => { //监听内容变化 this.getUEContent(); }) }) }, getUEContent() { // 获取内容方法 this.flag = false; let content = this.editor.getContent(); // content = content.replace(/<p([\s\S]*?)<\/p>/g, "<div$1</div>"); // 将默认p标签设置为div标签 this.$emit("getUEContent", content) } }, destroyed() {//退出后销毁 this.editor.destroy(); }}</script><style scoped lang="scss"> .ueditor { // 防止外部样式影响变形 line-height:normal; }</style>三、组件的使用......<el-form-item label="通知内容" prop="content"> <div> <UEditor :value="form.content" @getUEContent="getUEContent"></UEditor> </div></el-form-item>......getUEContent(value) { // 勉强实现v-model效果 this.form.content = value;},......使用方法如上,想实现v-model的效果,没有实现,欢迎补充一下 ...

June 26, 2019 · 2 min · jiezi

vueelementUI-复杂表单的验证数据提交方案

背景当我们在做后台管理系统时,经常会遇到非常复杂的表单: 表单项非常多在各种表单类型下,显示不同的表单项在某些条件下,某些表单项会关闭验证每个表单项还会有其他自定义逻辑,比如输入框可以插入模板变量、输入字符数量显示、图片上传并显示、富文本。。。在这种错综复杂的情况下,完成表单的验证和提交可以查看具体例子:例子中省略了很多琐碎的功能,只保留整体的复杂表单框架,用于展示解决方案方案1: 在一个vue文件中所有的表单项显示隐藏、验证、数据获取、提交、自定义等逻辑放在一起 根据表单类型,使用v-if/v-show处理表单项显示隐藏在elementui自定义验证中,根据表单类型,判断表单项是否验证根据表单类型,获取不同的数据,并提交到不同的接口其余所有的自定义逻辑缺点乱乱还是乱一个vue文件,轻轻松松上2000行在我尝试加入一种新的表单类型时,我发现我已经无。从。下。手。方案2:分离组件其实很容易想到根据不同的表单类型,分离出多个相应类型的子表单。但我在实践时还是遇到了很多问题:父子表单验证、整体提交数据的获取等等,并总结出一套解决方案:1. 子组件所有的子组件中都需要包含两个方法validate、getData供父组件调用。 (1) validate方法用于验证本身组件的表单项,并返回一个promise对象 vaildate() { // 返回`elementUI`表单验证的结果(为`promise`对象) return this.$refs["ruleForm"].validate();}, (2) getData方法提供子组件中的数据 getData() { // 返回子组件的form return this.ruleForm;},2. 父组件(1) 策略模式使用策略模式存储并获取子表单的ref(用于获取子表单的方法)和提交函数 。省略了大量的if-else判断。 data:{ // type和ref名称的映射 typeRefMap: { 1: "message", 2: "mail", 3: "apppush" }, // type和提交函数的映射。不同类型,接口可能不同 typeSubmitMap: { 1: data => alert(`短信模板创建成功${JSON.stringify(data)}`), 2: data => alert(`邮件模板创建成功${JSON.stringify(data)}`), 3: data => alert(`push模板创建成功${JSON.stringify(data)}`) },}(2) submit方法用于父子组件表单验证、获取整体数据、调用当前类型提交函数提交数据 因为elementUI表单验证的validate方法可以返回promise结果,可以利用promise的特性来处理父子表单的验证。比如then函数可以返回另一个promise对象、catch可以获取它以上所有then的reject、Promise.all。父表单验证通过才会验证子表单,存在先后顺序 // 父表单验证通过才会验证子表单,存在先后顺序submitForm() { const templateType = this.typeRefMap[this.indexForm.type]; this.$refs["indexForm"] .validate() .then(res => { // 父表单验证成功后,验证子表单 return this.$refs[templateType].vaildate(); }) .then(res => { // 全部验证通过 // 获取整体数据 const reqData = { // 获取子组件数据 ...this.$refs[templateType].getData(), ...this.indexForm }; // 获取当前表单类型的提交函数,并提交 this.typeSubmitMap[this.indexForm.type](reqData); }) .catch(err => { console.log(err); });},父表单,子表单一起验证 ...

June 25, 2019 · 1 min · jiezi

Vue-Element-vuequilleditor-实现源码编辑自定义图片上传和汉化

集百家之长,看了很多博客再结合自身情况,写了这个小组件功能,仅供参考。实现源码编辑vue-quill-editor的配置文件: // toolbar工具栏的工具选项(默认展示全部)const toolOptions = [ // 加粗 斜体 下划线 删除线 ['bold', 'italic', 'underline', 'strike'], // 加粗 斜体 下划线 删除线 ['blockquote', 'code-block'], // 1、2 级标题 [{header: 1}, {header: 2}], // 有序、无序列表 [{list: 'ordered'}, {list: 'bullet'}], // 上标/下标 [{script: 'sub'}, {script: 'super'}], // 缩进 [{indent: '-1'}, {indent: '+1'}], // 文本方向 [{direction: 'rtl'}], // 字体大小 [{size: ['small', false, 'large', 'huge']}], // 标题 [{header: [1, 2, 3, 4, 5, 6, false]}], // 字体颜色、字体背景颜色 [{color: []}, {background: []}], // 字体种类 [{font: []}], // 对齐方式 [{align: []}], [{clean: '源码编辑'}], // 这是自己加的 // 链接、图片、视频 ['link', 'image'], // 新添加的工具 ['sourceEditor']];const handlers = { shadeBox: null, // 添加工具方法 sourceEditor: function () { // alert('我新添加的工具方法'); const container = this.container; const firstChild = container.nextElementSibling.firstChild;// 在第一次点击源码编辑的时候,会在整个工具条上加一个div,层级比工具条高,再次点击工具条任意位置,就会退出源码编辑。可以在下面cssText里面加个背景颜色看看效果。 if (!this.shadeBox) { let shadeBox = this.shadeBox = document.createElement('div'); shadeBox.style.cssText = 'position:absolute; top:0; left:0; width:100%; height:100%; cursor:pointer'; container.style.position = 'relative'; container.appendChild(shadeBox); firstChild.innerText = firstChild.innerHTML; shadeBox.addEventListener('click', function () { this.style.display = 'none'; firstChild.innerHTML = firstChild.innerText.trim(); }, false); } else { this.shadeBox.style.display = 'block'; firstChild.innerText = firstChild.innerHTML; } }};export default { placeholder: '', // 主题 theme: 'snow', modules: { toolbar: { // 工具栏选项 container: toolOptions, // 事件重写 handlers: handlers } }, // 在使用的页面中初始化按钮样式 initButton: function () { // 样式随便改 const sourceEditorButton = document.querySelector('.ql-sourceEditor'); sourceEditorButton.style.cssText = 'font-size:18px'; // 加了elementui的icon sourceEditorButton.classList.add('el-icon-edit-outline'); // 鼠标放上去显示的提示文字 sourceEditorButton.title = '源码编辑'; }};工具名,工具方法名,类名:这里要注意的是:工具名和工具方法名是一样的,并且生成的button工具拥有ql-工具名的类名。例如上面代码中,我的工具名是sourceEditor,我的方法名也是sourceEditor,而生成的button工具的类名就是ql-sourceEditor了。 ...

June 24, 2019 · 4 min · jiezi

vue框架为element组件赋初始值以后无法更改值得问题

情况描述:组件未加载时已有初始值,mounted里面加载数据,赋值,渲染以后,组件无法更改内容data里面已经有这个表单对象的初始值但还是无法修改,之前有过一次,没有给表单绑定对象,所以赋值以后无法修改,这次还是无法修改。后来找了好久才知道,初始值要一直存在比如说:data里面有一个对象 addForm: { bcorpID: '', workerLists: [], payBankCardNumberidx: -1 },然后如果在mounted里面请求数据在获取返回的数据以后直接进行赋值但是如果返回的数据里面没有payBankCardNumberidx这个字段的话就会造成赋值以后无法修改组件的值,即使你在为addForm赋值为返回值以后又马上为addForm加了字段payBankCardNumberidx,也是不行的。(个人理解是,vue的数据双向绑定,在你为addForm赋值返回的数据后,vue框架立刻对组件进行渲染以及数据的更新,如果大佬们看到这有其他的理解,可以在下面评论下谢谢)

June 21, 2019 · 1 min · jiezi

elementui设置下拉选择切换必填和非必填

➢ 需求默认都是必选 下拉选择的时候 选择必填,活动名称为必填,需要校验和显示* 选择非必填,活动名称不做校验,隐藏* ➢ 初始校验规则经测试,网上其他的方式都没有实现需求,动态切换rules中的required没有作用 因为按照以下的写法的话,element-ui在组件初始化后校验规则就定型了,切换也没用 rules: { name: [ { required: true, message: "请输入名称", trigger: "blur" } ], region: [ { required: true, message: "请选择类型", trigger: "blur" } ]}➢ 解决方案第一步: 去除rules中需要动态校验的字段规则 去除name rules: { region: [ { required: true, message: "请选择类型", trigger: "blur" } ]}第二步: 在字段为name的form-item上,添加required属性 下面代码isHaveTo为新字段,根据下拉框选择的值来决定是为true还是false <el-form-item label="活动名称" prop="name" :required="isHaveTo"> <el-input v-model="ruleForm.name"></el-input></el-form-item>第三步: 计算属性,新增字段isHaveTo 下拉选择框非必须是为1,其他都是必填,包括默认 computed: { isHaveTo: function() { return this.ruleForm.region === `0`; }},效果如图: ...

June 16, 2019 · 2 min · jiezi

ElementUI-select-把整个option对象作为值

大部分时候我们使用select,选中选项我们只需要他的ID值,如果同时要在其他地方展示label或者获取选中对象中的其他值怎么办。看图。关键设置有两处。 1 option中 :value="item"2 selection中 value-key="id"

June 14, 2019 · 1 min · jiezi

To-Be-Simple基于elementUI的功能扩展组件系列1之Table篇

项目地址:tbs-ve-template 前言结合日常开发,封装常用功能,提高开发效率。让程序猿兄弟姐妹们也有时间约约女票,逗逗男票,做做自己想做的事情,不要天天在办公室造轮子! 1.通用Table思路类似easy-ui的table加载方式 环境简述开发框架:基于vue-admin-templategithub:https://github.com/PanJiaChen... JS 包管理工具: Yarn安装方法:https://www.cnblogs.com/xiang... 项目 启动: 第一步:yarn install 下载所有依赖包第二步:yarn run dev 下载所有依赖包第三步:访问http://localhost:9528 项目结构说明:为了避免代码过长不易浏览,讲vue代码与js代码分开编辑。 1. 通用Table显示效果 支持类型类型:文本 | 链接 | 文档 | 图片 用法 相关属性Table 属性: 参数说明类型可选值默认值listLoading动画效果boolean—truecolumnstable 的列(column)的配置对象,更多细节请参见列(column)属性。array——uitable显示效果的配置对象,更多细节请参见列(ui)属性array——data显示数据集合,一般从远程获取数据后进行赋值array——pagetable分页配置对象,更多细节请参见列(page)属性array——Table列(Column)属性: 参数说明类型可选值默认值label列的标题文本string——field列的字段名,与data属性中的名称对应string——width列的宽度number——showtype列的类型,normal:为文本类型stringnormal、http、file、imagenormalfilter过滤器,类似easyui中formatter属性,用于格式数据string——Table样式(ui)属性: 参数说明类型可选值默认值tableHeightTable高string、number——tableWidthTable宽度string、number——Table分页(page)属性: 参数说明类型可选值默认值total总条目数number——listQuery分页参数,listQuery:{ page:当前页码,limit:每页条目数 }number——有问题反馈在使用中有任何问题,欢迎反馈给我,可以用以下联系方式跟我交流 QQ: 375766253邮件:375766253@qq.com

June 10, 2019 · 1 min · jiezi

vue2-配置elementui

1、安装element-ui npm/cnpm install element-ui -S2、main.js中配置 ...import ElementUI from 'element-ui'import 'element-ui/lib/theme-chalk/index.css' // 全局引入css样式Vue.use(ElementUI)3、在项目中使用 element组件库,直接按照组件使用就可以了。https://element.eleme.cn/#/zh-CN/component/installation注意:在初始化项目的时候,必须使用css对浏览器进行初始化,要不然会遇到很多问题,我之前遇到的坑和大家分享,避免浪费大家时间我项目中使用flex布局,发现flex: 1无效,最后找到原因就是html,body,#app高度没有设置,最后初始化100%;就好了。虽然是个小问题,但是会浪费开发时间。

June 6, 2019 · 1 min · jiezi

Laravel-58集合-vueelementadmin-踩坑记

创建 Laravel 项目按照官方文档,进行安装: composer create-project --prefer-dist laravel/laravel laravel-vue-admin 下载 vue-element-admin$ git clone https://github.com/PanJiaChen/vue-element-admin.git初始化package将 vue-element-admin 中 package.json 中的 dependencies 与devDependencies 合并到 Laravel 的 package.json 中。 版本以 vue-element-admin 的版本为准 复制文件将 vue-element-admin 中整个 src 目录下的文件复制到 laravel 项目中的 resources/backend 目录中。 安装前端依赖$ npm install 添加打包语句修改 webpack.mix.js 为: const mix = require('laravel-mix');mix.js('resources/js/app.js', 'public/js') .sass('resources/sass/app.scss', 'public/css');mix.js('resources/backend/main.js', 'public/js')添加入口文件在 resources/views 中添加 admin.blade.php 视图文件,代码如下: <!doctype html><html lang="{{ app()->getLocale() }}"><head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="csrf-token" content="{{ csrf_token() }}"> <title>title</title></head><body><div id="app"></div><script src="{{ mix('/js/main.js') }}"></script></body></html>配置路由映射在 routes/web.php 文件中添加如下路由: ...

June 5, 2019 · 3 min · jiezi

弹窗实现自制

自制手写弹窗在实际开发中,我们不可避免的需要使用到弹窗,但是我们经常总是直接使用的第三方模态框,这就导致我们自己对于弹窗的理解并不深;如果有时候需要我们手写一个简单弹窗时,你可能写不出来(不要笑,大部分都是,包括有些前端也写不出来)。原因只是我们并没有深入的了解并理解了弹窗的原理。作为一名开发者,我们一定要既知其然又知其所以然,虽然我们不是专业的前端,但是我们也要向全栈方向努力嘛????直接看代码吧<!-- created by util.you.com@gmail.com time: 2019-06-04 22:26--><!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>自写弹窗,体会原理</title> <script src="https://cdn.jsdelivr.net/npm/vue"></script> <script src="https://unpkg.com/element-ui/lib/index.js"></script> <style type="text/css"> @import url("https://unpkg.com/element-ui/lib/theme-chalk/index.css"); </style></head><body><div id="app"> <h1>自制弹窗,理解实现原理</h1> <p>首页内容</p> <p>用户列表: </p> <el-table ref="multipleTable" :data="userList" border style="width:50%;" height="200px" highlight-current-row > <el-table-column fixed type="selection" align="center" width="50"></el-table-column> <el-table-column prop="name" align="center" label="姓名" show-overflow-tooltip min-width="100"></el-table-column> <el-table-column prop="pwd" align="center" label="密码" show-overflow-tooltip min-width="100"></el-table-column> </el-table> <button @click="showDialog">显示对话框</button> <!-- 弹窗体 --> <div id="addUserDialog"> <!-- 弹窗的主体内容 --> <div class="addUser"> <h3 style="background-color: antiquewhite;padding-left: 144px;">添加用户</h3> <a href="#" id="addUserClose">关闭</a> <el-form v-model="userForm" style="width: 350px; padding-left: 50px;"> <el-form-item label="账号" > <el-input type="text" v-model="userForm.userName" placeholder="请输入账号" style="width: 220px;"></el-input> </el-form-item> <el-form-item label="密码"> <el-input type="password" v-model="userForm.userPwd" placeholder="请输入账号" style="width: 220px;"></el-input> </el-form-item> </el-form> <el-button type="success" @click="addUser" style="margin-left: 120px;">确定</el-button> <el-button type="danger" @click="cancelAddUser">取消</el-button> </div> <!-- 弹窗的背景图层 --> <div class="addUserBackground"></div> </div></div><script> let vm = new Vue({ el: '#app', data(){ return{ userName:'', userPwd: '', userList:[ {name: '小米', pwd: '123456'}, {name: '劫', pwd: '任我行'} ], userForm:[], } }, methods:{ showDialog(){ // 点击显示对话框目的: 将对话框的 display 变成 block let addUserDialog = document.getElementById('addUserDialog'); let addUserClose = document.getElementById('addUserClose'); addUserDialog.style.display = 'block'; addUserClose.onclick = function () { addUserDialog.style.display = 'none'; } }, cancelAddUser(){ // 取消或者关闭窗口,就是要讲 该窗口的 display 设置成 none 即可 let addUserDialog = document.getElementById('addUserDialog'); addUserDialog.style.display = 'none'; }, addUser(){ let self = this; self.userList.push({'name': self.userForm.userName, 'pwd': self.userForm.userPwd}); // 添加完后,将遮罩层去掉 let addUserDialog = document.getElementById('addUserDialog'); addUserDialog.style.display = 'none'; } } });</script><style type="text/css"> /*全局样式*/ body{ background-color: #00FFFF; } /** 以下这四部分样式,是制作一个弹窗最少需要的样式控制,当然你可以再在此基础上优化 addUser addUserBackground 是最关键的两部分,刚开始背下来样式控制即可,后续理解后再回过头来就明白了 **/ /*隐藏弹窗*/ #addUserDialog { display: none; } /*弹窗样式*/ .addUser { width: 400px; height: 300px; background-color: #fff; border: 1px solid #000; position: fixed; left: 50%; top: 50%; margin-left: -200px; margin-top: -150px; /*层次级别*/ z-index: 9999; } /*弹窗背景灰度*/ .addUserBackground { position: fixed; width: 100%; height: 100%; background-color: #000; left: 0; top: 0; /*透明度*/ opacity: 0.3; /*兼容ie*/ filter: alpha(opacity=30); z-index: 9990; } #addUserClose { text-decoration: none; position: absolute; right: 10px; top: 10px; }</style></body></html>为了方便大家直接使用理解,我这里使用的脚本等都是在线链接,确保大家直接 down 下来就能运行处效果的。而那四个css就是弹窗最核心,最关键的部分。大家只要理解这两点就够了:1).弹窗之所以能起到弹窗效果,是因为它的图层(z-index) 高于 背景层,所以才能跃然背景上;2).弹窗的启用与关闭,其实就是切换弹窗体的 display 即可。额外说明:在初学时我们一定不明白或者不好想到怎么设置 .addUserBackground 和 .addUser 这两个最关键的 class 属性,没关系,你只需要记住,并且经常使用,慢慢就理解了【有一句话说得好:当我们不理解一件事时,我们只需要去照做、背记即可,用的多了慢慢就理解了】声明原创手敲不易,转载请注明出处,谢谢。我是拉丁小毛,欢迎大家关注我哦,一起交流,共同进步。有问题可以邮我哦(util.you.com@gmail.com)

June 4, 2019 · 2 min · jiezi

关于-elementui-的隐藏组件elscrollbar

虽然在官方文档中没有给出这个组件,但是在源码中是有的。所以我们可以直接使用: <el-scrollbar></el-scrollbar>但是我们需要微调一下样式,两种情况的演示代码如下: 已知内容高度<div style='height:800px'><el-scrollbar class='page-component__scroll'></el-scrollbar><div><style>.page-component__scroll{ height: 100%;}.page-component__scroll .el-scrollbar__wrap { overflow-x: auto;}<style>高度由内容撑开<html> <body> <div style='height:100%'> <el-scrollbar class='page-component__scroll'></el-scrollbar> <div> </body></html><style>html,body{ height:100% overflow:hidden; /*有效防止在页面进行手动刷新时显示内置滚动条*/}.page-component__scroll{ height: 100%;}.page-component__scroll .el-scrollbar__wrap { overflow-x: auto;}<style>

May 29, 2019 · 1 min · jiezi

elementui-table-表格-大数据展示解决方案-支付万级数据展示

解决使用element-ui table时前端大数据展示的问题 解决方案:虚拟滚动 在线demo展示:https://ckang1229.github.io/e... github地址:https://github.com/ckang1229/...

May 26, 2019 · 1 min · jiezi

elementuivue批量编辑批量删除批量新增-单元格编辑数据验证的最优解决方案

使用el-table-editabled 实现el-table的各种复杂编辑场景 在线demo演示: https://ckang1229.github.io/e...github: https://github.com/ckang1229/...

May 25, 2019 · 1 min · jiezi

动态增加select框elementUI-框架

<!DOCTYPE html><html> <head> <meta charset="utf-8"> <title>演示动态增加select框(elementUI 框架)</title> <script src="//unpkg.com/vue/dist/vue.js"></script> <script src="//unpkg.com/element-ui@2.8.2/lib/index.js"></script> <style type="text/css"> @import url("//unpkg.com/element-ui@2.8.2/lib/theme-chalk/index.css"); </style> </head> <body> <div id="app"> <!-- 带多选的 select --> <el-select v-model="value5" multiple placeholder="请选择"> <el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value"> </el-option> </el-select> <!-- 带清除的 select --> <el-select v-model="value5" clearable placeholder="请选择"> <el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value"> </el-option> </el-select> <!-- 带计数的 select --> <el-select v-model="value11" multiple collapse-tags style="margin-left: 20px;" placeholder="请选择"> <el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value"> </el-option> </el-select> <br/> <hr style="height: 20px; color: aqua; background-color: aqua;" /> <el-form> <el-form-item v-for="(it, index) in list" :key="index"> <el-select v-model="oneId[index]" placeholder="请选择" @change="saveList($event, index)"> <el-option v-for="item in array" :key="item.id" :label="item.name" :value="item.id" > </el-option> </el-select> <el-button @click="addItem" type="primary">增加</el-button> <el-button @click="removeItem(it, index)" type="danger">删除</el-button> </el-form-item> <el-button type="success" @click="submit">提交</el-button> </el-form> </div> </body> <script> var vue = new Vue({ el: '#app', data() { return { options: [ {value: '选项1',label: '黄金糕'}, {value: '选项2',label: '双皮奶'}, {value: '选项3',label: '蚵仔煎'}, {value: '选项4',label: '龙须面'}, {value: '选项5',label: '北京烤鸭'}, ], value5: [], value11: [], oneId: [], array:[ {id: '1',name: '黄金糕'}, {id: '2',name: '双皮奶'}, {id: '3',name: '蚵仔煎'}, {id: '4',name: '龙须面'}, {id: '5',name: '北京烤鸭'} ], list:[{"oneId":''}], selectedList:[], // 存储每次 option 选中的 集合 } }, methods:{ addItem(){ // 1。这里为什么改变list的大小就能实现动态增加呢?因为 el-form-item 遍历的是 list,list 中的每一项都是一个 el-form-item // 也就是说因为刚开始 list:[{"oneId":''}] 中,只有一个对象,所以才会只出现一个 el-form-item // 不信可以自己在初始化时 list 中多加入几个对象进行尝试(一定要理解,这里 list 集合的大小与 el-form-item 之间的关系) // 2、第二个问题:el-form-item 是动态增加了,但是如果 el-select 那里写的是 v-model="oneId" 呢?会发生什么?结果你会发现,只要增加一项 el-form-item ,每一项绑定的值都是你所选中的那一个值.为什么呢?因为每一项的 el-option的 :value 值都绑定在 el-select 的 v-model 上,但这是一个全局唯一值,当下一个 el-form-item 产生后,它里面的 el-select 中绑定的 v-model 还是那个 oneId 的值,因此才会出现这样的问题.好了,我们既然找到了原因,那就要来解决一下了,怎么解决呢?很简单:因为我前面说了,每一个 list 的遍历对象,都是一项 el-form-item,即 el-form-item 项数是和 list 的下标(里面存的对象的索引下标)相关联的,而这个下标,在每一个 el-form-item 中肯定是不一样的,因此我们只需要将 oneId 与这个 下标(即此处的 index) 发生关系即可,因此我们这里将 oneId 声明为了一个数组,当你每选中一个 option 时,都将这个 option 的value放入 oneId[当前el-form-item项数下标] 数组中 this.list.push({"oneId": ''}); }, removeItem(it, index){ // 删除时,我们带两个参数,这个 it 可用可不用,因为我当时只是想看到删除的这个对象的信息,故而带上了; index 是 list 中该对象对应的下标,也是 el-form-item 的项数 // 根据这个 index 下标删除 list 中 的该对象 if(index != 0){ this.list.splice(index, 1); } }, saveList(event, index){ // 当每选一个 option 时,我们需要将这个 选中的 oneId 放入 对应的 list 中即可,最后都选中完后,我们只要获取这个 list,即可拿到所有的数据 this.list[index].oneId = event; }, submit(){ // 这里我们打印一下 最后的 list,确保我们都拿到数据了 alert(`最终的数据: ${JSON.stringify(this.list)}`); }, }, }); </script></html>声明我只写自己的原创内容,所有内容都是自己手敲,实践过得,这个也是我在实际项目中要用,所以自己就敲了一遍。如需转载请注明出处,谢谢

May 24, 2019 · 2 min · jiezi

vueElementUi-选择框选中之后翻页进行状态保持及默认选中

复选框<el-table:data="list"ref="multipleTable":row-key="(row)=>{ return row.classId}"@selection-change="handleSelectionChange"style="width: 100%"><el-table-column type="selection" :reserve-selection="true" ></el-table-column></el-table>//切换分页持久化选中表格 :row-key="(row)=>{ return row.classId}":reserve-selection="true"//@selection-change 会返回所有选中的数据//@select 会返回所有选中的数据及当前操作的数据 清空所有选中this.$refs.multipleTable.clearSelection();//页面中有搜索或重置时可能会用到。 默认选中this.$refs.multipleTable.toggleRowSelection(this.list[index]);//必须传表格的数据;以数组[下标]格式传递 单选框<el-table :data="list" ref="multipleTable" :row-key="(row)=>{ return row.classId}" @current-change="handleCurrentRadio" style="width: 100%"> <el-table-column width="80" v-if="radioShow"> <template slot-scope="scope"> <el-radio v-model="radio" :label="scope.row.classId">{{''}}</el-radio> </template> </el-table-column> </el-table>//@current-change="handleCurrentRadio"会返回选中的数据。可以在这个事件用return false 来阻止选中 //label 和原生的value属性一样。 利用v-model来绑定唯一值,意味着label的值为唯一的。 //{{""}}为了让单选框不显示label。

May 22, 2019 · 1 min · jiezi

vue-elementUI-table-自定义表头和行合并

最近项目中做表格比较多,对element表格的使用,只需要传递进去数据,然后写死表头即可渲染。 但现实中应用中,如果写死表头,并且每个组件中写自己的表格,不仅浪费时间而且消耗性能。这个时候需要动态渲染表头。 而官方例子都是写死表头,那么为了满足项目需求,只能自己来研究一下。 1、自定义表头代码如下,其实就是分了两部分,表格主数据是在TableData对象中,表头的数据保存在headerDatas,headerDatas.label其实就是表头的值,如果表头是“序号”,那么headerDatas.label="序号",在TableData中构建TableData[序号]= 1 这样的map对象,就可以动态渲染出想要的表格 <el-table :data="TableData" :height="tableHeight" :row-class-name="showEmergencyLine" border element-loading-spinner="el-icon-loading" element-loading-text="拼命加载中" @selection-change="handleSelectionChange" v-loading.lock="TableLoading" @header-dragend="changeHeaderWidth" > <el-table-column v-for="header in headerDatas" :prop="header.type" :key="header.label" :label="header.label" :width="header.width" :minWidth="header.minWidth" :itemname="header.mid" :align="header.align" header-align="center" > <template slot-scope="scope"> <div v-else >{{scope.row[scope.column.property]}}</div> </template> </el-table-column></el-table>2、行合并在项目中,有些表格常常会有像下面这样的需求,一行合并后面几行,那么这个怎么处理呢 官方文档中有这个方法 通过给table传入span-method方法可以实现合并行或列,方法的参数是一个对象,里面包含当前行row、当前列column、当前行号rowIndex、当前列号columnIndex四个属性。该函数可以返回一个包含两个元素的数组,第一个元素代表rowspan,第二个元素代表colspan。 也可以返回一个键名为rowspan和colspan的对象。 <el-table :data="tableData" :span-method="objectSpanMethod" highlight-current-row element-loading-spinner="el-icon-loading" element-loading-text="拼命加载中" v-loading.lock="mainTableLoading" border style="width: 100%; margin-top: 25px" ></el-table> arraySpanMethod({ row, column, rowIndex, columnIndex }) { if (rowIndex % 2 === 0) {//偶数行 if (columnIndex === 0) {//第一列 return [1, 2];//1合并一行,2占两行 } else if (columnIndex === 1) {//第二列 return [0, 0];//0合并0行,0占0行 } } }, objectSpanMethod({ row, column, rowIndex, columnIndex }) { if (columnIndex === 0) { if (rowIndex % 2 === 0) { return { rowspan: 2,//合并的行数 colspan: 1//合并的列数,设为0则直接不显示 }; } else { return { rowspan: 0, colspan: 0 }; } } }这里面可以通过对rowIndex,columnIndex根据自己的要求作一些条件判断,然后返回rowspan,colspan就可以合并了。 ...

May 22, 2019 · 1 min · jiezi

Copy攻城狮日志Error-if-theres-nested-data-rowKey-is-required

Created by huqi at 2019-5-18 10:32:30 Updated by huqi at 2019-5-18 12:32:23 ↑开局一张图,故事全靠编↑ 从最新学习d2开源项目说起有时候,非常非常地迷茫,找不到方向,找不到人生的方向,找不到未来的方向,找不到学习的方向。现在的状态,犹如一叶扁舟,漂浮着茫茫的大海之上。工作日,每天起床机械地去上班,周末,每天优哉游哉,好像一个木偶,被生活蹂躏的木偶,意识以外的力量在操纵着的木偶。在技术上的积累也渐渐走下坡路了,基础不牢固,也采取过一些办法,不知是疗程不够还是病入膏肓已无可救药,总之,明明知道有病,却怎么也治不好。最近,又加入了梁sir的暴走前端计划,重新折腾起来,于是开始学习,目前折腾了一下Vue.js,在看d2改版renren的项目,跟着@FairyEver大佬踩了不少坑,其中就有element-ui的这个坑--"Error: if there's nested data, rowKey is required." 刨根问到底,探究报错的原因首先,不用怀疑,这是一个bug,理论上是element-ui中el-table的一个bug,但又不能说是一个bug,因为人家框架原型设计的就是这样,只是可能我们使用不当。先粗略分析一下报错的原因: 1. 没有加row-key属性如文档中所提及的,结合报错的字面意思 ☞文档:table: 支持树类型的数据。此时,必须要指定 row-key。支持子节点数据异步加载。设置 Table 的 lazy 属性为 true 与 加载函数 load ,指定 row 中的 hasChildren 来确定哪些行是包含子节点。`那就copy一下官方案例,el-table加上row-key="id" <el-table row-key="id" > </ el-table>不过,对row-key的支持应该是2019年3月左右提供的。☞Table: support tree structure data 修改element-ui版本很气人啊,我只能修改package.json文件中依赖element-ui的版本。一般来说,默认安装的依赖,如果package.json中带 ^ 符号的话,会默认安装最近的版本,去掉 ^ 符号,重新安装一下element-ui依赖就可以解决了。至于改用那个版本,按照实际来吧,如果去掉 ^ 符号重新安装能成功就可以了,不行就换个版本吧。 修改children字段这个就不是很好的处理方式了,毕竟后台返回来的数据,你要他改字段,呵呵呵,当然,你话语权足够的话,又不想改前台代码,就让他替换一下childre字段吧。当然,官方将提供更改children键值的api。别问我children字段哪来的,我的是后台传过来的; 也别问我为什么会冲突,我猜是和之里冲突☞源码: getChildren(forceInit = false) { // this is data if (this.level === 0) return this.data; const data = this.data; if (!data) return null; const props = this.store.props; let children = 'children'; if (props) { children = props.children || 'children'; } if (data[children] === undefined) { data[children] = null; } if (forceInit && !data[children]) { data[children] = []; } return data[children]; }至于怎么前台怎么修改children字段,我也不会,大概是深浅拷贝之类的操作吧 ...

May 18, 2019 · 1 min · jiezi

elementUI-table表格动态合并

1.最近在做的erp项目,有一个需求是同一个客户下的同种订单,需要合并展示。使用elementUI table组件的方法 :span-method="objectSpanMethod"。官网上看了一下demo,做的很直白,不过不太符合业务。在网上找了篇文章参考了一下2.效果图如下: 在动态处理从后端拿回来的数据的时候,是需要从数据中找到一个唯一的“标识”去判断是否是相同种类的数据。然后根据这个“标识”去做逻辑判断。3.代码://合并单元格 二维数组-> 根据“标识”去遍历数据data() { return { spanArr: [], //遍历数据时,根据相同的标识去存储记录 pos: 0 // 二维数组的索引}}// methods中定义方法getSpanArr(data) { let that = this//页面展示的数据,不一定是全部的数据,所以每次都清空之前存储的 保证遍历的数据是最新的数据。以免造成数据渲染混乱that.spanArr = []that.pos = 0//遍历数据data.forEach((item, index) => { //判断是否是第一项 if (index === 0) { this.spanArr.push(1) this.pos = 0 } else { //不是第一项时,就根据标识去存储 if (data[index].moldName === data[index - 1].moldName) { // 查找到符合条件的数据时每次要把之前存储的数据+1 this.spanArr[this.pos] += 1 this.spanArr.push(0) } else { // 没有符合的数据时,要记住当前的index this.spanArr.push(1) this.pos = index } } })console.log(this.spanArr, this.pos)},// 列表方法objectSpanMethod({rowIndex, columnIndex}) { ...

May 14, 2019 · 1 min · jiezi

组件化页面封装elform

目前在编写个人项目,有一个是管理平台,基本每个页面都有el-from,所以对el-form做了二次封装, 组件在个人开发使用不错,但不确定能满足各种业务需求,所以这里主要和大家分享一下设计思路。设计组件分析问题el-form是element-ui库的表单组件,如果我们要将其进行二次封装,那么需要考虑几个问题: 如何设计表单渲染字段不同类型的el-form-item怎么去渲染,比如input,select,或者自定义显示内容等表单联动怎么去处理事件绑定表单验证更多需求...下面通过这些点,来实现封装一个二次的el-form组件。 从字段开始 拿业务用到的表单来举例,在这个表单中的需求分析。 首先是el-form-item的类型: tag类型显示input输入select选择按钮或者图片的显示或者绑定操作textarea输入然后再分析每个节点: label宽度是否需要验证placeholder显示验证规则绑定的相关事件是否可为readonly/disabled节点的class/样式 (一行显示一个或者多个)初步分析,大概会有这些需求,接下来对单个问题一一来分析解决。 设计渲染字段列表正常情况下,我们使用form,它的子项会是这样的,比如有input和select: // input类型<el-form-item label="活动名称" prop="name"> <el-input v-model="ruleForm.name"></el-input></el-form-item>// select类型<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 label="label" prop="prop"> // 如果是input类型 <el-input v-if="input" v-model="ruleForm.name"></el-input> // 如果是select类型 <el-select v-if="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>那由此我们可以设计出循环form的字段列表: label (字段名)value (字段值 prop,后面会有value代替)type (字段类型 input/select/password/textarea等)然后除了上面的例子我们还可以自己扩展一些字段: event (绑定的方法)list (列表 如果是select类型,需要有对于的list去渲染)TimePickerOptions (时间配置 如果是时间类型,可以传入配置)disabled (是否禁止)filterable (是否可筛选)clearable (是否可清除)required (是否必填 根据这个字段,去设置对于的验证规则)validator (自定义验证 验证时将使用自定义验证方法)show (是否显示, 布尔值或者是函数,下面会对联动渲染详细分析)更多(根据需求和场景扩展更多字段)然后完整的字段配置列表大概是这样的(个人使用场景,不需要使用到所有的设计字段): fieldList: [ { label: '账号', value: 'account', type: 'input', required: true, validator: checkAccount }, { label: '密码', value: 'password', type: 'password', required: true, validator: checkPwd }, { label: '昵称', value: 'name', type: 'input', required: true }, { label: '性别', value: 'sex', type: 'select', list: 'sexList', required: true }, { label: '头像', value: 'avatar', type: 'slot', className: 'el-form-block' }, { label: '手机号码', value: 'phone', type: 'input', validator: checkPhone }, { label: '微信', value: 'wechat', type: 'input', validator: checkWechat }, { label: 'QQ', value: 'qq', type: 'input', validator: checkQQ }, { label: '邮箱', value: 'email', type: 'input', validator: checkEmail }, { label: '描述', value: 'desc', type: 'textarea', className: 'el-form-block' }, { label: '状态', value: 'status', type: 'select', list: 'statusList', required: true } ]组件内部怎么操作,很简单,根据规则,一一对应循环字段,绑定属性就ok了,所以组件内部template就是这么点代码: ...

May 9, 2019 · 3 min · jiezi

vue-实现搜索的结果页面支持全选与取消全选

演示地址,打开、搜索、随便点demo : http://msisliao.github.io/dem... 仓库代码 : https://github.com/msisliao/v... 安装vue-cli安装elementUI npm i element-ui -S在main.js 引入elementUI// main.jsimport ElementUI from 'element-ui'import 'element-ui/lib/theme-chalk/index.css'Vue.use(ElementUI)demo功能概览默认没有全选,搜索时支持全选与取消全选,将选择的数据添加到已选中,已选删除时改变当前搜索列表的状态与全选按钮的状态全选时全部追加到已选,取消全选时从已选中删除当前搜索的列表功能列表1、搜索时展示相应的数据列表,支持全选与取消全选,(默认展示所有数据时不支持全选) datas() { // 每次搜索的数据 根据下拉菜单的值的变化 if (this.value !== "") { return this.listItem.list.filter(item => { return item.BrandNames.includes(this.value); }); } else { return this.listItem.list; // 没有搜索的关键词时展示全部数据 } },2、搜索的下拉菜单去重 filDatas() { // 利用reduce 下拉菜单去重 var obj = {}; return this.listItem.list.reduce(function(item, next) { obj[next.BrandNames] ? "" : (obj[next.BrandNames] = true && item.push(next)); return item; }, []); }3、当前界面全选时添加到已选中,当前界面取消全选时,从已选的数据删除当前搜索出来的列表数据, ...

May 9, 2019 · 4 min · jiezi

组件化页面封装eltable

项目做的越来越多,重复的东西不断的封装成了组件,慢慢的,页面就组件化了,只需要定义组件配置数据,使用就好了,这是一件非常舒服的事情,这篇文章主要和大家讲讲如何对element-ui中的el-table进行二次封装。 分析需求公有组件,可以被任何页面调用,首先要保证组件不和外部业务发生耦合,其次就是要设计合理的字段,使用时通过不同的配置即可使用。那先大致来分析以下可能有的需求: 动态表头嵌套表头表格显示内容类型自定义(文字,图片,超链接等)动态接口加载数据表格和分页联动分页和查询数据联动表格事件的处理className, width, height...更多需求...目前封装的组件并不算完美,不可能满足所以需求,这里的话主要还是和大家分享思路 动态表头和嵌套表头的实现实现动态表头,这个应该是许多使用table的朋友们的痛点,明明是一样的东西,却要写多个表格,实在不能忍,让我们今天来一举歼灭它。 分析表头结构el-table表头有两个必须的属性,prop值和label名字,其他非必须的有fixed,align,width或者min-width等,那由此可设计出一个这样的数据结构: { prop: 'name', label: '名称', fixed: true/false, align: 'center', minWidth: 160}进阶->嵌套表格上面我们得出了普通表头列的设计,那我们继续分析,看看嵌套表格配置多了哪些字段。根据element-ui官网文档,可以看到前面字段基本一样,嵌套表格多了children字段,用来循环子级表头,那由此我们可以设计出这样的数据结构: { prop: 'name', label: '名称', fixed: true/false, align: 'center', minWidth: 160, children: [ { prop: 'oldName', label: '旧名称', fixed: true/false, align: 'center', minWidth: 160, }, { prop: 'newName', label: '新名称', fixed: true/false, align: 'center', minWidth: 160, } ]}表头设计总结表头设计思路大概是这样,并不复杂,根据业务需求,大家都可以设计适合自己使用的字段。完整的表头设计字段应该大概会是这个样子这个是个人字段配置的例子,其中将prop字段改成了value, 下面代码统一会使用value代替prop。 fieldList: [ { label: '账号', value: 'account' }, { label: '用户名', value: 'name' }, { label: '所属角色', value: 'role_name', minWidth: 120 }, { label: '性别', value: 'sex', width: 80, list: 'sexList' }, { label: '账号类型', value: 'type', width: 100, list: 'accountTypeList' }, { label: '状态', value: 'status', width: 90, type: 'slot', list: 'statusList' }, { label: '创建人', value: 'create_user_name' }, { label: '创建时间', value: 'create_time', minWidth: 180 }, { label: '更新人', value: 'update_user_name' }, { label: '更新时间', value: 'update_time', minWidth: 180 } ]表格显示内容类型自定义表头设计只是将一些基本的需求实现了,但是实际业务往往更为复杂,比如当前列要显示的是图片,tag,超链接,或者列的数据是一个id要显示对应的label。 ...

May 9, 2019 · 3 min · jiezi

Elementui-elscrollbar-源码解析

前几天美化博客时发现滚动条在window下实在太难看,所以在基于vue的技术上寻找美化滚动条的方法。记得Element-ui源码中有名为 el-scrollbar 的滚动组件,虽然文档上没有提到,但使用的人还是不少。今天记录下源码的阅读心得。 在这之前在看苦涩的代码前,先大概描述一下滚动条组件的用处和行为,方便理解代码逻辑。 因为操作系统和浏览器的不同,滚动条外观是不一样的。需要做风格统一时,就需要做自定义滚动条。当然也可以直接修改CSS3中的 ::-webkit-scrollbar 相关属性来达到修改原生滚动条外观,但这个属性部分浏览器上没有能够完美兼容。 在一个固定高度的元素中,如内部内容超出了父级元素的固定高。为了让用户浏览其余的内容,通常都会设置父级元素overflow-y: scroll 出现滚动条。允许用户以滚动的形式来浏览剩下的内容。 而自定义滚动条,是先通过偏移视图元素,达到隐藏原生滚动条的效果。同时在视图元素的右侧和下方,增加用标签写出的模拟滚动条。监听模拟滚动条的事件(按下滑块或点击轨道),来动态更新视图窗口的scrollTop或scrollLeft值。同样的,也会监听视图窗口的事件(滚动事件或视图窗口的尺寸缩放事件),来同步更新自定义滚动条的状态(滑块所处的位置或滑块长度)。 滚动条其实是当前已浏览内容的一个直观展示,在固定元素中,如果scrollTop发生改变往下滚动。滚动条中的滑块也会向下移动。此时能够通过滚动条来得知内容的已滚动程度和剩余程度。 我们将页面想象成一个很长的画布,而我们能看到的是一个移动的窗口。当页面往下滚动时,窗口在画布中也就往下移动,来查看被遮挡的内容。同样的,滚动块里的滑块也往下移动同样比例的距离。所以滚动条就是一个等比例的缩小模型。 也就是说,固定元素的高度clientHeight 除以 固定元素包括溢出的总高度scrollHeight。同等于 滑块的高度 除以 滚动条的高度。他们的比例是一样的。 在大概了解滚动条的工作内容和计算公式后,看看源码中是如何处理他们之间的计算关系的。 文件scrollbar组件在 package/scrollbar/index.js 中被导出,其中 package/scrollbar/src 是代码的核心部分,入口文件是 main.js。 结构<el-scrollbar> <div style="height: 300px;"> <div style="height: 600px;"></div> </div></el-scrollbar>使用自定义标签 el-scrollbar 裹住使用的区域,scrollbar 组件会生成view 和 wrap 两个父级元素包裹插槽中的内容,还有两种类型的自定义滚动条 horizontal 和 vertical。 main.jsmain.js默认导出一个对象,接收一系列配置。 name: 'ElScrollbar',components: { // 滚动条组件,拥有水平与垂直两种形态 Bar },props: { native: Boolean, // 是否使用原生滚动条,即不附加自定义滚动条 wrapStyle: {}, // wrap的内联样式 wrapClass: {}, // wrap的样式名 viewClass: {}, // view的样式名 viewStyle: {}, // view的内联样式 noresize: Boolean, // 当container尺寸发生变化时,自动更新滚动条组件的状态 tag: { // 组件最外层的标签属性,默认为 div type: String, default: 'div' }},data() { return { sizeWidth: '0', // 水平滚动条的宽度 sizeHeight: '0', // 垂直滚动条的高度 moveX: 0, // 垂直滚动条的移动比例 moveY: 0 // 水平滚动条的移动比例 };},组件在render函数中生成结构。 ...

May 1, 2019 · 7 min · jiezi

Vue使用Canvas绘制图片矩形线条文字下载图片

1 前言1.1 业务场景图片储存在后台中,根据图片的地址,在vue页面中,查看图片,并根据坐标标注指定区域。 由于浏览器的机制,使用window.location.href下载图片时,并不会保存到本地,会在浏览器打开。 2 实现原理2.1 绘制画布<el-dialog title="查看图片" :visible.sync="dialogJPG" append-to-body> <canvas id="mycanvas" width="940" height="570"></canvas></el-dialog>这里为了交互体验,使用了element-ui的弹窗方式。将canvas画布放到了弹窗中。 为了突出画布效果可以在css中设置一个边框。 #mycanvas { border: 1px solid rgb(199, 198, 198);}2.2 绘制图片// imageUrl为后台提供图片地址doDraw(imageUrl){ // 获取canvas var canvas = document.getElementById("mycanvas") // 由于弹窗,确保已获取到 var a = setInterval(() =>{ // 重复获取 canvas = document.getElementById("mycanvas") if(!canvas){ return false } else { clearInterval(a) // 可以理解为一个画笔,可画路径、矩形、文字、图像 var context = canvas.getContext('2d') var img = new Image() img.src = imageUrl // 加载图片 img.onload = function(){ if(img.complete){ // 根据图像重新设定了canvas的长宽 canvas.setAttribute("width",img.width) canvas.setAttribute("height",img.height) // 绘制图片 context.drawImage(img,0,0,img.width,img.height) } } } },1)},context.drawImage()方法的参数介绍,可参照 W3school ...

April 26, 2019 · 1 min · jiezi

vue全家桶Echarts百度地图搭建数据可视化系统

本文章篇幅略长,内容有点多大佬可根据目录选择性查阅新人可一步步来阅读1 前言1.1 业务场景突然接到产品说要做一个数据监控的系统。有线图、柱状图、地图,类似于数据可视化的方式。 本人之前从未接触过Echarts,然后需要2周拿出成果,有点慌???????? 1.2 业务分析拿到需求看了一下 支持用户名、密码登录,默认显示一个维度数据,然后点击可钻取进入第二维度数据,再点击进入第三维度数据展示。 大致估摸着。。。 系统搭建vue-clivuex记录登录信息vue-router路由跳转3个维度的页面,提取出共用的组件各个组件开发调节样式,增加UI加入后台接口数据优化显示测试上线当然这不是要2周内,全做完。应该是完成步骤6。 相对于公司就我一个前端,没接触过Echarts,有问题都没人讨论的情况下。。。 心里还是忍不住想吐槽一下???????????? 1.3 效果展示这里列出了第一维度页面的样式。文字请无视,哈哈。 2 系统安装吐槽归吐槽,活还是要干的。????因为本人最熟悉的还是vue,所以还是选择了用vue全家桶来做。这部分可参考 vue build 2.1 安装node环境下载安装node环境,直接去官网下载即可 node.js安装完后可在命令行运行node -v npm -v 查看是否安装成功以及版本2.2 安装Vue项目2.2.1 安装vue官方文档:vuejs 这里我们使用npm的方式 npm i vue2.2.2 安装Vue CLI官方文档:vue CLI npm i -g @vue/cli安装之后,你就可以在命令行中访问 vue 命令。你可以用这个命令来查看其版本。vue -V2.2.3 创建项目这里安装的时候,注意将我们要使用的安装上。vuex、vue-router,其他可根据需要添加。 方法一vue create hello-world这里参照官方网站,有很详细的介绍。参照:vue create方法二使用图形化界面 vue ui界面含中文,很好操作。参照:vue ui2.2.4 安装插件方法一最直接也是推荐的 npm i xxx 这里介绍一下 -S -D -g 的区别 npm i -S xxx 文件写入dependencies,用于工程中开发时使用到的插件,会发布到生产环境如UI,JS等。npm i -D xxx 文件写入devDependencies,用于工程支持类插件,不会发布到生产环境,如gulp等压缩打包工具。npm i -g xxx 全局安装,如vue、ncu等。安装目录为:C:Users用户AppDataRoamingnpm方法二vue ui图像化界面中包含了若干插件,可点击安装,但不一定是最新版本。 ...

April 26, 2019 · 6 min · jiezi

VueElement前端导入导出Excel

1 前言1.1 业务场景由前台导入Excel表格,获取批量数据。 根据一个数组导出Excel表格。 2 实现原理2.1 引入工具库file-saver、xlsx、script-loadernpm install -S file-saver xlsx npm install -D script-loader 2.2 导入Excel2.2.1 Element 上传控件 <el-upload class="upload-demo" action="" :on-change="handleChange" :on-exceed="handleExceed" :on-remove="handleRemove" :file-list="fileListUpload" :limit="limitUpload" accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-excel" :auto-upload="false"> <el-button size="small" type="primary">点击上传</el-button> <div slot="tip" class="el-upload__tip">只 能 上 传 xlsx / xls 文 件</div></el-upload>limitUpload = 1限制只能上传1个文件 accept为默认打开的可上传的文件格式 handleChange(file, fileList){ this.fileTemp = file.raw},handleRemove(file,fileList){ this.fileTemp = null},fileTemp这里定义了一下变量,指向最新上传的附件,起始定义为null。 这里发现控件file.raw是我们要用的File类型。 2.2.2 导入判断if(this.fileTemp){ if((this.fileTemp.type == 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') || (this.fileTemp.type == 'application/vnd.ms-excel')){ this.importfxx(this.fileTemp) } else { this.$message({ type:'warning', message:'附件格式错误,请删除后重新上传!' }) }} else { this.$message({ type:'warning', message:'请上传附件!' })}2.2.3 导入函数importfxx(obj) { let _this = this; // 通过DOM取文件数据 this.file = obj var rABS = false; //是否将文件读取为二进制字符串 var f = this.file; var reader = new FileReader(); //if (!FileReader.prototype.readAsBinaryString) { FileReader.prototype.readAsBinaryString = function(f) { var binary = ""; var rABS = false; //是否将文件读取为二进制字符串 var pt = this; var wb; //读取完成的数据 var outdata; var reader = new FileReader(); reader.onload = function(e) { var bytes = new Uint8Array(reader.result); var length = bytes.byteLength; for(var i = 0; i < length; i++) { binary += String.fromCharCode(bytes[i]); } var XLSX = require('xlsx'); if(rABS) { wb = XLSX.read(btoa(fixdata(binary)), { //手动转化 type: 'base64' }); } else { wb = XLSX.read(binary, { type: 'binary' }); } outdata = XLSX.utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]]);//outdata就是你想要的东西 this.da = [...outdata] let arr = [] this.da.map(v => { let obj = {} obj.code = v['设备ID'] obj.type = v['设备型号'] arr.push(obj) }) return arr } reader.readAsArrayBuffer(f); } if(rABS) { reader.readAsArrayBuffer(f); } else { reader.readAsBinaryString(f); }},arr就是我们要的结果,是一个数组。每一个值是个对象,包含了code type两个属性。 ...

April 26, 2019 · 2 min · jiezi

elementiconfont-实现iconPicker组件

在做后台管理项目的时候,有一个功能是侧边栏可配置。可配置项有:名字、路由、图标、权限。其中名字、路由、权限在大神的vue-element-admin中已经有很详细的介绍了,问题就在于icon选择配置(这个做完之后,发现不是很有必要,因为这个项目就内部人员在用,配图表手动输入class名就ok了),具体方案是用element-ui的select组件自定义模板实现icon可视化选择。 功能实现第一步,搜索引擎大法,用baidu、google分别搜索了关键词iconpicker,一大堆搜索结果,有bootstrap的,有layui的,有jquery的。但他们的icon都是固定的库,不能自己去增删改。而且项目中这几个框架都没有引入,为了一个功能去引入一个库感觉有点不划算(库的OS:谁稀罕你用似的)。 搜索无果后,决定自己动手做一个组件,然后先列出自己想要的几个关键点 icon可维护增删icon简单图形化选择为了后期icon的维护(认真思考后感觉又是一个不是必要的选项)选择了用iconfont,能随时添加删除icon。然后图形化选择的话,用element-ui自带的select组件就行了但问题关键在于将iconfont 引入项目项目引入,直接将iconfont项目下载下来,放到/src/assets/font文件夹中 文件倒是放到本地了,但问题是我如何知道我引入了哪些icon,以及将icon的class名输出到一个数组里,并在项目中可用。手动粘贴复制倒是可以,但下次再增删几个icon 还要一一对比吗?所以条路肯定不行了。现在就是要读去iconfont.css中的内容,并将其中的所以class提取出来 第一个我能想到的方法就是,在vue.config.js中使用node.js的api将iconfont.css的文件内容读取出来。 const path = require('path')const rf = require('fs')function resolve (dir) { return path.join(__dirname, dir)}rf.readFile(resolve('src/assets/font/iconfont.css'), 'utf-8', function (err, data) { if (err) { console.log('error') return false } else { console.log(data)})代码确实输出了iconfont.css中的所有内容 然后我需要对这个data进行处理,输出一个数组 const res = data.match(/.iconfont*.+:before/g) 提取出icon名,在输出到一个变量中,但问题是从vue.config.js将变量输出到哪去,才能在整个项目中使用呢?localstorage中?而且在开发环境下能够使用node.js 在生产环境可不行。所以我采取了一个折中的方法,将这个变量输出到一个文件当中,然后文件中export 这个变量 function replacer (match, p1, p2, p3, offset, string) { // p1 is nondigits, p2 digits, and p3 non-alphanumerics return p2}rf.readFile(resolve('src/assets/font/iconfont.1.css'), 'utf-8', function (err, data) { if (err) { console.log('error') return false } else { const res = data.match(/.iconfont*.+:before/g) icondata = res.map(item => { return `'${item.replace(/(.iconfont-)(.*)(:before)/, replacer)}'` }) icondata = `export default [${icondata.toString()}]` rf.writeFile(resolve('src/utils/icon.js'), icondata, (err) => { if (err) throw err console.log('The file has been saved!') }) }})replacer 函数来源 ...

April 23, 2019 · 1 min · jiezi

Vue $mount实战--实现消息弹窗组件

之前的项目一直在使用Element-UI框架,element中的Notification、Message组件使用时不需要在html写标签,而是使用js调用。那时就很疑惑,为什么element ui使用this.$notify、this.$message就可以实现这样的功能?1、实现消息弹窗组件的几个问题如何在任何组件中使用this.$message就可以显示消息?如何将消息的dom节点插入到body中?同时出现多个消息弹窗时,消息弹窗的z-index如何控制?2、效果预览3、代码实现PMessage.vue<template> <transition name=“message-fade”> <div class=“p-message” :class="[type, extraClass]" v-show=“show” @mouseenter=“clearTimer” @mouseleave=“startTimer”> <div class=“p-message-container”> <i class=“p-message-icon” :class="p-message-icon-${type}"></i> <div class=“p-message-content”> <slot class=“p-message-content”> <div v-html=“message”></div> </slot> </div> </div> </div> </transition></template><script> // 绑定事件 function _addEvent(el, eventName, fn){ if(document.addEventListener){ el.addEventListener(eventName, fn, false); }else if(window.attachEvent){ el.attactEvent(‘on’ + eventName, fn); } }; // 解绑事件 function _offEvent(el, eventName, fn){ if(document.removeEventListener){ el.removeEventListener(eventName, fn, false); }else if(window.detachEvent){ el.detachEvent(‘on’ + eventName, fn); } }; export default { name: “PMessage”, data(){ return { type: ‘success’, duration: 3000, extraClass: ‘’, message: ‘’, timer: null, closed: false, show: false } }, methods: { startTimer(){ if(this.duration > 0){ this.timer = setTimeout(() => { if(!this.closed){ this.close(); } }, this.duration); } }, clearTimer(){ clearTimeout(this.timer); }, close(){ this.closed = true; if(typeof this.onClose === ‘function’){ // 调用onClose方法,以从p-message.js中的instances数组中移除当前组件,不移除的话就占空间了 this.onClose(); } }, // 销毁组件 destroyElement(){ _offEvent(this.$el, ’transitionend’, this.destroyElement); // 手动销毁组件 this.$destroy(true); this.$el.parentNode.removeChild(this.$el); }, }, watch: { // 监听closed,如果它为true,则销毁message组件 closed(newVal){ if(newVal){ this.show = false; // message过渡完成后再去销毁message组件及移除元素 addEvent(this.$el, ’transitionend’, this.destroyElement); } } }, mounted() { this.startTimer(); } }</script><style lang=“stylus”>@import “p-message.styl”</style>p-message.jsimport Vue from ‘vue’;import PMessage from ‘./PMessage.vue’;import {popupManager} from “../../common/js/popup-manager”;let PMessageControl = Vue.extend(PMessage);let count = 0;// 存储message组件实例,如需有关闭所有message的功能就需要将每个message组件都存储起来let instances = [];const isVNode = function (node) { return node !== null && typeof node === ‘object’ && Object.prototype.hasOwnProperty.call(node, ‘componentOptions’);};const Message = function (options) { options = options || {}; if(typeof options === ‘string’){ options = { message: options }; } let id = ‘message’ + ++count; let userOnClose = options.onClose; // PMsesage.vue销毁时会调用传递进去的onClose,而onClose的处理就是将指定id的message组件从instances中移除 options.onClose = function (){ Message._close(id, userOnClose); }; /* 这里传递给PMessageControl的data不会覆盖PMessage.vue中原有的data,而是与PMessage.vue中原有的data进行合并,类似 * 与mixin,包括传递methods、生命周期函数也是一样 / let instance = new PMessageControl({ data: options }); // 传递vNode if(isVNode(instance.message)){ instance.$slots.default = [instance.message]; instance.message = null; } instance.id = id; // 渲染元素,随后使用原生appendChild将dom插入到页面中 instance.$mount(); let $el = instance.$el; // message弹窗的z-index由popupManager来提供 $el.style.zIndex = popupManager.getNextZIndex(); document.body.appendChild($el); // 将message显示出来 instance.show = true; console.log(instance) instances.push(instance); return instance;};// message简化操作[‘success’,’error’].forEach(function (item) { Message[item] = options => { if(typeof options === ‘string’){ options = { message: options } } options.type = item; return Message(options); }});/* * 从instances删除指定message,内部使用 * @param id * @param userOnClose * @private /Message._close = function (id, userOnClose) { for(var i = 0, len = instances.length; i < len; i++){ if(instances[i].id === id){ if(typeof userOnClose === ‘function’){ userOnClose(instances[i]); } instances.splice(i, 1); break; } }};// 关闭所有messageMessage.closeAll = function () { for(var i = instances.length - 1; i >= 0; i–){ instances.close(); }};export default Message;popup-manager.jslet zIndex = 1000;let hasZIndexInited = false;const popupManager = { // 获取索引 getNextZIndex(){ if(!hasZIndexInited){ hasZIndexInited = true; return zIndex; } return zIndex++; }};export {popupManager};p-index.jsimport pMessage from ‘./p-message.js’;export default pMessage;p-message.styl.p-message{ position: fixed; top: 20px; left: 50%; padding: 8px 15px; border-radius: 4px; background-color: #fff; color: #000; transform: translateX(-50%); transition: opacity .3s, transform .4s; &.message-fade-enter, &.message-fade-leave-to{ opacity: 0; transform: translateX(-50%) translateY(-30px); } &.message-fade-enter-to, &.message-fade-leave{ opacity: 1; transform: translateX(-50%) translateY(0); } &.error{ color: #ff3737; } .p-message-icon{ / 使图标与内容能够垂直居中 / display: table-cell; vertical-align: middle; width: 64px; height: 45px; &.p-message-icon-success{ background: url("../../assets/images/icons/message-icon/icon_success.png") no-repeat 0 0; } &.p-message-icon-error{ background: url("../../assets/images/icons/message-icon/icon_error.png") no-repeat 0 0; } } .p-message-content{ / 使图标与内容能够垂直居中 */ display: table-cell; vertical-align: middle; padding-left: 15px; }}main.js// 引入pMessage组件import pMessage from ‘./components/p-message/p-index.js’;// 将pMessage绑定到Vue.prototype中。这样在组件中就可以通过this.$pMessage()的形式来使用了Vue.prototype.$pMessage = pMessage;3、参考参考了 Element-UI 的message代码封装Vue组件的一些技巧 ...

April 19, 2019 · 3 min · jiezi

element ui radio组件添加点击可取消选中状态

有人会问:既然要取消选中为什么不用checkbox呢?举个栗子,比如选中性别时,用户可以选男或者女(二选一),然后也可以取消选中(二者都不选)这时这个demo就派上用场了 <el-radio-group v-model=“area”> <el-radio @click.native.prevent=“clickitem(item.AreaName)” :label=“item.AreaName” v-for="(item,index) in areaItem" :key=“index”>{{item.AreaName}}</el-radio> </el-radio-group> <!– 如果直接@click会触发两次 默认有change事件 @click.native.prevent 加上这个阻止默认事件 –> <script> export default { data () { return { area: ‘’, areaItem:[ {AreaName: “东北”, ID: 1}, {AreaName: “华南”, ID: 2}, {AreaName: “西北”, ID: 3} ] }; }, methods:{ clickitem(e){ if(e===area){ this.area = ’’ //如果点击的对象是area就将v-model的值清空 radio状态为空 }else{ this.area = e //否则就把点击的值赋值给area 即绑定的radio } } }}</script>

April 19, 2019 · 1 min · jiezi

[Vue warn]: Invalid prop: type check failed for prop ****.

以下错误都是同样的原因[Vue warn]: Invalid prop: type check failed for prop “readonly”. Expected Boolean, got String with value “true”.[Vue warn]: Invalid prop: type check failed for prop “disabled”. Expected Boolean, got String with value “true”.……刚刚开始学习vue,为了实现只读展示所以添加readonly属性,查询官方文档发现默认值为false,所以最终代码中为readonly=“true”。官方文档我的代码<el-col :span=“8”> <el-form-item label=“客户名称” prop=“custName” status-icon> <el-input maxlength=“100” v-model=“custName” readonly=“true”></el-input> </el-form-item></el-col>测试发现虽然实现只读展示,但是在浏览器控制报错。解决方案直接使用readonly即可,不需要=“ture”。<el-col :span=“8”> <el-form-item label=“客户名称” prop=“custName” status-icon> <el-input maxlength=“100” v-model=“custName” readonly></el-input> </el-form-item></el-col>

April 18, 2019 · 1 min · jiezi

造轮子:写vue组件库orange-ui

项目搭建项目效果地址项目搭建参考 从零开始搭建 Vue 组件库 VV-UI项目地址记录模仿 VV-UI 组件库造轮子中遇到的问题,以及解决方案新建项目对于脚手架环境的问题,目前已经有非常成熟的 vue 官方的脚手架,我们拿来用就好了npm install vue-cli -gvue init webpack origin-uicd origin-uinpm installnpm run dev项目可以正常启动,在此基础上进行改造更改目录|– examples // 原 src 目录,改成 examples 用作示例展示 |– assets // api文档logo 样式文件 |– docs // api文档 |– router // api文档路由|– packages // 新增 packages 用于编写存放组件 |– button // 组件 |– theme-default // 组件样式 gulp 运行目录 |– lib // 编译后css |– src // 编译前css |– gulpfile.js // gulp 写打包css的task |– salad.config.json // BEM的配置文件 |– index.js // 导出组件原 src 目录,改成 examples 用作示例展示,需要对应修改 webpack 配置把原先的编译指向 src 的目录改成 examples{ test: /.(js|vue)$/, loader: ’eslint-loader’, include: [resolve(’examples’), resolve(’test’), resolve(‘packages’)],// 修改}entry: { app: ‘./examples/main.js’ // 程序入口修改},resolve: { alias: { vue$: ‘vue/dist/vue.esm.js’, ‘@’: resolve(’examples’) // 根据实际情况修改 }},如何编写文档使用vue-markdown-loader在 vue 下可以去写 markdown 文档安装# For Vue2npm i vue-markdown-loader -Dnpm i vue-loader vue-template-compiler -D使用webpack.config.js file:module.exports = { module: { rules: [ { test: /.md$/, loader: ‘vue-markdown-loader’ } ] }}在 example/docs 目录下新建 test.md同时创建一个新的路由,指向我们的 md 文件:{ path: ‘/test’, name: ’test’, component: r => require.ensure([], () => r(requi(’../docs/test.md’)))}打开浏览器访问http://localhost:8080/#/test实现 demo/代码演示需求 1 就是拦截 import,并且解析 markdowm 语法需求 2 在析 markdown 中也可以写 Vue 的组件全部配置可参考Vue 加载 Markdown 格式组件有详细注释markdown-it,支持 options 选项。这样我们就可以为我们的 markdown 定义独特的标识符,这里我用 demo 标识需要显示代码块的地方,所以我需要配置 options 选项 :const vueMarkdown = { preprocess: (MarkdownIt, source) => { MarkdownIt.renderer.rules.table_open = function() { return ‘<table class=“table”>’ } MarkdownIt.renderer.rules.fence = utils.wrapCustomClass( MarkdownIt.renderer.rules.fence ) return source }, use: [ [ MarkdownItContainer, ‘demo’, { // 用于校验包含demo的代码块 validate: params => params.trim().match(/^demo\s*(.)$/), render: function(tokens, idx) { var m = tokens[idx].info.trim().match(/^demo\s(.)$/) if (tokens[idx].nesting === 1) { var desc = tokens[idx + 2].content // 编译成html const html = utils.convertHtml( striptags(tokens[idx + 1].content, ‘script’) ) // 移除描述,防止被添加到代码块 tokens[idx + 2].children = [] return &lt;demo-block&gt; &lt;div slot="desc"&gt;${html}&lt;/div&gt; &lt;div slot="highlight"&gt; } return ‘</div></demo-block>\n’ } } ] ]}这里简单的描述一下这段代码是干什么的:首先把内容里面 vue 片段编译成 html,用于显示,另一方面用 highlight 来高亮代码块。demo-block 本身是我们定义好的组件:<template> <div class=“docs-demo-wrapper”> <div :style="{maxHeight: isExpand ? ‘700px’ : ‘0’}" class=“demo-container”> <div span=“14”> <div class=“docs-demo docs-demo–expand”> <div class=“highlight-wrapper”> <slot name=“highlight”></slot> </div> </div> </div> </div> <span class=“docs-trans docs-demo__triangle” @click=“toggle” >{{isExpand ? ‘隐藏代码’ : ‘显示代码’}}</span > </div></template>基本用法:::: demo<o-button>默认按钮</o-button>:::如何编写组件环境准备完毕,紧接着要开始编写组件,考虑的是组件库,所以我们竟可能让我们的组件支持全局引入和按需引入,如果全局引入,那么所有的组件需要要注册到 Vue component 上,并导出:const install = function(Vue) { if (install.installed) return components.map(component => Vue.component(component.name, component))}export default { install}着要实现按需加载,我们只需要单个导出组件即可:import Button from ‘./button/index.js’import Row from ‘./row/index’import Col from ‘./col/index’const components = [Button, Row, Col]const install = function(Vue) { if (install.installed) return components.map(component => Vue.component(component.name, component))}if (typeof window !== ‘undefined’ && window.Vue) { install(window.Vue)}export { install, Button, Row, Col }既然是单页面应用,必然要去解决样式冲突问题,如果组件内使用 soped,那么样式就无法从组件内抽离出来,达不到可定制化主题颜色的目的。我们需要一套可以分离处理的样式,可以自行编译,可以相互不污染。这时候 css 的 BEM 规范就显得尤为重要。如果你还不知道什么是 BEM 参考: http://www.w3cplus.com/css/cs…。说到这里,目前对 BEM 规范支持较好的插件就是 postcss 了,他允许我们配置 BEM 之间的连接符和缩写:{ “browsers”: [“ie > 8”, “last 2 versions”], “features”: { “bem”: { “shortcuts”: { “component”: “b”, “modifier”: “m”, “descendent”: “e” }, “separators”: { “descendent”: “__”, “modifier”: “–” } } }}这样我们就可以把样式单独的抽离出来,通过 gulp 进行打包编译:gulp.task(‘compile’, function() { return gulp .src(’./src/.css’) .pipe(postcss([salad])) .pipe(cssmin()) .pipe(gulp.dest(’./lib’))})关于 gulp 的使用npm install –global gulpnpm install –save-dev gulp进入 packages/theme-default 中,运行 gulpgulp ...

April 17, 2019 · 2 min · jiezi

el-table使用:render-header方法设置el-checkbox

最近有个需求,需要在每次对el-table的单项进行勾选时,使用@select-change去调取后台接口,更改表格数据。然而,el-table的selection列有个大bug。首先,获取后的数据对于el-table的selection列来说,没有字段props可以去接收,这就导致没有数据是选中的,会直接触发@select-change方法,回调参数val为[]。其次,selection需要使用toggleSelection方法去更改,当更改时,又一次触发@select-change方法,这显然不符合需求。因此,需要自定义table的表头信息,设置为el-checkbox。代码如下// 自定义表头select renderHeader(h, {column, $index}) { return h(“span”, {}, [ h(’el-checkbox’,{ props: { checked: this.allchecked }, on:{ change: this.updateAllSelected // 选中事件 } })]); },

April 10, 2019 · 1 min · jiezi

element-ui日期时间选择器的日期格式化问题

最近在做vue+element-ui的后台管理页面,其中用到了DateTimePicker来选择日期时间,但是在将数据传回后台的过程中遇到了一些令人头疼的问题,在此记录一下解决方案,以免日后再次遇到。前端代码 submitForm(formName) { this.$refs[formName].validate((valid) => { let url = ‘http://localhost:8088/pethospital/order-record’ let data = qs.stringify({ title: this.orderForm.title, hospitalId: this.orderForm.hospitalId, orderDate: this.orderForm.orderDate, orderType: this.orderForm.orderType, petVariety: this.orderForm.petVariety, mobilePhone: this.orderForm.mobilePhone, supplement: this.orderForm.supplement }) if (valid) { axios.post(url, data) .then(response => { }).catch(error => { this.$message({ message: ‘错误:’ + error, type: true }) }) } else { this.$message(‘验证错误:请确认信息是否填写完整’) } });} 实体类代码private Long id;private String title;private Integer hospitalId;private Date orderDate;private Integer orderType;private String petVariety;private String mobilePhone;private String supplement;Controller代码@PostMapping("/order-record")public CommonResult addOrderRecord(OrderRecordDO orderRecordDO) throws ParseException { System.out.println(“添加的预约记录:” + orderRecordDO); orderRecordDOMapper.insertSelective(orderRecordDO); return null;}控制台输出Field error in object ‘orderRecordDO’ on field ‘orderDate’: rejected value [2019-04-10 10:00:00]; codes [typeMismatch.orderRecordDO.orderDate,typeMismatch.orderDate,typeMismatch.java.util.Date,typeMismatch]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [orderRecordDO.orderDate,orderDate]; arguments []; default message [orderDate]]; default message [Failed to convert property value of type ‘java.lang.String’ to required type ‘java.util.Date’ for property ‘orderDate’; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [java.util.Date] for value ‘2019-04-10 10:00:00’; nested exception is java.lang.IllegalArgumentException]]看了控制台的输出信息,大概知道是前端将日期当做String类型传输的,但是我们后台定义日期用的是Date类型,因此这里报的转换异常。本来我想用SimpleDateFormat来转换的,但是觉得这样很麻烦,然后在网上查找相关资料发现可以有更简单的方法。尝试1:在实体类字段上添加@DateTimeFormat(pattern = “yyyy-MM-dd HH:mm:ss”)@DateTimeFormat(pattern = “yyyy-MM-dd HH:mm:ss”)private Date orderDate;控制台输出添加的预约记录:{“id”:null,“title”:“测试1”,“hospitalId”:1001,“orderDate”:“Wed Apr 10 10:00:00 CST 2019”,“orderType”:2001,“petVariety”:“哈士奇”,“mobilePhone”:“1000”,“supplement”:“二哈”}数据库记录遇到的问题:从数据库获取数据后在前端显示不友好尝试2:在实体类字段添加@DateTimeFormat(pattern = “yyyy-MM-dd HH:mm:ss”)和@JsonFormat(pattern = “yyyy-MM-dd HH:mm:ss”, timezone = “GMT+8”)/** * timezone = “GMT+8"指定时区 */@DateTimeFormat(pattern = “yyyy-MM-dd HH:mm:ss”)@JsonFormat(pattern = “yyyy-MM-dd HH:mm:ss”, timezone = “GMT+8”)private Date orderDate;前端显示效果:这下就能显示成我们想要的效果了尝试3:我的后台项目使用SpringBoot搭建的,我在application.yml文件中添加如下配置# 配置数据源spring: datasource: name: pet-hospital type: com.alibaba.druid.pool.DruidDataSource url: jdbc:mysql://localhost:3306/pet_hospital?serverTimezone=GMT%2B8 driver-class-name: com.mysql.cj.jdbc.Driver username: root password: 1741248769 # Vue前端传来的日期为String类型,下面的设置可以自动将其转换为Date类型,不需要手动转换 mvc: date-format: yyyy-MM-dd HH:mm:ss # 以下设置可以将Date类型自动转换为如下格式的日期,指定Jackson格式化日期使用的时区,Jackson默认使用UTC jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8显示效果总结:日期从前端传到后端(添加),由String类型解析成Date类型,从后端传到前端(查询),由Date类型解析成String类型可以使用注解的方式,@DateTimeFormat、@JsonFormat可以使用配置文件方式,spring.mvc.date-format、spring.jackson.date-format/time-zone为什么要设置time-zone?因为Jackson默认使用UTC时区,所以需要手动指定时区为GMT+8附:原时间2019-04-12 12:00:00,相差8个小时第一次用思否,大家多多包涵…… ...

April 9, 2019 · 1 min · jiezi

vue elementUI table表格数据 滚动懒加载

在项目中遇到了一个性能问题vue+elementUI table表格展示数据,当数据很多的时候,不能一页显示完,同时一次请求数据量太大,会增加网页渲染的时间,影响体验,这个时候常常有两种方法处理,1、分页,如下2、如果我不想分页,又想在一页显示全部数据呢?这个时候其实就可以用数据懒加载了如下一开始表格只显示31行数据当将滚动条拉到低的时候,就会再加载31条数据,如果剩下的数据不足31,那就加载剩下的根据项目需求,这需要一页可以看到全部数据,所以我选择了第二中方式那么第二种方式要怎么去实现呢?在了解它的原理前,你需要分清楚三个属性:scrollHeight:指元素的总高度,包含滚动条中的内容。只读属性。不带px单位。就是下图中,54条数据的高度,但是因为有滚动条,所以屏幕看不到这么高scrollTop:当元素出现滚动条时,向下拖动滚动条,内容向上滚动的距离。可读可写属性。不带px单位。如果该元素没有滚动条,则scrollTop的值为0,该值只能是正值。就是下图中红色框的高度clientHeight:元素客户区的大小,指的是元素内容及其边框所占据的空间大小,实际上就是可视区域的大小。就是下图红色箭头的高度那如何判断滚动条滚到底部了呢?scrollHeight-scrollTop-clientHeight=0,这个时候可以就是滚动条滚到底部的时候了。在第一次请求数据的时候,先设置一个变量来记录请求次数(其实后台也是做分页的处理)this.currentPage = 1,$this = this;this.$axios.fun().then(res=>{ $this.totalPage = res.totalPage; //这里需要知道总页数 $this.tableData = res.data;//表格数据})监听表格dom对象的滚动事件let dom = document.querySelector(targetDom); dom.addEventListener(“scroll”, function() { const scrollDistance =dom.scrollHeight - dom.scrollTop - dom.clientHeight; if(scrollDistance <=0){//等于0证明已经到底,可以请求接口 if($this.currentPage < $this.totalPage){//当前页数小于总页数就请求 $this.currentPage++;//当前页数自增 //请求接口的代码 $this.$axios.fun().then(res=>{ $this.tableData = $this.tableData.concat(res.data)//将请求回来的数据和当前展示的数据合并在一起 }) } } })好了,表格滚动下拉懒加载数据就是这样实现的,希望可以帮到有需求的同学。

April 3, 2019 · 1 min · jiezi

使用mixins,实现elementUI表单的全局验证

使用ElementUi搭建框架的时候,大家应该都有考虑过怎么做全局验证,毕竟复制粘贴什么的是最烦了,这里分享下个人的解决方法。验证规则分析规则一般验证规则,主要是是否必填,不为空,以及参数类型的验证。基于这个条件,我们开始找找思路, 单个字段的验证是这样的:name: { required: 是否必填, validator: 自定义规则, message: 失败提示消息(非自定义时触发), trigger: 触发方式}循环实现固定的规则。当一个东西固定之后,那必然是可以重复使用的,并且可以快速生成,我们可以用循环来实现它。但是用循环来实现,我们则需要一个数据规则。定义数据规则分析下需要的字段,大概就是以下几种,其他的可以根据自身的需求去增加:验证的字段名 label验证的值 value验证的类型 type是否必填 required自定义规则 rules那最终我们得到的是这样一个字段配置列表:fieldList: [ {label: ‘账号’, value: ‘account’, type: ‘input’, required: true}, {label: ‘密码’, value: ‘password’, type: ‘password’, required: true}, {label: ‘昵称’, value: ’name’, type: ‘input’, required: true}, {label: ‘性别’, value: ‘sex’, type: ‘select’, list: ‘sexList’, required: true}, {label: ‘头像’, value: ‘avatar’, type: ‘input’}, {label: ‘手机号码’, value: ‘phone’, type: ‘input’}, {label: ‘微信’, value: ‘wechat’, type: ‘input’}, {label: ‘QQ’, value: ‘qq’, type: ‘input’}, {label: ‘邮箱’, value: ’email’, type: ‘input’}, {label: ‘状态’, value: ‘status’, type: ‘select’, list: ‘statusList’, required: true} ]form完整的字段配置建议参考: // 表单相关 formInfo: { ref: null, data: { id: ‘’, // *唯一ID account: ‘’, // *用户账号 password: ‘’, // *用户密码 name: ‘’, // *用户昵称 type: 2, // *用户类型: 0: 手机注册 1: 论坛注册 2: 管理平台添加 sex: 0, // *性别: 0:男 1:女 avatar: ‘’, // 头像 phone: ‘’, // 手机号码 wechat: ‘’, // 微信 qq: ‘’, // qq email: ‘’, // 邮箱 status: 1 // *状态: 0:停用,1:启用(默认为1)’, // create_user: ‘’, // 创建人 // create_time: ‘’, // 创建时间 // update_user: ‘’, // 修改人 // update_time: ’’ // 修改时间 }, fieldList: [ {label: ‘账号’, value: ‘account’, type: ‘input’, required: true}, {label: ‘密码’, value: ‘password’, type: ‘password’, required: true}, {label: ‘昵称’, value: ’name’, type: ‘input’, required: true}, {label: ‘性别’, value: ‘sex’, type: ‘select’, list: ‘sexList’, required: true}, {label: ‘头像’, value: ‘avatar’, type: ‘input’}, {label: ‘手机号码’, value: ‘phone’, type: ‘input’}, {label: ‘微信’, value: ‘wechat’, type: ‘input’}, {label: ‘QQ’, value: ‘qq’, type: ‘input’}, {label: ‘邮箱’, value: ’email’, type: ‘input’}, {label: ‘状态’, value: ‘status’, type: ‘select’, list: ‘statusList’, required: true} ], rules: {}, labelWidth: ‘120px’ }实现验证方法循环字段列表,根据type判断是提示选择不能为空,还是输入不能为空如果字段必填,则根据是否有自定义验证去生成验证规则字段非必填,有自定义规则生成验证 // 初始化验证数据 _initRules (formInfo) { const obj = {}, fieldList = formInfo.fieldList // 循环字段列表 for (let item of fieldList) { let type = item.type === ‘select’ ? ‘选择’ : ‘输入’ if (item.required) { if (item.rules) { obj[item.value] = { required: item.required, validator: item.rules, trigger: ‘blur’ } } else { obj[item.value] = { required: item.required, message: ‘请’ + type + item.label, trigger: ‘blur’ } } } else if (item.rules) { obj[item.value] = { validator: item.rules, trigger: ‘blur’ } } } formInfo.rules = obj }怎么配置到全局通过mixin配置,然后在页面中使用(个人使用的是mixin)配置为全局方法在页面中调用挂在到vue实例上,通过this即可访问最后在项目的系统管理模块中可以看到示例代码:项目地址项目代码地址 ...

April 2, 2019 · 2 min · jiezi

Vue动态菜单(路由)的实现方案(beforeEach+addRoutes+elementUI)

前端路漫漫,挽起袖子干前言我之前总结过动态菜单的实现方案>动态菜单实现,只不过这篇写的有点稍微复杂,是用后端返回当前登录角色的路由表实现的,也就是前端只要从后端取到路由表进行渲染菜单即可;今天,我再讲解一种方案:路由表写在前端,后端返回用户的角色,前端进行角色对应的菜单渲染在线预览:动态路由github(记的star哈):https://github.com/Mrblackant…开始之前,自己要大概懂写关于vue-router的beforeEach(路由拦截)、addRoutes,elementUI的菜单组件等方法,不然理解可能会有点吃力思路分以下几步:1.前端在本地写好路由表,以及每个路由对应的角色,也就是哪些角色可以看到这个菜单/路由;2.登录的时候,向后端请求得到登录用户的角色(管理者、普通用户);3.利用路由拦截,根据取到的用户角色,跟本地的路由表进行对比,过滤出用户对应的路由,并利用路由进行左侧菜单渲染实现根据上述的3步,我们进行每一步的实现1.前端本地写好路由表我们分成两个路由表,一个是固定的,比如首页展示,每个人都能看到,一个需要根据用户角色动态展示的;这里就利用到了router的meta属性,我们在这里边写上菜单对应的:icon,对应的哪些角色可以看到这个菜单:roles一个完整的路由表如下://代码位置:router/index.js { path: ‘’, component: layout, //整体页面的布局(包含左侧菜单跟主内容区域) children: [{ path: ‘main’, component: main, meta: { title: ‘首页’, //菜单名称 roles: [‘user’, ‘admin’], //当前菜单哪些角色可以看到 icon: ’el-icon-info’ //菜单左侧的icon图标 } }] }2.用户登录,取到用户的角色本来我是写了mock数据,模拟用户登录,请求后端角色的接口,奈何mock挂了,所以我就直接模拟了:取到用户角色,存放进localStorage,然后跳转主页//代码位置:src/components/reLoad.vue // axios.post(’/temp’,this.formModel).then(res=>{}) // 我暂时就不模拟了,直接取 let getUserRole = this.formModel.user === ‘admin’ ? ‘admin’ : ‘user’ localStorage.setItem(‘userRole’, getUserRole) this.$router.push({ path: ‘/main’ })3.路由拦截beforeEach,并过滤出角色对应的路由表经过第2步,我们已经得到了用户的角色,这时候在路由拦截的地方我们就可以取到了,取到之后,结合第1步我们写好的路由,利用数组的filter方法,拿角色跟路由表里meta标签里的roless数据进行对比过滤好了,拿当前路由去渲染左侧菜单,这一步其实可以用vuex去实现,我担心有的小伙伴不理解,就用一个global(全局变量)替代了尤其要注意路由拦截这里,很容易陷入死循环,所以我建议大家先了解一下beforeEach和addRoutes的运行机制//代码位置:src/permission.jsrouter.beforeEach((to, from, next) => { // 取到用户的角色 let GetRole = localStorage.getItem(“userRole”) // 如果登录了 if (GetRole !== ‘unload’) { next() //next()方法后的代码也会执行 // 1.如果路由表 没根据角色进行筛选,就筛选一次 if (!addRouFlag) { addRouFlag = true // 2.根据用户的角色、和需要动态展示的路由,生成符合用户角色的路由 var getRoutes = baseRoleGetRouters(permissionRouter, GetRole.split(",")) // 3.利用global属性,让渲染菜单的组件sideMeuns.vue重新生成左侧菜单 global.antRouter = fixedRouter.concat(getRoutes) // 4.将生成好的路由addRoutes router.addRoutes(fixedRouter.concat(getRoutes)) // 5.push之后,会重新进入到beforeEach的钩子里,直接进入第一个if判断 router.push({ path: to.path }) } } else { // 用户没登录,跳转到登录页面 if (to.path === ‘/’) { next() } else { next(’/’) } }})整体流程走完了,再容易让人蒙的地方1.根据路由进行菜单展示代码位置:/src/components/sideMeuns.vue,先看下elementUI菜单组件,把一些基础的参数先了解一下,这里我把菜单渲染写成了一个组件:用到了递归属性,保证可以生成多级菜单,我建议不熟悉的,大家用组件先模拟着写一个包含跳转功能、icon展示的菜单,然后再看我写的组件2.用户退出系统代码位置:/src/components/layout.vue退出的时候,记得清除掉存在localStorage的用户角色,然后利用 window.location.href = “/“跳转到登录页,为什么要用location.href,这样会把之前addRoutes的路由清除掉,确保下个用户登陆后,会重新渲染正确的菜单如果有些地方不理解,师兄建议把不理解的点先单独拿出来跑跑,或者看看这篇文章的思路来源:手把手…如有不正确的地方,还望小伙伴指正哈 ...

April 1, 2019 · 1 min · jiezi

Vue + TypeScript + Element 项目实战及踩坑记

前言本文讲解如何在 Vue 项目中使用 TypeScript 来搭建并开发项目,并在此过程中踩过的坑 。 TypeScript 具有类型系统,且是 JavaScript 的超集,TypeScript 在 2018年 势头迅猛,可谓遍地开花。Vue3.0 将使用 TS 重写,重写后的 Vue3.0 将更好的支持 TS。2019 年 TypeScript 将会更加普及,能够熟练掌握 TS,并使用 TS 开发过项目,将更加成为前端开发者的优势。所以笔者就当然也要学这个必备技能,就以 边学边实践 的方式,做个博客项目来玩玩。此项目是基于 Vue 全家桶 + TypeScript + Element-UI 的技术栈,且已经开源,github 地址 blog-vue-typescript 。因为之前写了篇纯 Vue 项目搭建的相关文章 基于vue+mint-ui的mobile-h5的项目说明 ,有不少人加我微信,要源码来学习,但是这个是我司的项目,不能提供原码。所以做一个不是我司的项目,且又是 vue 相关的项目来练手并开源吧。1. 效果效果图:pc 端移动端完整效果请看:https://biaochenxuying.cn2. 功能已经完成功能[x] 登录[x] 注册[x] 文章列表[x] 文章归档[x] 标签[x] 关于[x] 点赞与评论[x] 留言[x] 历程[x] 文章详情(支持代码语法高亮)[x] 文章详情目录[x] 移动端适配[x] github 授权登录待优化或者实现[ ] 使用 vuex-class[ ] 更多 TypeScript 的优化技巧[ ] 服务器渲染 SSR3. 前端主要技术所有技术都是当前最新的。vue: ^2.6.6typescript : ^3.2.1element-ui: 2.6.3vue-router : ^3.0.1webpack: 4.28.4vuex: ^3.0.1axios:0.18.0redux: 4.0.0highlight.js: 9.15.6marked:0.6.14. 5 分钟上手 TypeScript如果没有一点点基础,可能没学过 TypeScript 的读者会看不懂往下的内容,所以先学点基础。TypeScript 的静态类型检查是个好东西,可以避免很多不必要的错误, 不用在调试或者项目上线的时候才发现问题 。类型注解TypeScript 里的类型注解是一种轻量级的为函数或变量添加约束的方式。变量定义时也要定义他的类型,比如常见的 :// 布尔值let isDone: boolean = false; // 相当于 js 的 let isDone = false;// 变量定义之后不可以随便变更它的类型isDone = true // 不报错isDone = “我要变为字符串” // 报错// 数字let decLiteral: number = 6; // 相当于 js 的 let decLiteral = 6;// 字符串let name: string = “bob”; // 相当于 js 的 let name = “bob”;// 数组 // 第一种,可以在元素类型后面接上 [],表示由此类型元素组成的一个数组:let list: number[] = [1, 2, 3]; // 相当于 js 的let list = [1, 2, 3];// 第二种方式是使用数组泛型,Array<元素类型>:let list: Array<number> = [1, 2, 3]; // 相当于 js 的let list = [1, 2, 3];// 在 TypeScript 中,我们使用接口(Interfaces)来定义 对象 的类型。interface Person { name: string; age: number;}let tom: Person = { name: ‘Tom’, age: 25};// 以上 对象 的代码相当于 let tom = { name: ‘Tom’, age: 25};// Any 可以随便变更类型 (当这个值可能来自于动态的内容,比如来自用户输入或第三方代码库)let notSure: any = 4;notSure = “我可以随便变更类型” // 不报错notSure = false; // 不报错// Void 当一个函数没有返回值时,你通常会见到其返回值类型是 voidfunction warnUser(): void { console.log(“This is my warning message”);}// 方法的参数也要定义类型,不知道就定义为 anyfunction fetch(url: string, id : number, params: any): void { console.log(“fetch”);}以上是最简单的一些知识点,更多知识请看 TypeScript 中文官网5. 5 分钟上手 Vue +TypeScriptvue-class-component vue-class-component 对 Vue 组件进行了一层封装,让 Vue 组件语法在结合了 TypeScript 语法之后更加扁平化:<template> <div> <input v-model=“msg”> <p>prop: {{propMessage}}</p> <p>msg: {{msg}}</p> <p>helloMsg: {{helloMsg}}</p> <p>computed msg: {{computedMsg}}</p> <button @click=“greet”>Greet</button> </div></template><script>import Vue from ‘vue’import Component from ‘vue-class-component’@Component({ props: { propMessage: String }})export default class App extends Vue { // initial data msg = 123 // use prop values for initial data helloMsg = ‘Hello, ’ + this.propMessage // lifecycle hook mounted () { this.greet() } // computed get computedMsg () { return ‘computed ’ + this.msg } // method greet () { alert(‘greeting: ’ + this.msg) }}</script>上面的代码跟下面的代码作用是一样的:<template> <div> <input v-model=“msg”> <p>prop: {{propMessage}}</p> <p>msg: {{msg}}</p> <p>helloMsg: {{helloMsg}}</p> <p>computed msg: {{computedMsg}}</p> <button @click=“greet”>Greet</button> </div></template><script>export default { // 属性 props: { propMessage: { type: String } }, data () { return { msg: 123, helloMsg: ‘Hello, ’ + this.propMessage } }, // 声明周期钩子 mounted () { this.greet() }, // 计算属性 computed: { computedMsg () { return ‘computed ’ + this.msg } }, // 方法 methods: { greet () { alert(‘greeting: ’ + this.msg) } },}</script>vue-property-decorator vue-property-decorator 是在 vue-class-component 上增强了更多的结合 Vue 特性的装饰器,新增了这 7 个装饰器:@Emit@Inject@Model@Prop@Provide@Watch@Component (从 vue-class-component 继承)在这里列举几个常用的@Prop/@Watch/@Component, 更多信息,详见官方文档import { Component, Emit, Inject, Model, Prop, Provide, Vue, Watch } from ‘vue-property-decorator’@Componentexport class MyComponent extends Vue { @Prop() propA: number = 1 @Prop({ default: ‘default value’ }) propB: string @Prop([String, Boolean]) propC: string | boolean @Prop({ type: null }) propD: any @Watch(‘child’) onChildChanged(val: string, oldVal: string) { }}上面的代码相当于:export default { props: { checked: Boolean, propA: Number, propB: { type: String, default: ‘default value’ }, propC: [String, Boolean], propD: { type: null } } methods: { onChildChanged(val, oldVal) { } }, watch: { ‘child’: { handler: ‘onChildChanged’, immediate: false, deep: false } }}vuex-classvuex-class :在 vue-class-component 写法中 绑定 vuex 。import Vue from ‘vue’import Component from ‘vue-class-component’import { State, Getter, Action, Mutation, namespace} from ‘vuex-class’const someModule = namespace(‘path/to/module’)@Componentexport class MyComp extends Vue { @State(‘foo’) stateFoo @State(state => state.bar) stateBar @Getter(‘foo’) getterFoo @Action(‘foo’) actionFoo @Mutation(‘foo’) mutationFoo @someModule.Getter(‘foo’) moduleGetterFoo // If the argument is omitted, use the property name // for each state/getter/action/mutation type @State foo @Getter bar @Action baz @Mutation qux created () { this.stateFoo // -> store.state.foo this.stateBar // -> store.state.bar this.getterFoo // -> store.getters.foo this.actionFoo({ value: true }) // -> store.dispatch(‘foo’, { value: true }) this.mutationFoo({ value: true }) // -> store.commit(‘foo’, { value: true }) this.moduleGetterFoo // -> store.getters[‘path/to/module/foo’] }}6. 用 vue-cli 搭建 项目笔者使用最新的 vue-cli 3 搭建项目,详细的教程,请看我之前写的 vue-cli3.x 新特性及踩坑记,里面已经有详细讲解 ,但文章里面的配置和此项目不同的是,我加入了 TypeScript ,其他的配置都是 vue-cli 本来配好的了。详情请看 vue-cli 官网 。6.1 安装及构建项目目录安装的依赖:安装过程选择的一些配置:搭建好之后,初始项目结构长这样:├── public // 静态页面├── src // 主目录 ├── assets // 静态资源 ├── components // 组件 ├── views // 页面 ├── App.vue // 页面主入口 ├── main.ts // 脚本主入口 ├── router.ts // 路由 ├── shims-tsx.d.ts // 相关 tsx 模块注入 ├── shims-vue.d.ts // Vue 模块注入 └── store.ts // vuex 配置├── tests // 测试用例├── .eslintrc.js // eslint 相关配置├── .gitignore // git 忽略文件配置├── babel.config.js // babel 配置├── postcss.config.js // postcss 配置├── package.json // 依赖└── tsconfig.json // ts 配置奔着 大型项目的结构 来改造项目结构,改造后 :├── public // 静态页面├── src // 主目录 ├── assets // 静态资源 ├── filters // 过滤 ├── store // vuex 配置 ├── less // 样式 ├── utils // 工具方法(axios封装,全局方法等) ├── views // 页面 ├── App.vue // 页面主入口 ├── main.ts // 脚本主入口 ├── router.ts // 路由 ├── shime-global.d.ts // 相关 全局或者插件 模块注入 ├── shims-tsx.d.ts // 相关 tsx 模块注入 ├── shims-vue.d.ts // Vue 模块注入, 使 TypeScript 支持 .vue 后缀的文件├── tests // 测试用例├── .eslintrc.js // eslint 相关配置├── postcss.config.js // postcss 配置├── .gitignore // git 忽略文件配置├── babel.config.js // preset 记录├── package.json // 依赖├── README.md // 项目 readme├── tsconfig.json // ts 配置└── vue.config.js // webpack 配置tsconfig.json 文件中指定了用来编译这个项目的根文件和编译选项。本项目的 tsconfig.json 配置如下 :{ // 编译选项 “compilerOptions”: { // 编译输出目标 ES 版本 “target”: “esnext”, // 采用的模块系统 “module”: “esnext”, // 以严格模式解析 “strict”: true, “jsx”: “preserve”, // 从 tslib 导入外部帮助库: 比如__extends,__rest等 “importHelpers”: true, // 如何处理模块 “moduleResolution”: “node”, // 启用装饰器 “experimentalDecorators”: true, “esModuleInterop”: true, // 允许从没有设置默认导出的模块中默认导入 “allowSyntheticDefaultImports”: true, // 定义一个变量就必须给它一个初始值 “strictPropertyInitialization” : false, // 允许编译javascript文件 “allowJs”: true, // 是否包含可以用于 debug 的 sourceMap “sourceMap”: true, // 忽略 this 的类型检查, Raise error on this expressions with an implied any type. “noImplicitThis”: false, // 解析非相对模块名的基准目录 “baseUrl”: “.”, // 给错误和消息设置样式,使用颜色和上下文。 “pretty”: true, // 设置引入的定义文件 “types”: [“webpack-env”, “mocha”, “chai”], // 指定特殊模块的路径 “paths”: { “@/”: [“src/”] }, // 编译过程中需要引入的库文件的列表 “lib”: [“esnext”, “dom”, “dom.iterable”, “scripthost”] }, // ts 管理的文件 “include”: [ “src/**/.ts”, “src//*.tsx”, “src//.vue”, “tests/**/.ts”, “tests/**/.tsx” ], // ts 排除的文件 “exclude”: [“node_modules”]}更多配置请看官网的 tsconfig.json 的 编译选项本项目的 vue.config.js:const path = require(“path”);const sourceMap = process.env.NODE_ENV === “development”;module.exports = { // 基本路径 publicPath: “./”, // 输出文件目录 outputDir: “dist”, // eslint-loader 是否在保存的时候检查 lintOnSave: false, // webpack配置 // see https://github.com/vuejs/vue-cli/blob/dev/docs/webpack.md chainWebpack: () => {}, configureWebpack: config => { if (process.env.NODE_ENV === “production”) { // 为生产环境修改配置… config.mode = “production”; } else { // 为开发环境修改配置… config.mode = “development”; } Object.assign(config, { // 开发生产共同配置 resolve: { extensions: [".js", “.vue”, “.json”, “.ts”, “.tsx”], alias: { vue$: “vue/dist/vue.js”, “@”: path.resolve(__dirname, “./src”) } } }); }, // 生产环境是否生成 sourceMap 文件 productionSourceMap: sourceMap, // css相关配置 css: { // 是否使用css分离插件 ExtractTextPlugin extract: true, // 开启 CSS source maps? sourceMap: false, // css预设器配置项 loaderOptions: {}, // 启用 CSS modules for all css / pre-processor files. modules: false }, // use thread-loader for babel & TS in production build // enabled by default if the machine has more than 1 cores parallel: require(“os”).cpus().length > 1, // PWA 插件相关配置 // see https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-pwa pwa: {}, // webpack-dev-server 相关配置 devServer: { open: process.platform === “darwin”, host: “localhost”, port: 3001, //8080, https: false, hotOnly: false, proxy: { // 设置代理 // proxy all requests starting with /api to jsonplaceholder “/api”: { // target: “https://emm.cmccbigdata.com:8443/", target: “http://localhost:3000/”, // target: “http://47.106.136.114/”, changeOrigin: true, ws: true, pathRewrite: { “^/api”: "” } } }, before: app => {} }, // 第三方插件配置 pluginOptions: { // … }};6.2 安装 element-ui本来想搭配 iview-ui 来用的,但后续还想把这个项目搞成 ssr 的,而 vue + typescript + iview + Nuxt.js 的服务端渲染还有不少坑, 而 vue + typescript + element + Nuxt.js 对 ssr 的支持已经不错了,所以选择了 element-ui 。安装:npm i element-ui -S按需引入, 借助 babel-plugin-component,我们可以只引入需要的组件,以达到减小项目体积的目的。npm install babel-plugin-component -D然后,将 babel.config.js 修改为:module.exports = { presets: ["@vue/app"], plugins: [ [ “component”, { libraryName: “element-ui”, styleLibraryName: “theme-chalk” } ] ]};接下来,如果你只希望引入部分组件,比如 Button 和 Select,那么需要在 main.js 中写入以下内容:import Vue from ‘vue’;import { Button, Select } from ’element-ui’;import App from ‘./App.vue’;Vue.component(Button.name, Button);Vue.component(Select.name, Select);/ 或写为 * Vue.use(Button) * Vue.use(Select) /new Vue({ el: ‘#app’, render: h => h(App)});6.3 完善项目目录与文件route使用路由懒加载功能。export default new Router({ mode: “history”, routes: [ { path: “/”, name: “home”, component: () => import(/ webpackChunkName: “home” / “./views/home.vue”) }, { path: “/articles”, name: “articles”, // route level code-splitting // this generates a separate chunk (articles.[hash].js) for this route // which is lazy-loaded when the route is visited. component: () => import(/ webpackChunkName: “articles” / “./views/articles.vue”) }, ]});utilsutils/utils.ts 常用函数的封装, 比如 事件的节流(throttle)与防抖(debounce)方法:// fn是我们需要包装的事件回调, delay是时间间隔的阈值export function throttle(fn: Function, delay: number) { // last为上一次触发回调的时间, timer是定时器 let last = 0, timer: any = null; // 将throttle处理结果当作函数返回 return function() { // 保留调用时的this上下文 let context = this; // 保留调用时传入的参数 let args = arguments; // 记录本次触发回调的时间 let now = +new Date(); // 判断上次触发的时间和本次触发的时间差是否小于时间间隔的阈值 if (now - last < delay) { // 如果时间间隔小于我们设定的时间间隔阈值,则为本次触发操作设立一个新的定时器 clearTimeout(timer); timer = setTimeout(function() { last = now; fn.apply(context, args); }, delay); } else { // 如果时间间隔超出了我们设定的时间间隔阈值,那就不等了,无论如何要反馈给用户一次响应 last = now; fn.apply(context, args); } };}utils/config.ts 配置文件,比如 github 授权登录的回调地址、client_id、client_secret 等。const config = { ‘oauth_uri’: ‘https://github.com/login/oauth/authorize', ‘redirect_uri’: ‘https://biaochenxuying.cn/login', ‘client_id’: ‘XXXXXXXXXX’, ‘client_secret’: ‘XXXXXXXXXX’,};// 本地开发环境下if (process.env.NODE_ENV === ‘development’) { config.redirect_uri = “http://localhost:3001/login” config.client_id = “502176cec65773057a9e” config.client_secret = “65d444de381a026301a2c7cffb6952b9a86ac235”}export default config;如果你的生产环境也要 github 登录授权的话,请在 github 上申请一个 Oauth App ,把你的 redirect_uri,client_id,client_secret 的信息填在 config 里面即可。具体详情请看我写的这篇文章 github 授权登录教程与如何设计第三方授权登录的用户表utils/urls.ts 请求接口地址,统一管理。// url的链接export const urls: object = { login: “login”, register: “register”, getArticleList: “getArticleList”,};export default urls;utils/https.ts axios 请求的封装。import axios from “axios”;// 创建axios实例let service: any = {};service = axios.create({ baseURL: “/api”, // api的base_url timeout: 50000 // 请求超时时间 });// request拦截器 axios的一些配置service.interceptors.request.use( (config: any) => { return config; }, (error: any) => { // Do something with request error console.error(“error:”, error); // for debug Promise.reject(error); });// respone拦截器 axios的一些配置service.interceptors.response.use( (response: any) => { return response; }, (error: any) => { console.error(“error:” + error); // for debug return Promise.reject(error); });export default service;把 urls 和 https 挂载到 main.ts 里面的 Vue 的 prototype 上面。import service from “./utils/https”;import urls from “./utils/urls”;Vue.prototype.$https = service; // 其他页面在使用 axios 的时候直接 this.$http 就可以了Vue.prototype.$urls = urls; // 其他页面在使用 urls 的时候直接 this.$urls 就可以了然后就可以统一管理接口,而且调用起来也很方便啦。比如下面 文章列表的请求。async handleSearch() { this.isLoading = true; const res: any = await this.$https.get(this.$urls.getArticleList, { params: this.params }); this.isLoading = false; if (res.status === 200) { if (res.data.code === 0) { const data: any = res.data.data; this.articlesList = […this.articlesList, …data.list]; this.total = data.count; this.params.pageNum++; if (this.total === this.articlesList.length) { this.isLoadEnd = true; } } else { this.$message({ message: res.data.message, type: “error” }); } } else { this.$message({ message: “网络错误!”, type: “error” }); } }store ( Vuex )一般大型的项目都有很多模块的,比如本项目中有公共信息(比如 token )、 用户模块、文章模块。├── modules // 模块 ├── user.ts // 用户模块 ├── article.ts // 文章模块 ├── types.ts // 类型└── index.ts // vuex 主入口store/index.ts 存放公共的信息,并导入其他模块import Vue from “vue”;import Vuex from “vuex”;import * as types from “./types”;import user from “./modules/user”;import article from “./modules/article”;Vue.use(Vuex);const initPageState = () => { return { token: "" };};const store = new Vuex.Store({ strict: process.env.NODE_ENV !== “production”, // 具体模块 modules: { user, article }, state: initPageState(), mutations: { [types.SAVE_TOKEN](state: any, pageState: any) { for (const prop in pageState) { state[prop] = pageState[prop]; } } }, actions: {}});export default store;types.ts// 公共 tokenexport const SAVE_TOKEN = “SAVE_TOKEN”;// 用户export const SAVE_USER = “SAVE_USER”;user.tsimport * as types from “../types”;const initPageState = () => { return { userInfo: { _id: “”, name: “”, avator: "" } };};const user = { state: initPageState(), mutations: { [types.SAVE_USER](state: any, pageState: any) { for (const prop in pageState) { state[prop] = pageState[prop]; } } }, actions: {}};export default user;7. markdown 渲染markdown 渲染效果图: markdown 渲染 采用了开源的 marked, 代码高亮用了 highlight.js 。用法:第一步:npm i marked highlight.js –savenpm i marked highlight.js –save第二步: 导入封装成 markdown.js,将文章详情由字符串转成 html, 并抽离出文章目录。marked 的封装 得感谢这位老哥。const highlight = require(“highlight.js”);const marked = require(“marked”);const tocObj = { add: function(text, level) { var anchor = #toc${level}${++this.index}; this.toc.push({ anchor: anchor, level: level, text: text }); return anchor; }, // 使用堆栈的方式处理嵌套的ul,li,level即ul的嵌套层次,1是最外层 // <ul> // <li></li> // <ul> // <li></li> // </ul> // <li></li> // </ul> toHTML: function() { let levelStack = []; let result = “”; const addStartUL = () => { result += ‘<ul class=“anchor-ul” id=“anchor-fix”>’; }; const addEndUL = () => { result += “</ul>\n”; }; const addLI = (anchor, text) => { result += ‘<li><a class=“toc-link” href="#’ + anchor + ‘">’ + text + “<a></li>\n”; }; this.toc.forEach(function(item) { let levelIndex = levelStack.indexOf(item.level); // 没有找到相应level的ul标签,则将li放入新增的ul中 if (levelIndex === -1) { levelStack.unshift(item.level); addStartUL(); addLI(item.anchor, item.text); } // 找到了相应level的ul标签,并且在栈顶的位置则直接将li放在此ul下 else if (levelIndex === 0) { addLI(item.anchor, item.text); } // 找到了相应level的ul标签,但是不在栈顶位置,需要将之前的所有level出栈并且打上闭合标签,最后新增li else { while (levelIndex–) { levelStack.shift(); addEndUL(); } addLI(item.anchor, item.text); } }); // 如果栈中还有level,全部出栈打上闭合标签 while (levelStack.length) { levelStack.shift(); addEndUL(); } // 清理先前数据供下次使用 this.toc = []; this.index = 0; return result; }, toc: [], index: 0};class MarkUtils { constructor() { this.rendererMD = new marked.Renderer(); this.rendererMD.heading = function(text, level, raw) { var anchor = tocObj.add(text, level); return &lt;h${level} id=${anchor}&gt;${text}&lt;/h${level}&gt;\n; }; highlight.configure({ useBR: true }); marked.setOptions({ renderer: this.rendererMD, headerIds: false, gfm: true, tables: true, breaks: false, pedantic: false, sanitize: false, smartLists: true, smartypants: false, highlight: function(code) { return highlight.highlightAuto(code).value; } }); } async marked(data) { if (data) { let content = await marked(data); // 文章内容 let toc = tocObj.toHTML(); // 文章目录 return { content: content, toc: toc }; } else { return null; } }}const markdown = new MarkUtils();export default markdown;第三步: 使用import markdown from “@/utils/markdown”;// 获取文章详情async handleSearch() { const res: any = await this.$https.post( this.$urls.getArticleDetail, this.params ); if (res.status === 200) { if (res.data.code === 0) { this.articleDetail = res.data.data; // 使用 marked 转换 const article = markdown.marked(res.data.data.content); article.then((response: any) => { this.articleDetail.content = response.content; this.articleDetail.toc = response.toc; }); } else { // … } else { // … } }// 渲染<div id=“content” class=“article-detail” v-html=“articleDetail.content”></div>第四步:引入 monokai_sublime 的 css 样式<link href=“http://cdn.bootcss.com/highlight.js/8.0/styles/monokai_sublime.min.css" rel=“stylesheet”>第五步:对 markdown 样式的补充如果不补充样式,是没有黑色背景的,字体大小等也会比较小,图片也不会居中显示/对 markdown 样式的补充/pre { display: block; padding: 10px; margin: 0 0 10px; font-size: 14px; line-height: 1.42857143; color: #abb2bf; background: #282c34; word-break: break-all; word-wrap: break-word; overflow: auto;}h1,h2,h3,h4,h5,h6{ margin-top: 1em; / margin-bottom: 1em; /}strong { font-weight: bold;}p > code:not([class]) { padding: 2px 4px; font-size: 90%; color: #c7254e; background-color: #f9f2f4; border-radius: 4px;}p img{ / 图片居中 */ margin: 0 auto; display: flex;}#content { font-family: “Microsoft YaHei”, ‘sans-serif’; font-size: 16px; line-height: 30px;}#content .desc ul,#content .desc ol { color: #333333; margin: 1.5em 0 0 25px;}#content .desc h1, #content .desc h2 { border-bottom: 1px solid #eee; padding-bottom: 10px;}#content .desc a { color: #009a61;}8. 注意点关于 页面对于 关于 的页面,其实是一篇文章来的,根据文章类型 type 来决定的,数据库里面 type 为 3 的文章,只能有一篇就是 博主介绍 ;达到了想什么时候修改内容都可以。所以当 当前路由 === ‘/about’ 时就是请求类型为 博主介绍 的文章。type: 3, // 文章类型: 1:普通文章;2:是博主简历;3 :是博主简介;移动端适配移动端使用 rem 单位适配。// 屏幕适配( window.screen.width / 移动端设计稿宽 * 100)也即是 (window.screen.width / 750 * 100) ——*100 为了方便计算。即 font-size 值是手机 deviceWidth 与设计稿比值的 100 倍document.getElementsByTagName(‘html’)[0].style.fontSize = window.screen.width / 7.5 + ‘px’;如上:通过查询屏幕宽度,动态的设置 html 的 font-size 值,移动端的设计稿大多以宽为 750 px 来设置的。比如在设计图上一个 150 * 250 的盒子(单位 px):原本在 css 中的写法:width: 150px;heigth: 250px;通过上述换算后,在 css 中对应的 rem 值只需要写:width: 1.5rem; // 150 / 100 remheigth: 2.5rem; // 250 / 100 rem如果你的移动端的设计稿是以宽为 1080 px 来设置的话,就用 window.screen.width / 10.8 吧。9. 踩坑记1. 让 vue 识别全局方法/变量我们经常在 main.ts 中给 vue.prototype 挂载实例或者内容,以方便在组件里面使用。import service from “./utils/https”;import urls from “./utils/urls”;Vue.prototype.$https = service; // 其他页面在使用 axios 的时候直接 this.$http 就可以了Vue.prototype.$urls = urls; // 其他页面在使用 urls 的时候直接 this.$urls 就可以了然而当你在组件中直接 this.$http 或者 this.$urls 时会报错的,那是因为 $http 和 $urls 属性,并没有在 vue 实例中声明。再比如使用 Element-uI 的 meesage。import { Message } from “element-ui”;Vue.prototype.$message = Message;之前用法如下图: this.$message({ message: ‘恭喜你,这是一条成功消息’, type: ‘success’ })然而还是会报错的。再比如 监听路由的变化:import { Vue, Watch } from “vue-property-decorator”;import Component from “vue-class-component”;import { Route } from “vue-router”;@Componentexport default class App extends Vue { @Watch("$route”) routeChange(val: Route, oldVal: Route) { // do something }}只是这样写的话,监听 $route 还是会报错的。想要以上三种做法都正常执行,就还要补充如下内容:在 src 下的 shims-vue.d.ts 中加入要挂载的内容。 表示 vue 里面的 this 下有这些东西。import VueRouter, { Route } from “vue-router”;declare module “vue/types/vue” { interface Vue { $router: VueRouter; // 这表示this下有这个东西 $route: Route; $https: any; // 不知道类型就定为 any 吧(偷懒) $urls: any; $Message: any; }}2. 引入的模块要声明比如 在组件里面使用 window.document 或者 document.querySelector 的时候会报错的,npm run build 不给通过。再比如:按需引用 element 的组件与动画组件:import { Button } from “element-ui”;import CollapseTransition from “element-ui/lib/transitions/collapse-transition”;npm run serve 时可以执行,但是在 npm run build 的时候,会直接报错的,因为没有声明。正确做法:我在 src 下新建一个文件 shime-global.d.ts ,加入内容如下:// 声明全局的 window ,不然使用 window.XX 时会报错declare var window: Window;declare var document: Document;declare module “element-ui/lib/transitions/collapse-transition”;declare module “element-ui”;当然,这个文件你加在其他地方也可以,起其他名字都 OK。但是即使配置了以上方法之后,有些地方使用 document.XXX ,比如 document.title 的时候,npm run build 还是通过不了,所以只能这样了:<script lang=“ts”>// 在用到 document.XXX 的文件中声明一下即可declare var document: any;// 此处省略 XXXX 多的代码</script>3. this 的类型检查比如之前的 事件的节流(throttle)与防抖(debounce)方法:export function throttle(fn: Function, delay: number) { return function() { // 保留调用时的 this 上下文 let context = this;}function 里面的 this 在 npm run serve 时会报错的,因为 tyescript 检测到它不是在类(class)里面。正确做法:在根目录的 tsconfig.json 里面加上 “noImplicitThis”: false ,忽略 this 的类型检查。// 忽略 this 的类型检查, Raise error on this expressions with an implied any type.“noImplicitThis”: false,4. import 的 .vue 文件import .vue 的文件的时候,要补全 .vue 的后缀,不然 npm run build 会报错的。比如:import Nav from “@/components/nav”; // @ is an alias to /srcimport Footer from “@/components/footer”; // @ is an alias to /src要修改为:import Nav from “@/components/nav.vue”; // @ is an alias to /srcimport Footer from “@/components/footer.vue”; // @ is an alias to /src5. 装饰器 @Component报错。<script lang=“ts”>import { Vue, Component } from “vue-property-decorator”;export default class LoadingCustom extends Vue {}</script>以下才是正确,因为这里的 Vue 是从 vue-property-decorator import 来的。<script lang=“ts”>import { Vue, Component } from “vue-property-decorator”;@Componentexport default class LoadingCustom extends Vue {}</script>6. 路由的组件导航守卫失效vue-class-component 官网里面的路由的导航钩子的用法是没有效果的 Adding Custom Hooks路由的导航钩子不属于 Vue 本身,这会导致 class 组件转义到配置对象时导航钩子无效,因此如果要使用导航钩子需要在 router 的配置里声明(网上别人说的,还没实践,不确定是否可行)。7. tsconfig.json 的 strictPropertyInitialization 设为 false,不然你定义一个变量就必须给它一个初始值。position: sticky;本项目中的文章详情的目录就是用了 sticky。.anchor { position: sticky; top: 213px; margin-top: 213px;}position:sticky 是 css 定位新增属性;可以说是相对定位 relative 和固定定位 fixed 的结合;它主要用在对 scroll 事件的监听上;简单来说,在滑动过程中,某个元素距离其父元素的距离达到 sticky 粘性定位的要求时(比如 top:100px );position:sticky 这时的效果相当于 fixed 定位,固定到适当位置。用法像上面那样用即可,但是有使用条件:1、父元素不能 overflow:hidden 或者 overflow:auto 属性。2、必须指定 top、bottom、left、right 4 个值之一,否则只会处于相对定位3、父元素的高度不能低于 sticky 元素的高度4、sticky 元素仅在其父元素内生效8. eslint 报找不到文件和装饰器的错App.vue 中只是写了引用文件而已,而且 webpack 和 tsconfig.josn 里面已经配置了别名了的。import Nav from “@/components/nav.vue”; // @ is an alias to /srcimport Slider from “@/components/slider.vue”; // @ is an alias to /srcimport Footer from “@/components/footer.vue”; // @ is an alias to /srcimport ArrowUp from “@/components/arrowUp.vue”; // @ is an alias to /srcimport { isMobileOrPc } from “@/utils/utils”;但是,还是会报如下的错:只是代码不影响文件的打包,而且本地与生产环境的代码也正常,没报错而已。这个 eslint 的检测目前还没找到相关的配置可以把这些错误去掉。9. 路由模式修改为 history因为文章详情页面有目录,点击目录时定位定相应的内容,但是这个目录定位内容是根据锚点来做的,如果路由模式为 hash 模式的话,本来文章详情页面的路由就是 #articleDetail 了,再点击目录的话(比如 #title2 ),会在 #articleDetail 后面再加上 #title2,一刷新会找不到这个页面的。10. Build Setup # clonegit clone https://github.com/biaochenxuying/blog-vue-typescript.git# cdcd blog-vue-typescript# install dependenciesnpm install# Compiles and hot-reloads for developmentnpm run serve# Compiles and minifies for productionnpm run build### Run your testsnpm run test### Lints and fixes filesnpm run lint### Run your unit testsnpm run test:unitCustomize configurationSee Configuration Reference.如果要看有后台数据完整的效果,是要和后台项目 blog-node 一起运行才行的,不然接口请求会失败。虽然引入了 mock 了,但是还没有时间做模拟数据,想看具体效果,请稳步到我的网站上查看 https://biaochenxuying.cn11. 项目地址与系列相关文章基于 Vue + TypeScript + Element 的 blog-vue-typescript 前台展示: https://github.com/biaochenxuying/blog-vue-typescript基于 react + node + express + ant + mongodb 的博客前台,这个是笔者之前做的,效果和这个类似,地址如下: blog-react 前台展示: https://github.com/biaochenxuying/blog-react推荐阅读 :本博客系统的系列文章:react + node + express + ant + mongodb 的简洁兼时尚的博客网站react + Ant Design + 支持 markdown 的 blog-react 项目文档说明基于 node + express + mongodb 的 blog-node 项目文档说明服务器小白的我,是如何将node+mongodb项目部署在服务器上并进行性能优化的github 授权登录教程与如何设计第三方授权登录的用户表一次网站的性能优化之路 – 天下武功,唯快不破Vue + TypeScript + Element 搭建简洁时尚的博客网站及踩坑记12. 最后笔者也是初学 TS ,如果文章有错的地方,请指出,感谢。一开始用 Vue + TS 来搭建时,我也是挺抵触的,因为踩了好多坑,而且很多类型检查方面也挺烦人。后面解决了,明白原理之后,是越用越爽,哈哈。权衡如何更好的利用 JS 的动态性和 TS 的静态特质,我们需要结合项目的实际情况来进行综合判断。一些建议:如果是中小型项目,且生命周期不是很长,那就直接用 JS 吧,不要被 TS 束缚住了手脚。如果是大型应用,且生命周期比较长,那建议试试 TS。如果是框架、库之类的公共模块,那更建议用 TS 了。至于到底用不用TS,还是要看实际项目规模、项目生命周期、团队规模、团队成员情况等实际情况综合考虑。其实本项目也是小项目来的,其实并不太适合加入 TypeScript ,不过这个项目是个人的项目,是为了练手用的,所以就无伤大大雅。未来,class-compoent 也将成为主流,现在写 TypeScript 以后进行 3.0 的迁移会更加方便。每天下班后,用几个晚上的时间来写这篇文章,码字不易,如果您觉得这篇文章不错或者对你有所帮助,请给个赞或者星吧,你的点赞就是我继续创作的最大动力。参考文章:vue + typescript 项目起手式TypeScript + 大型项目实战欢迎关注以下公众号,学到不一样的 武功秘籍 !关注公众号并回复 福利 可领取免费学习资料,福利详情请猛戳: 免费资源获取–Python、Java、Linux、Go、node、vue、react、javaScript ...

March 31, 2019 · 14 min · jiezi

数据驱动,快速开发组件(ElementUI篇)

在日常开发中,我们肯定不止一次碰到重复的业务代码,明明功能相似,但总没思路去把它封装成组件。关于封装组件,希望这篇文章能带给大家新的思路,去更高效的完成日常开发。(注:例子都是基于ElementUI, 但思路都是一样的)示例地址-> https://www.lyh.red.admin代码地址数据驱动构建页面:设计数据结构(绑定value,绑定事件,相关属性)-> 生成dom -> dom绑定相关监听事件:操作UI -> 触发事件 -> 更新数据 -> 更新UI数据驱动是基于数据触发的,在编写业务的时候,只需要编写好组件的dom结构,之后我们便可以不用再去关心dom层,只需要关心数据就ok。基于这种思路,那留给我们的只有两步,组件设计和数据设计。先看看效果搜索栏配置以及生成效果 // 过滤相关配置 filterInfo: { query: { create_user: ‘’, account: ‘’, name: ’’ }, list: [ {type: ‘input’, label: ‘账户’, value: ‘account’}, {type: ‘input’, label: ‘用户名’, value: ’name’}, // {type: ‘select’, label: ‘创建人’, value: ‘create_user’}, // {type: ‘date’, label: ‘创建时间’, value: ‘create_time’}, {type: ‘button’, label: ‘搜索’, btType: ‘primary’, icon: ’el-icon-search’, event: ‘search’, show: true}, {type: ‘button’, label: ‘添加’, btType: ‘primary’, icon: ’el-icon-plus’, event: ‘add’, show: true} ] }表格配置以及生成效果 // 表格相关 tableInfo: { refresh: false, initCurpage: false, data: [], fieldList: [ {label: ‘账号’, value: ‘account’}, {label: ‘用户名’, value: ’name’}, {label: ‘性别’, value: ‘sex’, width: 80, list: ‘sexList’}, {label: ‘账号类型’, value: ’type’, width: 100, list: ‘accountTypeList’}, {label: ‘状态’, value: ‘status’, width: 90, list: ‘statusList’}, {label: ‘创建人’, value: ‘create_user’}, {label: ‘创建时间’, value: ‘create_time’, minWidth: 180}, {label: ‘更新人’, value: ‘update_user’}, {label: ‘更新时间’, value: ‘update_time’, minWidth: 180} ], handle: { fixed: ‘right’, label: ‘操作’, width: ‘180’, btList: [ {label: ‘编辑’, type: ‘’, icon: ’el-icon-edit’, event: ‘update’, show: true}, {label: ‘删除’, type: ‘danger’, icon: ’el-icon-delete’, event: ‘delete’, show: true} ] } }dom配置和完整页面<template> <div class=“app-container”> <!– 条件栏 –> <page-filter :query.sync=“filterInfo.query” :filterList=“filterInfo.list” :listTypeInfo=“listTypeInfo” @handleClickBt=“handleClickBt” @handleEvent=“handleEvent”> </page-filter> <!– 表格 –> <page-table :refresh=“tableInfo.refresh” :initCurpage=“tableInfo.initCurpage” :data.sync=“tableInfo.data” :api=“getListApi” :query=“filterInfo.query” :fieldList=“tableInfo.fieldList” :listTypeInfo=“listTypeInfo” :handle=“tableInfo.handle” @handleClickBt=“handleClickBt” @handleEvent=“handleEvent”> </page-table> <!– 弹窗 –> <page-dialog :title=“dialogInfo.title[dialogInfo.type]” :visible.sync=“dialogInfo.visible” :width=“dialogInfo.width” :btLoading=“dialogInfo.btLoading” :btList=“dialogInfo.btList” @handleClickBt=“handleClickBt” @handleEvent=“handleEvent”> <!– form –> <page-form :refObj.sync=“formInfo.ref” :data=“formInfo.data” :fieldList=“formInfo.fieldList” :rules=“formInfo.rules” :labelWidth=“formInfo.labelWidth” :listTypeInfo=“listTypeInfo”> </page-form> </page-dialog> </div></template>封装一个搜索栏(功能栏)组件根据需求设计数据结构参数设计搜索参数query,比如要查询的参数有账号,名字。dom相关属性设计首先要考虑dom的类型,和显示,这是基本的,还有扩展类型,比如事件可以设置event属性,是否显示设置show属性,这些是比较通用的。而基于不同类型的dom,如果是input,select,datetime类型的dom,作为一个承载数据的容器,则需要一个value属性去和query中的属性名对上,除此之外不同类型的dom还有不同的特定属性,比如select需要提供对应的list,datetime需要相关的pickersOptions去限制时间范围,如果是按钮,比如el-button,则可以设置icon,按钮相关type。最终实现:filterInfo: { query: { create_user: ‘’, account: ‘’, name: ’’ }, list: [ {type: ‘input’, label: ‘账户’, value: ‘account’}, {type: ‘input’, label: ‘用户名’, value: ’name’}, // {type: ‘select’, label: ‘创建人’, value: ‘create_user’}, // {type: ‘date’, label: ‘创建时间’, value: ‘create_time’}, {type: ‘button’, label: ‘搜索’, btType: ‘primary’, icon: ’el-icon-search’, event: ‘search’, show: true}, {type: ‘button’, label: ‘添加’, btType: ‘primary’, icon: ’el-icon-plus’, event: ‘add’, show: true} ] }循环的dom列表设计dom结构先考虑设计的这个dom需要什么属性比如dom是el-input,一个输入框,可以设置是否禁止disabled,可以设置是否可清空clearable,v-model要绑定的数据,设置dom的class名,设置dom绑定的事件。比如dom是el-select, 除了上面这些属性,我们还需要这个元素可循环的list最终dom结构为: <div class=“filter-item” v-for="(item, index) in getConfigList()" :key=“index”> <!– <label class=“filter-label” v-if=“item.type !== ‘button’">{{item.key}}</label> –> <!– 输入框 –> <el-input :class="filter-${item.type}” v-if=“item.type === ‘input’” :type=“item.type” :disabled=“item.disabled” :clearable=“item.clearable || true” :placeholder=“getPlaceholder(item)” @focus=“handleEvent(item.event)” v-model=“searchQuery[item.value]"> </el-input> <!– 选择框 –> <el-select :class="filter-${item.type}” v-if=“item.type === ‘select’” v-model=“searchQuery[item.value]” :disabled=“item.disabled” @change=“handleEvent(item.even)” :clearable=“item.clearable || true” :filterable=“item.filterable || true” :placeholder=“getPlaceholder(item)"> <el-option v-for="(item ,index) in listTypeInfo[item.list]” :key=“index” :label=“item.key” :value=“item.value”></el-option> </el-select> <!– 时间选择框 –> <el-time-select :class="filter-${item.type}" v-if=“item.type === ’time’” v-model=“searchQuery[item.value]” :picker-options=“item.TimePickerOptions” :clearable=“item.clearable || true” :disabled=“item.disabled” :placeholder=“getPlaceholder(item)"> </el-time-select> <!– 日期选择框 –> <el-date-picker :class="filter-${item.type}” v-if=“item.type === ‘date’” v-model=“searchQuery[item.value]” :picker-options=“item.datePickerOptions || datePickerOptions” :type=“item.dateType” :clearable=“item.clearable || true” :disabled=“item.disabled” @focus=“handleEvent(item.event)” :placeholder=“getPlaceholder(item)"> </el-date-picker> <!– 按钮 –> <el-button :class="filter-${item.type}” v-else-if=“item.type === ‘button’” v-waves :type=“item.btType” :icon=“item.icon” @click=“handleClickBt(item.event)">{{item.label}}</el-button> </div> </div>事件的处理事件怎么绑定在dom上绑定事件,可以在数据结构中给dom设置一个event属性,比如说是查询search,在dom结构中我们可以设计一个中间层函数去处理,比如:<!– 按钮 –> <el-button :class="filter-${item.type}” v-else-if=“item.type === ‘button’” v-waves :type=“item.btType” :icon=“item.icon” @click=“handleClickBt(item.event)">{{item.label}}</el-button>中间层函数接收事件类型,然后统一处理。组件中的函数,外部怎么处理我觉得组件的话,就承载一个去重复的作用,将所以重复的事情去除就可以,像如果是表格,表单,功能栏类似这种可能显示重复但是事件多变性的组件,我们则可以考虑将它们的事件派发到业务相关页面处理,组件保持去除重复的工作,简单干净明了就好了。将事件全部交给父级处理: // 绑定的相关事件 handleEvent (evnet) { this.$emit(‘handleEvent’, evnet) }, // 派发按钮点击事件 handleClickBt (event, data) { this.$emit(‘handleClickBt’, event, data) }封装一个tree组件在后台管理页面树状组件用到次数实在太多了,静态的树数据加载,动态的树数据懒加载,左键点击事件,右键点击事件等等,封装之后,哼哼,谁用谁知道,一个字,爽。设计属性其实就是将elementui中的大部分用上的tree属性加上,然后再设计一部分让组件更加好用的属性,部分举个例子。属性类型描述lazyBoolean是否懒加载lazyInfoArray懒加载相关数据loadInfoObject正常相关数据rightClickBoolean是否需要右键菜单rightMenuListArray右键菜单列表懒加载数据和正常加载数据结构的详细设计 /** * 懒加载相关数据 * key -> 唯一标识 label -> 显示 type -> 类型 api -> 接口 params -> 参数 leaf -> 是否叶子节点 / lazyInfo: { type: Array, default: () => { return [ { key: ‘id’, label: ’name’, type: ‘’, api: () => {}, params: {key: ‘’, value: ‘’, type: ‘url’}, // url/query->{data: [{key: ‘’, value: ‘’, default: ‘’}] type: ‘query’} leaf: true } ] } }, /* * 正常加载相关 */ loadInfo: { key: ‘id’, label: ’name’, api: () => {}, params: {key: ‘’, value: ‘’, type: ‘url’} // url/query->{data: [{key: ‘’, value: ‘’, default: ‘’}] type: ‘query’} },事件处理事件处理同样是需要派发到父级处理,以保证组件的可复用性,通过中间件将树组件的相关事件派发搭到父级。实现效果懒加载树组件相关数据配置: // 树相关信息 treeInfo: { refresh: false, refreshLevel: 0, nodeKey: ‘key’, lazy: true, type: 0, // 省市区类型 lazyInfo: [ { key: ‘id’, label: ’name’, type: 1, api: getAllApi, params: {key: ‘pid’, value: 1, type: ‘url’} }, { key: ‘id’, label: ’name’, type: 2, api: getAllApi, params: {key: ‘pid’, value: ‘’, type: ‘url’}, leaf: true } ], rightMenuList: [] },懒加载树dom结构: <div class=“page-tree” v-loading=“treeLoading” @contextmenu.prevent=“handleTreeClick”> <el-tree class=“tree-component disabled-select” ref=“TreeComponent” :show-checkbox=“checkBox” :node-key=“nodeKey” :data=“treeData” :load=“handleLoadNode” :lazy=“lazy” :draggable=“draggable” :allow-drop=“handleDrop” :expand-on-click-node=“false” :check-strictly=“checkStrictly” :filter-node-method=“filterNode” :default-checked-keys=“defaultChecked” :default-expanded-keys=“defaultExpanded” @node-click=“handleClickLeft” @node-contextmenu=“handleClickRight” @check=“handleCheck” @check-change=“handleCheck” @current-change=“handleCheck” @node-expand=“handleCheck” highlight-current :render-content=“renderContent” :props=“treeProps”> </el-tree> <!– 右键菜单 –> <ul class=‘contextmenu’ v-show=“rightMenu.show” :style="{left: rightMenu.left +‘px’,top: rightMenu.top +‘px’}"> <li v-for="(item, index) in rightMenu.list” :key=“index” @click=“handleRightEvent(item.type, item.data, item.node, item.vm)">{{item.name}}</li> </ul> </div>实现效果:总结本文以后台管理页面为例,一般后台管理页面常用到的tree, table, form, dialog, 搜索栏已经全部做成了可复用的组件,只需要配置好相关数据,引入组件即可使用。关于组件的相关逻辑,可能要在文章里面一次性说清楚,还是需要费很大的精力,不过希望数据驱动的思想能够让之前没有体会到这种开发乐趣的小伙伴们有到新的想法。 ...

March 30, 2019 · 4 min · jiezi

彻底搞懂elementUI指令与服务模式原理

不甘做轮子的搬运工!!!记录一个实习菜鸟写图片预览组件的艰辛道路elementUI很多组件中使用了指令模式和服务模式,比如:loding、message…以下以loading组件为例:指令模式:<template> <div :v-loading.fullscreen=“true”>全屏覆盖</div></template>服务模式:const loading = this.$loading({ lock: true, text: ‘Loading’, spinner: ’el-icon-loading’, background: ‘rgba(0, 0, 0, 0.7)’});跟大多数萌新一样,啥是服务?!先看看elmentUI的目录结构:打开node_modules目录,找到其下elementUI目录:element-ui\src\index.js文件中有一大坨组件注册信息,重点找到我们要找的loading…// …// directive 指令装载Vue.use(Loading.directive)// prototype 服务装载Vue.prototype.$loading = Loading.service// …Vue.use() 这个指令是 Vue 用来安装插件的,如果传入的参数是一个对象,则该对象要提供一个 install 方法,如果是一个函数,则该函数被视为 install 方法,在 install 方法调用时,会将 Vue 作为参数传入。开始叭!先看看loading/index.js文件中是什么鬼?//引入指令文件和服务文件,directive为指令模式文件,index.js为服务模式文件import directive from ‘./src/directive’;import service from ‘./src/index’;export default { //install方法注册组件,不在赘述install的用法,star-pic-list图片预览组件文章中已经介绍过 install(Vue) { Vue.use(directive); //在vue的原型对象上注册一个$loading的对象,这个$loading非常眼熟,看上面服务模式的使用,用到了this.$loading,源头找到了 Vue.prototype.$loading = service; }, //引入的directive文件 directive, //引入的index.js文件 service};v-loading 指令解析篇幅太长,其中我们只取 fullscreen 修饰词。// 引入 .vue 文件import Vue from ‘vue’// 引入loading.vue基础文件,里面包含的是组件的基础结构,如html结构,loading显示的页面结构都在这里面import Loading from ‘./loading.vue’// 后面重点讲解extend()构造器// Vue.extend() 是vue构造器,它返回的是一个扩展实例构造器,也就是预设了部分选项的Vue实例构造器,// mask字面意思是面具,掩饰,可以猜出来,这个通过Vue.extend(Loading)返回构造器应该是用于我们loading加载时的遮罩层用的// loading就是预设选项,就像vue示例中,有components,name,data,methods…好像有点明白了const Mask = Vue.extend(Loading)const loadingDirective = {}// 还记得 Vue.use() 的使用方法么?若传入的是对象,该对象需要一个 install 属性loadingDirective.install = Vue => { // toggleLoading 方法看名字就是切换loading显示和隐藏的嘛 const toggleLoading = (el, binding) => { // 若绑定值为 truthy 则插入 loading 元素 // binding 值是一个对象,有指令名、指令的绑定值、modifiers修饰符对象等等等等,具体的可以去了解自定义指令相关内容 if (binding.value) { //binding.value是绑定的指令值 if (binding.modifiers.fullscreen) { 还记得我们插入的指令吗?:v-loading.fullscreen=“true” , .fullscreen就是修饰符 insertDom(document.body, el, binding) //insertDom看名字就知道是插入新的元素 } // visible 是loading.vue data里面定义的值 } else { el.instance.visible = false } } const insertDom = (parent, el, binding) => { // loading 设为可见 el.instance.visible = true // appendChild 添加的元素若为同一个,则不会重复添加 parent.appendChild(el.mask) } // 在此注册 directive 指令 Vue.directive(’loading’, { bind: function(el, binding, vnode) { // 创建一个子组件,这里和 new Vue(options) 类似 // 返回一个组件实例 const mask = new Mask({ el: document.createElement(‘div’), // 有些人看到这里会迷惑,为什么这个 data 不按照 Vue 官方建议传函数进去呢? // 其实这里两者皆可 // 稍微做一点延展好了,在 Vue 源码里面,data 是延迟求值的 // 贴一点 Vue 源码上来 // return function mergedInstanceDataFn() { // let instanceData = typeof childVal === ‘function’ // ? childVal.call(vm, vm) // : childVal; // let defaultData = typeof parentVal === ‘function’ // ? parentVal.call(vm, vm) // : parentVal; // if (instanceData) { // return mergeData(instanceData, defaultData) // } else { // return defaultData // } // } // instanceData 就是我们现在传入的 data: {} // defaultData 就是我们 loading.vue 里面的 data() {} // 看了这段代码应该就不难理解为什么可以传对象进去了 data: { fullscreen: !!binding.modifiers.fullscreen } }) // 将创建的子类挂载到 el 上 // 在 directive 的文档中建议 // 应该保证除了 el 之外其他参数(binding、vnode)都是只读的 el.instance = mask // 挂载 dom // bind 只会调用一次,在bind 的时候给 el.mask 赋值,因此el.mask 所指的为同一个 dom 元素 el.mask = mask.$el // 若 binding 的值为 truthy 运行 toogleLoading binding.value && toggleLoading(el, binding) }, update: function(el, binding) { // 若旧不等于新值得时候(一般都是由 true 切换为 false 的时候) if (binding.oldValue !== binding.value) { // 切换显示或消失 toggleLoading(el, binding) } }, unbind: function(el, binding) { // 当组件 unbind 的时候,执行组件销毁 el.instance && el.instance.$destroy() } })}export default loadingDirective关于extend()更多内容请参考这里,非常通熟易懂!loading服务方式调用原理直接看源码:import Vue from ‘vue’import loadingVue from ‘./loading.vue’// 和指令模式一样,创建实例构造器const LoadingConstructor = Vue.extend(loadingVue)// 定义变量,若使用的是全屏 loading 那就要保证全局的 loading 只有一个let fullscreenLoading// 这里可以看到和指令模式不同的地方// 在调用了 close 之后就会移除该元素并销毁组件LoadingConstructor.prototype.close = function() { setTimeout(() => { if (this.$el && this.$el.parentNode) { this.$el.parentNode.removeChild(this.$el) } this.$destroy() }, 3000)}const Loading = (options = {}) => { // 若调用 loading 的时候传入了 fullscreen 并且 fullscreenLoading 不为 falsy // fullscreenLoading 只会在下面赋值,并且指向了 loading 实例 if (options.fullscreen && fullscreenLoading) { return fullscreenLoading } // 这里就不用说了吧,和指令中是一样的 let instance = new LoadingConstructor({ el: document.createElement(‘div’), data: options }) let parent = document.body // 直接添加元素 parent.appendChild(instance.$el) // 将其设置为可见 // 另外,写到这里的时候我查阅了相关的资料 // 自己以前一直理解 nextTick 是在 dom 元素更新完毕之后再执行回调 // 但是发现可能并不是这么回事,后续我会继续研究 // 如果干货足够的话我会写一篇关于 nextTick ui-render microtask macrotask 的文章 Vue.nextTick(() => { instance.visible = true }) // 若传入了 fullscreen 参数,则将实例存储 if (options.fullscreen) { fullscreenLoading = instance } // 返回实例,方便之后能够调用原型上的 close() 方法 return instance}export default Loading现学现用-写一个点击图片预览的组件试试看目录结构:directive.js是指令模式文件,index.js是服务模式文件,star-pic-preview.vue是基础单文件,包含了基础的html结构先看指令模式:使用:我直接使用了指令,并没有传参,因为功能简单,默认参数就是false <img src=“http://img5.imgtn.bdimg.com/it/u=3300305952,1328708913&fm=26&gp=0.jpg" v-pic-preview >效果:点击出现遮罩层,图片居中显示预览服务模式:使用:<img src=“http://img4q.duitang.com/uploads/item/201502/22/20150222191447_jdBYa.thumb.700_0.jpeg" @click=“openImagePreview2”> methods: { // 服务方式 openImagePreview2(e) { // 如果只传图片 this.$picPreview(e.target.src); //如果传复杂对象,可以配置遮罩层的背景颜色等… // this.$picPreview({ // background: ‘rgba(0, 0, 0, 0.7)’, // imageUrl: e.target.src, // }); },}效果如上源码请参考github地址: 源码地址 ...

March 30, 2019 · 3 min · jiezi

基于elementUI实现图片预览组件

这是一个简单的点击图片预览的组件顺便记录一下写组件期间踩的vue中scope的坑~从注册全局组件开始叭!项目目录:模仿elementUI目录结构,目录名是组件名,src中是组件源文件(或者js服务文件),文件目录下还有一个index.js用于同一管理src中的所有文件,导出并注册,这个组件我们只有一个vue文件件先看index.js文件里有什么://引入了src下的vue组件文件import starPicList from ‘./src/star-pic-list’;/* istanbul ignore next */starPicList.install = function(Vue) { //starPicList.name这就是后面可以使用的组件的名字(star-pic-list.vue文件里面定义的name),install是默认的一个方法 Vue.component(starPicList.name, starPicList);};export default starPicList;接下来介绍一下install方法:Vue.use( plugin ):安装 Vue.js 插件。如果插件是一个对象,必须提供 install 方法。如果插件是一个函数,它会被作为 install 方法。install 方法将被作为 Vue 的参数调用。当 install 方法被同一个插件多次调用,插件将只会被安装一次。Vue.js 的插件应当有一个公开方法 install 。这个方法的第一个参数是 Vue 构造器,第二个参数是一个可选的选项对象:导出starPicList组件后在管理组件js文件中引用,然后由这个统一管理js文件导出注册到全局即可:好了,这些是废话!组件的使用:<!–图片列表形式,点击查看图片列表,点击显示上(下)一张–><template v-slot=“scope”> <star-pic-list :data=“scope.row.pic” :max-show=“2”/></template>参数data: 传入图片数组;max-show: 一次最多显示几张图片效果如下:补充:vue组件开发中 style 添加scoped后,修改第三方组件样式没有效果问题:在vue的开发中,我们通常和element-UI配合开发,就会遇到,在组件style中添加scoped后,element-ui中使用的子组件样式无法改变。不用scoped,去掉这个属性,但是会污染全局样式,(可配合less 或者 scss(推荐scss),所有样式写在当前组件id或class下面)组件源码:<template> <div id=“star-pic-vue”> <template v-if=“data”> <img v-for=“item in images” :src=“item” id=“contract_url” @click=“enlargePic”/> <template v-if=“isDialogShow”> </template> <el-dialog :visible.sync=“centerDialogVisible” modal close-on-click-modal custom-class=“dialog” > <el-carousel :autoplay=“false” arrow=“always”> <el-carousel-item v-for=“item in data” :key=“item”> <img :src=“item”> </el-carousel-item> </el-carousel> </el-dialog> </template> </div></template><script> export default { name: “star-pic-list”, props: [“data”,“maxShow”], data(){ return{ centerDialogVisible: false, showPic: ‘’, isDialogShow: false, index: 0, } }, computed: { images() { if (this.data instanceof Array && this.data.length > 2) { return this.data.splice(0,this.maxShow) } else { return this.data } } }, methods: { // 放大图片 enlargePic(e){ this.isDialogShow = true; this.centerDialogVisible = true; this.showPic = this.data[0]; console.log(this.images) }, } }</script><style lang=“less”>#star-pic-vue{ width: 200px; height: auto; display: flex; flex-wrap: wrap; img{ width: 80px; height: 80px; margin: 4px; } .dialog { img{ width: 100%; height: 100%; margin: 0; } } .el-carousel__item h3 { color: #475669; font-size: 18px; opacity: 0.75; line-height: 300px; margin: 0; height: 100%; width: 100%; } .el-dialog__header{ display: none; } .el-dialog__body { padding: 0 !important; margin: 0 !important; height: 600px; } .el-carousel{ height: 100%; } .el-carousel__container { height: 100%; }}</style>更多组件点击这儿 –> link : github>components>star-pic-list ...

March 30, 2019 · 1 min · jiezi

跟着element学习写组件

如何使用vue写一个组件库组件以插件的形式引入使用,当然,也可以直接在页面引入组件文件,两者按需使用。安装插件:import Button from ‘./oyButton’;Button.install = function (Vue) { Vue.component(Button.name, Button);}export default Button;vue.install源码:export function initUse (Vue: GlobalAPI) { Vue.use = function (plugin: Function | Object) { # /检测该插件是否已经被安装/ if (plugin.installed) { return } const args = toArray(arguments, 1) args.unshift(this) if (typeof plugin.install === ‘function’) { # /install执行插件安装/ plugin.install.apply(plugin, args) } else if (typeof plugin === ‘function’) { plugin.apply(null, args) } plugin.installed = true return this }}通过源码可知,vue不会重复安装同一个插件。以第一次安装为准现在,可以在代码中使用组件啦~<oy-button>我是按钮按钮</oy-button>以上,是一个非常简单的组件库实现。现在来看看element组件库是如何实现的。element组件项目结构这里重点说下packages目录和src目录|– packages # 组件源码目录 |– button # button组件目录,一个组件一个文件,方便管理 |– src # 组件实现代码 |– button-group.vue |– button.vue |– index.js # 组件入口文件|– src |–directives # 实现滚轮优化,鼠标点击优化 |–locale # 国际化 |–mixins # 公用逻辑代码 |–transitions # 样式过度效果 |–utils # 工具类包 |–index.js # 源码入口文件整个目录结构非常清晰。button模块解析button模块目录,有一个index.js作为模块入口import ElButton from ‘./src/button’;ElButton.install = function(Vue) { Vue.component(ElButton.name, ElButton);};export default ElButton;在index.js文件中,对组件进行拓展,添加Install方法。element组件入口文件解析import Button from ‘../packages/button/index.js’;const components = [Button]# 定义一个install方法const install = function(Vue, opts = {}) { locale.use(opts.locale); locale.i18n(opts.i18n);# 将所有的功能模块进行注册。 components.map(component => { Vue.component(component.name, component); });# 注册插件 Vue.use(Loading.directive); const ELEMENT = {}; ELEMENT.size = opts.size || ‘’; # 绑定Vue实例方法 Vue.prototype.$message = Message;};if (typeof window !== ‘undefined’ && window.Vue) { install(window.Vue);}# 最后,将所有功能模块和install方法一起导出。# 这样当引入element-ui时,便可以使用vue.use(element-ui)进行注册,即将所有的功能组件进行全局注册。module.exports = { version: ‘2.3.8’, locale: locale.use, i18n: locale.i18n, install, Button,}module.exports.default = module.exports;我写的组件与elemnet组件有什么不同代码实现1.html语义化element组件实现时,html基本实现了语义化标签。这样在无CSS样子时也容易阅读,便于阅读维护和理解。便于浏览器、搜索引擎解析。 利于爬虫标记、利于SEO标记组件。Badge 标记组件部分源码:<!– sup标签语义:上标文本 –><transition name=“el-zoom-in-center”> <sup v-show="!hidden && (content || content === 0 || isDot)" v-text=“content” class=“el-badge__content” :class="{ ‘is-fixed’: $slots.default, ‘is-dot’: isDot }"> </sup></transition>ps: 自己写代码都是div span2.兼容 v-modelelement组件基本都兼容了v-model绑定值,组件使用起来更加舒适~兼容v-model需要做一下几点:props中要定义value属性。数据变化后,通过事件触发父组件更新数据,同时传递变更后的值。(如text元素使用input事件来改变value属性 和 checkbox使用的change事件来改变check属性)input组件源码: export default { props: { # 定义value value: [String, Number], }, methods: { handleInput(event) { if (this.isOnComposition) return; const value = event.target.value; # 变更数据以后通过input去更新父组件数据 this.$emit(‘input’, value); this.setCurrentValue(value); }, } }3.组件之间传递数据vue中,存在几种组件之间数据传递的方案:propsattrsprovide / injectthis.$parent/$this.$children在日常开发中,父子组件之间数据传递用到比较多的方案是props。当组件层次比较深,就使用attrs来透传数据:<el-select v-model=“selectValue” v-bind="$attrs" v-on="$listeners"> <template v-if=“label && keyValue”> <el-option v-for="(item, index) in selectList" :key=“index” :label=“item[label]” :value=“item[keyValue]"></el-option> </template></el-select>element组件,在父子组件传递数据也是使用props,但是当组件层次比较深,或者不清楚组件层次时,使用的是:provide / injectinject: { elForm: { default: ’’ }, elFormItem: { default: ’’ }},关于provide / inject:“这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效” –vue文档简单来说,就是父组件通过provide来提供变量,子组件通过inject来引用变量。vue的inject源码:# src/core/instance/inject.jsexport function initProvide (vm: Component) { const provide = vm.$options.provide if (provide) { vm._provided = typeof provide === ‘function’ ? provide.call(vm) : provide }}provide是向下传递数据,先获取provide内容,然后传递给vm._provided设置成全局数据。inject会根据选项的 key 数组一层层向上遍历,拿到结果。provide 相对于props,实现了跨层级提供数据。需要注意的是provide不是响应式的。方法解释适用场景props用于接收来自父组件的数据父子组件之间传递数据provide以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效替代嵌套过深的props,可以理解为一个bus,但只做父组件通知子组件的单向传递的一个属性attrs包含了父作用域中不作为 prop 被识别 (且获取) 的特性绑定 (class 和 style 除外)父组件传向子组件传的,子组件没有通过prop接受的数据都会放在$attrs中parent/child获取父/子组件实例 4.组件通信emit/props传递函数两者都是通知父组件执行事件的方法,但是有一定的区别:emit执行的是异步方法,props传递的函数在子组件中执行作为同步函数的形式执行的。emit无法返回函数结果,props传递的函数可以返回函数结果。发布订阅对于组件嵌套过深,element自己实现了一个简易版的发布订阅方式:function broadcast(componentName, eventName, params) { # 组件名称,事件名称,参数 # 当前组件下的子组件循环 this.$children.forEach(child => { # 获取组件名称 var name = child.$options.componentName; # 如果组件名称和要触发的事件组件名称相同 if (name === componentName) { # 当前子组件,调用$emit方法 child.$emit.apply(child, [eventName].concat(params)); } else { # 如果没有相等,那就继续查找当前子组件的子组件 broadcast.apply(child, [componentName, eventName].concat([params])); } });}组件设计1.扁平化参数传入的参数尽量设计简单点,避免复杂的对象。过于复杂的数据,在watch或者update的情况下,影响性能扁平化的props也可以更好的更新数据,重置数据。其次,复杂的数据变更,外部可能会监听不到数据变化。如果定义传入的传入数据是一个对象,那组件内部就要做大量的工作,来判断外部擦混入的对象的属性值是否正确,并找出需要的数据内容,增加了组件工作量,也不便组件的后续维护。2.良好的api接口设计保持组件外部提供接口的精简,不要过于泛滥的提供接口。组件可定制,如果常量变为 props 能应对更多的情况,那么就可以作为 props从父组件引入。原有的常量可作为默认值。按钮组件的样式存在默认样式,但是可以通过type传入类型,定制button组件样式,使组件可以适用更多场景。export default { name: ‘ElButton’, props: { type: { type: String, default: ‘default’ }, }, };3.可扩展性组件在使用过程中,会不断的优化添加功能,但是组件的内部变更不能影响组件的使用,这就需要组件有很好的扩展性,在一开始,能够提供足够比较友好的接口。如何实现?预留“锚点”在组件中预留一些“插槽”,使用组件的时候,可以再“插槽”中注入自定义的内容,从而改变组件渲染结果。element组件库在这方面做得很好。input组件部分源码:<div> <template v-if=“type !== ’textarea’"> <!– 前置元素 –> <div class=“el-input-group__prepend” v-if="$slots.prepend”> <slot name=“prepend”></slot> </div> <input> <!– 前置内容 –> <span class=“el-input__prefix” v-if="$slots.prefix || prefixIcon” :style=“prefixOffset”> <slot name=“prefix”></slot> </span> <!– 后置内容 –> <span> <span class=“el-input__suffix-inner”> <template v-if="!showClear"> <slot name=“suffix”></slot> </template> </span> </span> <!– 后置元素 –> <div class=“el-input-group__append” v-if="$slots.append"> <slot name=“append”></slot> </div> </template> </div>Input组件预留了四个“插槽”,允许使用者在前后位置都可以插入内容。提供丰富的钩子函数,使用者在数据变化时,能对数据进行相应处理element组件提供了丰富的钩子函数:focus() { (this.$refs.input || this.$refs.textarea).focus();},blur() { (this.$refs.input || this.$refs.textarea).blur();},4.错误处理组件要能接受一定的错误使用,能针对可预知的错误使用进行处理。给props属性设置多个数据类型,同时保证传入和传出的数据类型相同。如果组件中,某个字段是父组件一定要传入的,需要把props属性的require设置为true。给重要的prop属性设置默认数据。兜底:数据展示或者使用父组件传入内容之前,要先判断数据是否存在。focus() { # 先判断this.$refs.input是否存在,才进行接下来操作,避免数据为空报错情况。 (this.$refs.input || this.$refs.textarea).focus();} ...

March 29, 2019 · 2 min · jiezi

vue + iview/elementUi --城市多选

城市多选组件最近收到了一个需求,管理系统需要上线一个活动,但是活动是根据地区上线的,最小范围到市,于是有了下面这个组件页面展示如图:上代码~~~<template> <div class=“tm-mil-city”> <p class=“tm-mil-city-title tm-mil-mb20”>{{name}}</p> <div class=“tm-mil-district-box tm-mil-mb20”> <Select class=“tm-mil-selsect” style=‘width:200px’ v-model=‘province’ placeholder=‘全部’ @on-change=‘changeProvince’> <Option v-for=‘item in provinceList’ :value=‘item.id’ :key=‘item.id’>{{ item.regionName }}</Option> </Select> <span class=“tm-mil-selsect-all-btn tm-mil-ml20 tm-mil-colB” @click=“chooseAllRegion”>全选</span> <div class=“tm-mil-line-loading” v-if=“province && !cityList.length”><img src="../assets/loading.gif" alt=""></div> <div class=“tm-mil-mb20” v-if=“cityList.length”> <CheckboxGroup style=“marginTop:10px;width:900px” v-model=“checkCity”> <Checkbox v-for=‘item in cityList’ :key=‘item.id’ :label=“item.regionCode”>{{item.regionName}}</Checkbox> </CheckboxGroup> <Button v-show=“cityList.length” size=“small” style=“marginTop:10px” @click=“saveCheckCity”>确定</Button> </div> </div> <p class=“tm-mil-city-title tm-mil-mb20”>已选择的地区</p> <div class=“tm-mil-line-loading” v-if=“waiting”><img src="../assets/loading.gif" alt=""></div> <div class=“tm-mil-choose-district” v-else> <div v-for="(item, idx) in allCheckCityShowList" :key=“idx”> <span class=“tm-mil-colB”>{{provinceFilter(item.province)}}</span> <span class=“tm-mil-ml10” v-for="(obj, indx) in item.cityList" :key=“indx” >{{obj}}</span> </div> <span v-if="!allCheckCityShowList.length">全部地区</span> </div> </div></template>注: <Select></Select>/<CheckboxGroup></CheckboxGroup>都是iview的组件,详情请看iview官网,同理elementUi也有相同的组件iview官网 elementUi官网 data() { return { waiting: false, // loading province: ‘’, // 当前的省 provinceList: [], // 省列表 Municipality: [{id: 2, name: ‘北京’}, {id: 3, name: ‘天津’}, {id: 10, name: ‘上海’}, {id: 23, name: ‘重庆’}, {id: 2, name: ‘北京’}], // 直辖市 cityList: [], // 城市列表 activityTime: [], // 活动时间 checkCity: [], // 当前省所选的市列表 – 展示 allCheckCityApi: [], // 所有的市列表 – 接口 allCheckCitySave: { // 存储所有选择各省对应的市列表 – 存储 // ‘10001’: [{code:‘10111’, name:‘北京’}] }, allCheckCityShowList: [ // 存储所有选择的市列表 – 展示 // { province: ‘10001’, cityList: [‘上海’, 2, 3]} ] } },函数:请输入代码 // 获取省级数据 getOrigin() { this.axios.get(/users/open/region/topRegions).then(res => { if (res.status === 200) { this.provinceList = res.data } }) } // 返回省名称 provinceFilter(id) { return this.provinceList.filter(item => item.id === id)[0].regionName } // 选择全部地区 chooseAllRegion() { this.province = 0 this.cityList = [] this.checkCity = [] this.allCheckCityApi = [] this.allCheckCitySave = [] this.allCheckCityShowList = [] } // 保存城市后存储数据 – 接口 handleSaveCityList() { let _arr = [] for (var key in this.allCheckCitySave) { _arr = _arr.concat(this.allCheckCitySave[key]) } this.allCheckCityApi = _arr } // 更改省 changeProvince(parentId) { if (!parentId) { return } this.cityList = [] //获取该省下的市列表 this.axios.get(/users/open/region/${parentId}/subRegions).then(res => { if (res.status === 200) { this.cityList = res.data } }) // 处理已经选择的市 let _checkCity = this.allCheckCitySave[parentId] || [] let _checkCityList = [] _checkCity.forEach(el => { _checkCityList.push(el.regionCode) }) this.checkCity = JSON.parse(JSON.stringify(_checkCityList)) } // 保存所选的当前市 saveCheckCity() { // 处理选择不同省,保存当前省已选择的投放城市 if (!this.checkCity.length) { return } this.waiting = true // 当前城市的省code let _province = JSON.parse(JSON.stringify(this.province)) // 当前城市的省名称 let _provinceName = this.Municipality.filter(el => el.id === _province)[0] && this.Municipality.filter(el => el.id === _province)[0].name || ’’ // 已选择城市(code name level)列表 let _arrCheckMsgList = [] // 当前选择的城市code let _arrCheck = JSON.parse(JSON.stringify(this.checkCity)) _arrCheck.forEach(el => { let _obj = this.cityList.filter(_el => _el.regionCode === el)[0] // 区别市辖区 let _regionName = _provinceName + _obj.regionName let _regionLevel = _obj.regionLevel let obj = {regionCode: el, regionName: _regionName, regionLevel: _regionLevel, parentId: _province} // let obj = {regionCode: el, regionName: _regionName, regionLevel: _regionLevel} _arrCheckMsgList.push(obj) }) // 存储到当前省对应的已选择的市列表 – 存储 this.$set(this.allCheckCitySave, _province, _arrCheckMsgList) // 保存城市后存储数据 – 接口 this.handleSaveCityList() // 处理已选择的投放地区数据展示 let _arrCheckMsg = [] // 处理展示列表-城市名称 -- 直辖市(北京,上海等)选地区时要加上直辖市前缀,如 北京市辖区/北京县 this.cityList.map(obj => { if (_arrCheck.indexOf(obj.regionCode) > -1) { _arrCheckMsg.push(_provinceName + obj.regionName) } }) let _msgObj = { province: _province, cityList: _arrCheckMsg } let _len = this.allCheckCityShowList.filter(item => item.province === _province).length || 0 // 新增 / 替换 if (!_len) { this.allCheckCityShowList.push(_msgObj) this.waiting = false } else { this.allCheckCityShowList.forEach((item, idx) => { if (item.province === _province) { this.$set(this.allCheckCityShowList, idx, _msgObj) this.waiting = false return } }) } }已上,具体的解释都在注释里面,有疑问的地方欢迎留言~ ...

March 27, 2019 · 3 min · jiezi

【mone-query】基于 element-ui 的通用查询组件

mone-queryGithub: https://github.com/jczzq/mone...Demo: https://blog.jczzq.com/mone-q...mone-query是基于element-ui封装的通用查询组件,它通过丰富的配置让你尽可能少的前端编码就可以完成大部分报表需求。必要依赖vue >= 2.5.2 element-ui >= 2.4.0 axios >= 0.16.0安装CommonJS方式npm install mone-query -Dumd方式<link href=“mone-query/lib/style.css” rel=“stylesheet”><script src="../mone-query/lib/index.js"></script>快速上手import Vue from “vue”;import ‘mone-query/style.css’;import MoneQuery from “mone-query”;Vue.use(MoneQuery, { // options …})<mone-query border :config=“config” :data=“data” />…import Config from “@/Config.json”;import Data from “@/Data.json”;data() { return { // 传入请求路径 config: “/api/config”, data: “/api/data” // or // 传入对象 config: Config.resultData, data: Data.resultData };}运行机制校验入参mone-query有两个必要参数config和data, config控制表格结构和自定义功能,data渲染数据行,这两个参数可以传入ajax路径字符串,也可以传入json对象,其他类型的参数传入时会抛出异常。注意组件还有其它一些参数可以传入,组件传入的参数与config有一些参数是重复的,甚至你还可以在Vue.use时给所有的组件实例传入默认参数,这是为了增加组件配置的灵活性,不过通过组件直接传入的参数优先级最高,config属性其次,最后是默认配置根据config参数获取配置如果config传入字符串,那么组件首先会发起Get请求查询配置json,这段期间表格会一直loading到请求结束,如果请求失败,表格将会触发on-error事件,使用者可以监听事件做后续处理,或者让页面保持错误面板的状态;请求成功的结果将直接作为配置对象使用。如果config传入json,将直接作为配置对象使用(具体结构示例参照Config.json)。根据配置对象渲染表结构受配置影响的部分包括:colbox: 工具栏的字段面板cols: 表头……根据data参数获取结果集如果data传入字符串,那么组件会将之作为ajax路径查询结果集。如果data传入json,将直接作为结果集使用(具体结构示例参照Data.json)。有时候在结果集渲染之前需要对某些字段进行处理,比如映射新值、时间转换等等,这时提供formatters对象,这是一个属性名为col.prop、属性值为该列过滤函数的对象,过滤函数有四个形参:row, column, cellValue, indexMoneQuery Attributes属性名类型含义可选值默认值*configObject , String表头列ajax路径配置对象{}*dataArray , String数据行ajax路径数据集合[]heightString/NumberTable高度auto8080pxautomax-heightString/NumberTable的最大高度–borderBoolean是否带有纵向边框-falseprimary-keyString主键(数据行多选时必要)-idpage-nameString分页参数中的pageSize键名-pageIndexsize-nameString分页参数中的pageSize键名-pageSizerows-nameStringajax请求结果集键名-resultData.rowstotal-nameStringajax请求结果总数键名-resultData.totalcolboxObject字段显示框方位-{ placement: “top”, width: “540px”, trigger: “click” }visible-fieldsBoolean,Array默认显示字段true: 全部显示; false: 全部隐藏; [“propA”, “propB”, …]truevisible-fields-configArray<FieldGroup>字段在工具栏的显示配置–formattersObject包含各个列过滤函数的对象-{}show-actionBoolean是否显示右侧操作栏-falseshow-deleteBoolean是否显示删除按钮-falseshow-headerBoolean是否显示表头(包含筛选条件)-trueshow-selectionBoolean是否显示多选框-trueshow-indexBoolean是否自定义序列-falseMoneQuery Events事件名说明参数delete点击删除按钮触发selectionsearch点击查询按钮触发parametersconfig-success查询配置成功resconfig-error查询配置失败errorconfig-complete查询配置完成(成功或失败都会触发)-data-success查询结果集成功resdata-error查询结果集失败errordata-complete查询结果集完成(成功或失败都会触发)-config Attributes属性名类型含义可选值默认值baseUrlStringajax根路径–primary-keyString主键(数据行多选时必要)-idcolsArray<Col>列-idpage-nameString分页参数中的pageSize键名-pageIndexsize-nameString分页参数中的pageSize键名-pageSizerows-nameStringajax请求结果集键名-resultData.rowstotal-nameStringajax请求结果总数键名-resultData.totalcolboxObject字段显示框方位-{ placement: “top”, width: “540px”, trigger: “click” }visible-fieldsBoolean,Array默认显示字段true: 全部显示; false: 全部隐藏; [“propA”, “propB”, …]truevisible-fields-configArray<FieldGroup>字段在工具栏的显示配置–show-actionBoolean是否显示右侧操作栏-falseshow-deleteBoolean是否显示删除按钮-falseshow-headerBoolean是否显示表头(包含筛选条件)-trueshow-selectionBoolean是否显示多选框-trueshow-indexBoolean是否自定义序列-falsecolbox Attributes属性名类型含义可选值默认值placementString展示方位-topwidthString面板宽度-540pxtriggerString触发事件类型-clickCol class属性名类型含义可选值默认值labelString列标题–propString列字段名–typeString列字段类型varchar、option、date、datetime-widthString列宽度widthwidthorderNumber排列顺序–actionString查询类型-varchar:lk,option:in; data:lt& gt; datetime:le& geplaceholderString输入提示–fixedBoolean是否固定列–optionsArray多选组件的选项列表,eg: [{label, value}]–Col.type enum枚举值含义varchar文本option多选date日期datetime日期时间FieldGroup class属性名类型含义可选值默认值titleString组标题–orderString排列顺序–selectionArray选中的–checkAllBoolean是否全选-trueisIndeterminateBoolean是否半选-falsecolPropsArray组成员–查询配置标准结构(application/json)resultCode 响应码resultData 响应结果(注意resultData必须是JSON对象)primaryKey String 主键prop(默认:id)rowsName String 结果集属性名,将根据rowsName指定的属性名获取目标结果集(默认: resultData.rows)totalName String 结果总数属性名,将根据totalName指定的属性名获取目标结果总数(默认: resultData.total)pageName String 当前页属性名,将根据pageName指定的属性名设置分页参数(默认: pageIndex)sizeName String 每页条数属性名,将根据sizeName指定的属性名设置分页参数(默认: pageSize)pageSize Number 默认每页查询的数据数量(默认: 20)cols Array 列头(字段)对象集合label String 标题prop String 属性名type String 属性类型varchar 文本option 多选date 日期datetime 日期时间bit 单选(暂不支持)int 数字(暂不支持)time 时间(暂不支持)width String 列宽order Number 排列顺序colbox Object 可选字段配置placement String 展示方位(默认:top)width String 面板宽度(默认:540px)trigger String 触发事件类型(默认:click)value Array (默认:所有字段)查询参数标准结构(application/json)colProps Array 要查询的字段列params Array 查询参数字段field Stringaction Stringeq 等于ne 不等于in 包含在ni 不包含在sw start withew end withlk likegt 大于lt 小于ge 大于等于le 小于等于filedType Stringvarchar 字符串option 多选date 日期范围datetime 日期时间范围bit 单选(暂不支持)int 数字(暂不支持)time 时间(暂不支持)value anysort Array 排序字段prop String 字段名type String 排序方式asc 正序desc 降序page ObjectpageIndex Number 当前页(默认:1)pageSize Number 每页多少条(默认:20)# 查询参数示例cols: [“name”, “age”, “grade”, “isMarried”, “birthday”, “schoolTime”],params: [ { field: “name”, action: “lk”, filedType: “varchar”, value: “李”, }, { field: “birthday”, action: “lt”, filedType: “datetime”, value: “2019-04-13”, }, { field: “birthday”, action: “ge”, filedType: “datetime”, value: “2019-03-13”, }, { field: “grade”, action: “in”, filedType: “option”, value: [1,2,5], }]sort: [ { prop: “age’, type: “asc” }],page: { pageIndex: 1, pageSize: 20}查询结果集标准结构(application/json)[resultCode] 响应码[resultData] 响应结果[rows] 结果集[total] 结果总数轻量版mone-query默认依赖element-ui部分组件和axios请求库,这里有两个构建版供你的应用选择:全部引入(js≈360kb, style≈103kb):包括element相关组件(可以在应用中使用这些组件,因为它们已经全局注册过了);另外是axios。只引入核心代码(≈24kb, style≈1kb),如果你的应用已经引入了element和axios,那建议只引入核心代码即可。 import “mone-query/lib/lite/style.css”; import moneQuery from “mone-query/lib/lite”; …PS: 这里说来说去最终不影响物质守恒,如果你不是太关心你应用的构建体积可以忽略 ...

March 27, 2019 · 2 min · jiezi

Element中的Cascader(级联列表)动态加载省/市/区数据

element中的cascader其实是有动态加载次级选项的方法。方法的原理是利用址(引用)传递,动态修改:options。var c={name: ‘bob’}var d=cd.name = ’tom’console.log(c)// {name: “tom”}http://element-cn.eleme.io/#/…其中找到究竟需要在那层添加数据就变成一个很麻烦的问题。怎么找了?当然只能递归了。简化一下大致思路:var a = [ { value: ‘2’, children: [ { value: ‘2-1’, children: [ { value: ‘2-1-1’, children: [], }, ], }, { value: ‘2-2’, children: [ { value: ‘2-2-1’, children: [], }, { value: ‘2-2-2’, children: [ { value: ‘2-2-2-1’, children: [], }, ], }, ], }, ], },]var b = [‘2’,‘2-2’,‘2-2-1’]那么我们就需要通过b找到a所在的位置。a[0].children[1].children[0]{ value: ‘2-2-1’, children: [], },然后再赋值:a[0].children[1].children[0].children = [{value: ‘2-2-1-1’,children: []}]console.log(a)编写function:findRegionOption(regionOptions, regionArr) { if (_.isEmpty(regionArr) || _.isEmpty(regionOptions)) { return null } let regionId = _.first(regionArr) let regionOption = _.find(regionOptions, regionOption => { return regionOption.value === regionId }) if (!regionOption) { return null } let tailRegionArr = .tail(regionArr) // lodash的tail方法,获取除了array数组第一个元素以外的全部元素。 if (.isEmpty(tailRegionArr)) { return regionOption } return this.findRegionOption(regionOption.children, tailRegionArr)}动态加载数据:loadRegionChild(regionIdArr) { let regionOptions = this.regionHiera let regionOptionInUI = this.findRegionOption(regionOptions, regionIdArr) if ( !regionOptionInUI || !regionOptionInUI.children || regionOptionInUI.children.length > 0 ) { return null } let regionKey = .last(regionIdArr) if (!regionKey) { return null } api .getRegionHiera(regionKey) .then(res => { let regionHiera = res.data regionOptionInUI.children = regionChildrenTransfomed // 动态加载赋值 })}整个页面代码大致就是:<template> <div> <el-cascader :options=“regionHiera” v-model=“selectedRegion” change-on-select/> </div></template><script>export default { name: ‘Test’, data() { return { selectedRegion: [], regionHiera: [ { label: ‘Malaysia’, value: ‘136’, children: [] }, { label: ‘Indonesia’, value: ‘106’, children: [] }, { label: ‘中华人民共和国’, value: ‘100000’, children: [] }, { label: ‘United States’, value: ‘244’, children: [] }, ], } }, watch: { selectedRegion(nv) { this.loadRegionChild(nv) }, }, methods: { findRegionOption(regionOptions, regionArr) { if (.isEmpty(regionArr) || _.isEmpty(regionOptions)) { return null } let regionId = _.first(regionArr) let regionOption = _.find(regionOptions, regionOption => { return regionOption.value === regionId }) if (!regionOption) { return null } let tailRegionArr = .tail(regionArr) if (.isEmpty(tailRegionArr)) { return regionOption } return this.findRegionOption(regionOption.children, tailRegionArr) }, loadRegionChild(regionIdArr) { let regionOptions = this.regionHiera let regionOptionInUI = this.findRegionOption(regionOptions, regionIdArr) if ( !regionOptionInUI || !regionOptionInUI.children || regionOptionInUI.children.length > 0 ) { return null } let regionKey = _.last(regionIdArr) if (!regionKey) { return null } api .getRegionHiera(regionKey) .then(res => { let regionHiera = res.data //后台返回数据 regionOptionInUI.children = regionChildrenTransfomed }) }, }}</script>整体思路还是找到点击后的region,然后动态赋值给children。写的有点乱,希望有帮助吧。 ...

March 26, 2019 · 2 min · jiezi

lb-element-table基于 Element UI Tabel 封装的表格组件

基于 Element UI Tabel 封装的表格组件,使用更简单,无需大量重复的代码量,只需简单配置column和data即可,简单明了。完美支持Element UI Tabel的属性及事件,熟悉的参数、熟悉的味道,熟悉的写法。Github点击前往更多示例及参考点击前往简单示例参考<template> <lb-table :column=“tableData.column” :data=“tableData.data”> </lb-table></template><script>export default { data () { return { tableData: { column: [ { prop: ‘date’, label: ‘日期’ }, { prop: ’name’, label: ‘姓名’ }, { prop: ‘address’, label: ‘地址’ } ], data: [ { date: ‘2016-05-02’, name: ‘王小虎1’, address: ‘上海市普陀区金沙江路 1518 弄’ }, { date: ‘2016-05-02’, name: ‘王小虎2’, address: ‘上海市普陀区金沙江路 1518 弄’ }, { date: ‘2016-05-02’, name: ‘王小虎3’, address: ‘上海市普陀区金沙江路 1518 弄’ } ] } } }}</script> ...

March 25, 2019 · 1 min · jiezi

element-ui 源码解析,你知道 v-loading 是如何实现的吗?

前言相信大家肯定都用过 element-ui 里面的 v-loading 来写加载,但是如果让你来写一个的话你会怎么写呢?众所周知,element-ui 框架的 v-loading 有两种使用方式,一种是在需要 loading 的标签上直接使用 :v-loading=‘true’,这种方式官方称为指令,还有一种就是使用 this.$loading(options) 来调用,这种方式官方称之为服务。人类对于对于美好的事物总会有趋向性,我也不外乎如此,话不多说(个屁,就你话最多),直接扒源码。有些人天生就适合你,有的代码天生就适合阅读,优秀的开源项目都是如此,希望接下来我的解析也可以让你恍然大悟,随后窃笑一声,原来就这样。正文使用方式指令来回顾一下 v-loading 的指令使用方式<template> <div :v-loading.fullscreen=“true”>全屏覆盖</div></template>服务再来看看服务的使用方式mounted() { let loading = this.$loading({ fullscreen: true }) setTimeout(() => { loading.close() }, 1000)}稍微留点印象,我们简单粗暴一些,直接扒源码吧。起点打开你的 node_modules,目标 element-ui,在 src 目录下的 index.js 便是引入所有组件的地方,让我看看今天是哪两个小可爱要被我们扒光光。// element-ui\src\index.js// …// directive 指令装载Vue.use(Loading.directive)// prototype 服务装载Vue.prototype.$loading = Loading.service// …本着循序渐进的原则,我们先从使用较多的指令方式看起。Vue.use() 这个指令是 Vue 用来安装插件的,如果传入的参数是一个对象,则该对象要提供一个 install 方法,如果是一个函数,则该函数被视为 install 方法,在 install 方法调用时,会将 Vue 作为参数传入。如果要了解更多有关 plugin,点击这里了解更多有关 plugin 的内容,尤大大的官方文档简直百读不厌。但是老板!这 Loading 下的两个是啥玩意儿呢!来,我们看这里。import directive from ‘./src/directive’import service from ‘./src/index’export default { // 这里为什么有个 install 呢 // 当你使用单组件单注册的时候就会调用这里了 // 效果和下面一样,挂载指令,挂载服务 install(Vue) { Vue.use(directive) Vue.prototype.$loading = service }, // 就是上面的 Loading.directive directive, // 就是上面的 Loading.service service}接下来我们终于要深入源码了!dokidoki…v-loading 指令解析喝杯水,压抑住激动的心情,我们打开 packages 下的 loading\index.js,可以看到其对外曝露了 directive 指令,来,上路,我们来看他的源码。看似有百来行的代码,但是客官不要着急,我给你精简一下,贴上主要代码,为了方便解说,在其中我们只取 fullscreen 修饰词。import Vue from ‘vue’// 这里就是我们写的比较多的单 .vue 文件了,不拓展开了// 值得注意的是在这个单文件里面的 data() {} 声明的值// 我们下面会碰到import Loading from ‘./loading.vue’// 老板!Vue.extend() 是什么!// 代码片段之后我会简单介绍const Mask = Vue.extend(Loading)const loadingDirective = {}// 还记得 Vue.use() 的使用方法么?若传入的是对象,该对象需要一个 install 属性loadingDirective.install = Vue => { // 这里处理显示、消失 loading const toggleLoading = (el, binding) => { // 若绑定值为 truthy 则插入 loading 元素 // binding 值为 directive 的几个钩子中会接受到的参数 if (binding.value) { if (binding.modifiers.fullscreen) { insertDom(document.body, el, binding) } // 不然则将其设为不可见 // 从上往下读我们是第一次看到 visible 属性 // 别急,往下看,这个属性可以其实就是单文件 loading.vue 里面的 // data() { return { visible: false } } } else { el.instance.visible = false } } const insertDom = (parent, el, binding) => { // 将 loading 设为可见 el.instance.visible = true // appendChild 添加的元素若为同一个,则不会重复添加 // 我们 el.mask 所指的为同一个 dom 元素 // 因为我们只在 bind 的时候给 el.mask 赋值 // 并且在组件存在期间,bind 只会调用一次 parent.appendChild(el.mask) } // 在此注册 directive 指令 Vue.directive(’loading’, { bind: function(el, binding, vnode) { // 创建一个子组件,这里和 new Vue(options) 类似 // 返回一个组件实例 const mask = new Mask({ el: document.createElement(‘div’), // 有些人看到这里会迷惑,为什么这个 data 不按照 Vue 官方建议传函数进去呢? // 其实这里两者皆可 // 稍微做一点延展好了,在 Vue 源码里面,data 是延迟求值的 // 贴一点 Vue 源码上来 // return function mergedInstanceDataFn() { // let instanceData = typeof childVal === ‘function’ // ? childVal.call(vm, vm) // : childVal; // let defaultData = typeof parentVal === ‘function’ // ? parentVal.call(vm, vm) // : parentVal; // if (instanceData) { // return mergeData(instanceData, defaultData) // } else { // return defaultData // } // } // instanceData 就是我们现在传入的 data: {} // defaultData 就是我们 loading.vue 里面的 data() {} // 看了这段代码应该就不难理解为什么可以传对象进去了 data: { fullscreen: !!binding.modifiers.fullscreen } }) // 将创建的子类挂载到 el 上 // 在 directive 的文档中建议 // 应该保证除了 el 之外其他参数(binding、vnode)都是只读的 el.instance = mask // 挂载 dom el.mask = mask.$el // 若 binding 的值为 truthy 运行 toogleLoading binding.value && toggleLoading(el, binding) }, update: function(el, binding) { // 若旧不等于新值得时候(一般都是由 true 切换为 false 的时候) if (binding.oldValue !== binding.value) { // 切换显示或消失 toggleLoading(el, binding) } }, unbind: function(el, binding) { // 当组件 unbind 的时候,执行组件销毁 el.instance && el.instance.$destroy() } })}export default loadingDirectiveVue.extend 是什么在平时的代码中该方法我们主动调用的不多,但是在我们注册组件的时候,比如,Vue.component(‘my-component’, options),这个时候会自动调用 Vue.extend,直接上源码吧,该代码是当调用 Vue.component() 的时候将会执行的。ps:稍微做一下延展,对于 .vue 单文件,想必大家都能猜到是如何运作的了,首先会把 <template> 标签的内容转为 render() 函数,说到这个 render() 函数,我又想安利一波 JSX 了(打住!),然后就接着走 Vue.component() 这条线路注册组件了。…if (type === ‘component’ && isPlainObject(definition)) { definition.name = definition.name || id definition = this.options._base.extend(definition)}…而在 extend 里面又做了什么事情呢?我很想直接贴上源码再来分析,但是这样就超出我们今天要说的范围了,一言以蔽之, Vue.extend 接受参数并返回一个构造器,new 该构造器可以返回一个组件实例。相信到了这里大家已经对如何实现 v-loading 有了一定的了解了,勤快的小伙伴已经卷了袖子开干了,但是文章还没完,分析完指令模式我们还要看看服务模式,稍作休息,上路。点击这里了解更多有关 Vue.extend 的内容,依旧是官方文档,Vue 的文档我简直吹爆(破音)。服务如果打开开发者模式看过两种 loading 的方式,应该会注意到指令模式和服务模式的区别,最直观的就是若有 fullscreen 参数,指令模式下不会移除生成的 dom 元素,而在服务模式下会移除生成的 dom 元素。我的废话太多了,直接上源码,同样的,我会提取我们需要关注的代码片段方便分析,有了指令模式的基础,看服务模式的就没什么难度了。import Vue from ‘vue’import loadingVue from ‘./loading.vue’// 和指令模式一样,创建实例构造器const LoadingConstructor = Vue.extend(loadingVue)// 定义变量,若使用的是全屏 loading 那就要保证全局的 loading 只有一个let fullscreenLoading// 这里可以看到和指令模式不同的地方// 在调用了 close 之后就会移除该元素并销毁组件LoadingConstructor.prototype.close = function() { setTimeout(() => { if (this.$el && this.$el.parentNode) { this.$el.parentNode.removeChild(this.$el) } this.$destroy() }, 3000)}const Loading = (options = {}) => { // 若调用 loading 的时候传入了 fullscreen 并且 fullscreenLoading 不为 falsy // fullscreenLoading 只会在下面赋值,并且指向了 loading 实例 if (options.fullscreen && fullscreenLoading) { return fullscreenLoading } // 这里就不用说了吧,和指令中是一样的 let instance = new LoadingConstructor({ el: document.createElement(‘div’), data: options }) let parent = document.body // 直接添加元素 parent.appendChild(instance.$el) // 将其设置为可见 // 另外,写到这里的时候我查阅了相关的资料 // 自己以前一直理解 nextTick 是在 dom 元素更新完毕之后再执行回调 // 但是发现可能并不是这么回事,后续我会继续研究 // 如果干货足够的话我会写一篇关于 nextTick ui-render microtask macrotask 的文章 Vue.nextTick(() => { instance.visible = true }) // 若传入了 fullscreen 参数,则将实例存储 if (options.fullscreen) { fullscreenLoading = instance } // 返回实例,方便之后能够调用原型上的 close() 方法 return instance}export default Loading关于代码里的 fullscreenLoading 变量,根由 element-ui 官方的说明我们应该能了解个大概,这是为了保证覆盖整个页面的 loading 实例只有一个才存在的,官方文档说明如下。需要注意的是,以服务的方式调用的全屏 Loading 是单例的:若在前一个全屏 Loading 关闭前再次调用全屏 Loading,并不会创建一个新的 Loading 实例,而是返回现有全屏 Loading 的实例后语至此文章主体内容已经结束了,看起来只是一个 v-loading 的功能但却延伸出去很多的内容,我还精简了很多代码,所以我这只是管中窥豹很小的一部分内容,更多的内容推荐大家也去读读源码,你会发现不一样的世界。有些人和我说,看源码的时候一看开头,十来个模块引入,一看组件,百来行,瞬间就软掉了,没有看下去的欲望了,但怎么说呢,这个时候我推荐可以先从你有些了解的功能开始看起,比如 v-loading,看到这个就知道是 directive,再源码里看的时候关注关键点,比如 binding、el、vnode,很快就能理清楚代码的含义了。如果身边有更优秀的人那还好,可以以他为目标,但是当你一枝独秀,身边的人都不如自己,你不知自己该如何成长的时候,这个时候就应该去看看源码,从源码中学习,从源码中提升自己。该如何看待阅读?一天的饭钱就能买到别人可能一辈子的心血,多么值钱的买卖。— 无名虽然源码可能不能称作是一辈子的心血,但是想象一下,看源码的过程中就好像和作者面对面在交谈,这个模块怎么安排,那个算法怎么优化,看 Vue 的源码更是了,好像面对面和尤大大在聊天,这这这,想想就湿了啊!(眼角,为什么?因为太感动了)这篇文章只是我在和大家分享一些优秀的人的心血,结合了一些自己的理解,希望大家看完整篇文章之后能够有一些收获有一些沉淀,当然卷起袖子直接写一个自己的 v-custom-loading 就更好了。我在平时看源码的过程中会发现不少有意思的代码,如果有兴趣的话可以来我的项目里看看,里面就有我自己写的 v-custom-loading,还结合了一些自己的想法,比如将组件实例挂载的时候,我推荐如下写法。我的 vue-tiny-code 欢迎 starconst context = ‘@@loadingContext’…el[context] = { instance: mask }…这么写的原因就是不要污染 el 元素本身有的属性,毕竟有可能自己定义的属性会和 dom 原有的属性冲突。另外我在 vue-element-admin 项目中提了 pr 用的就是这种写法。具体可以看 vue-element-admin issue 1704 和 vue-element-admin issue 1705。ps:最近面试题泛滥,我想如果把标题改成《面试题解析,你知道 v-loading 该如何实现么?》会不会更有人关注一些?(狗头保平安)页脚代码即人生,我甘之如饴。我在这里 gayhub@jsjzh 欢迎大家来找我玩儿。小伙伴们可以直接加我或者加群,我们一起学前端、看源码、学算法,前端进阶,加油。 ...

March 17, 2019 · 4 min · jiezi

element-ui组件table实现自定义筛选功能

element-ui默认的table组件支持的表头筛选(过滤)是比较简单的,只支持数组的方式,单选或多选的形式,但有时候我们喜欢支持输入框形式(其实感觉有点扯淡,一般列表页上面都有搜索条件)。注意:里面用到的jsx语法,可能需要安装一些插件。准备工作:1.安装babel-plugin-jsx-v-model插件npm i babel-plugin-jsx-v-model -D或者yarn add babel-plugin-jsx-v-model -D2..babelrc:{ “presets”: [“es2015”], “plugins”: [“jsx-v-model”, “transform-vue-jsx”]}3.重启本地环境实现效果如下:代码如下:<template> <div> <el-table :data=“tableData”> <el-table-column label=“这是文字” :render-header=“renderHeader” prop=“name”></el-table-column> <el-table-column label=“地址” prop=“address”></el-table-column> </el-table> </div></template><script>export default { data() { return { search: ‘’, visible: false, tableData: [{ date: ‘2016-05-02’, name: ‘王小虎’, address: ‘上海市普陀区金沙江路 1518 弄’ }, { date: ‘2016-05-04’, name: ‘王小虎’, address: ‘上海市普陀区金沙江路 1517 弄’ }, { date: ‘2016-05-01’, name: ‘王小虎’, address: ‘上海市普陀区金沙江路 1519 弄’ }, { date: ‘2016-05-03’, name: ‘王小虎’, address: ‘上海市普陀区金沙江路 1516 弄’ }] } }, methods: { renderHeader(h, {column, $index}, index) { return ( <span> 问题分类 <el-popover placement=‘bottom’ width=‘200’ height=‘200’ trigger=“click” v-model={this.visible}> <span slot=“reference”> <i class=“el-icon-search” style={this.search ? {‘color’ : ‘red’} : {‘color’: ‘blue’}}></i> </span> <el-input size=‘small’ v-model={this.search} placeholder=‘请输入内容’></el-input> <div class=‘el-table-filter__bottom’> <button class={this.search ? ’’ : ‘is-disabled’}>筛选</button> <button on-click={this.clearSearch}>重置</button> </div> </el-popover> </span> ); }, clearSearch() { this.search = ‘’; } }}</script>参考: https://www.jianshu.com/p/f55… ...

March 15, 2019 · 1 min · jiezi

vue+elementui 实现table的row 在hover/click实现展开行效果

前言:产品的需求是,在table表格 click 一行时,展开一行,显示对于此行的增删改查等操作按钮,click当前行会收起操作按钮消息,click 别的行,会收起之前的展开且展开当前行.hover的实现一样,我就以click来说明了.吐槽下elementUI的api~~~ 确实不那么友好(不然也没必要写这个文章了是不~~~)哈哈需求明确了,现在来实现此图片是api的例子,type=“expand” 这样会出现一列’>’,然而并不能实现,继续看,有个这个方法,那我们来实现一些,代码如下:<el-table :data=“tableData” ref=“refTable” row-key=“id” :expand-row-keys=“expands” @expand-change=“expandChange” @row-click=“rowClick”>rowClick(row,column,event){ this.$refs.refTable.toggleRowExpansion(row); },expandChange(row,expandedRows){ if(expandedRows.length>1){ expandedRows.shift() } }, 可见, table标签里,有row-key 需要是你tableData的唯一标识,<el-table-column type=“expand” width=“0” fixed=“right” label=“more”> <template slot-scope=“scope”> 这里写你需要展开的内容 </template></el-table-column可以看到,我的width值设置为了0,是为了不让那个箭头列显示,也没有找到好的方法,项目也比较急,所以就先这样啦~有好方法欢迎留言给我哦如果对你有帮助,请点个赞,可以更加勤快的分享文章 哈哈

March 8, 2019 · 1 min · jiezi

在Element-UI中引入Iconfont图标

最近在使用在Element-UI的时候发现其图标太少了,连常用的reset,people之类的图标都没有,于是想引入第三方的图标库进行使用。在网上查找了资料后成功引入,下面就是我的引入流程:这里我选择的是阿里巴巴的Iconfont,打开https://www.iconfont.cn/,创建账号,然后选择你要引入的图标(加入购物车)点击添加至项目,然后新建一个项目点击跟多操作修改项目前缀,不能为 el-icon- ,因为这个可能会和Element-UI自带的图标重合点击生成在线CSS代码,复制代码链接,在项目的APP.vue中引入,这时我们就可以通过 class=“iconfont el-icon-mo-xxx” 的方式在项目中使用引入的第三方图标了,如果想要去掉 iconfont 这个类直接用 el-icon-mo-xxx 来使用图标的话我们还需要多一步操作:<template> <div id=“app”> <router-view/> </div></template><script>export default { name: ‘App’}</script><style> @import “//at.alicdn.com/t/font_1075796_5kpx2vwkmq3.css”; [class^=“el-icon-mo”], [class^=" el-icon-mo"] { font-family: “iconfont”, serif !important; font-size: 16px; font-style: normal; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }</style>PS每次新增或删除,特别是新增如果要引入新增的图标,要重新生成CSS文件,这时候需要把新生成的CSS文件链接重新在 App.vue 中引入,替换掉原来的链接。

March 7, 2019 · 1 min · jiezi

VUE全家桶+ElementUi 项目踩坑总结

项目简介vue + axios + vue-router + vuex + ElementUI架构vuevue数据更新,视图不更新只有当实例被创建时 data 中存在的属性才是响应式的,Vue 不能检测对象属性的添加或删除; 可以使用 Vue.set(object, key, value) 方法向嵌套对象添加响应式属性Vue.set(vm.userProfile, ‘age’, 27)this.$set(this.transportAddData.serviceOrderList[a].serviceOrderItems[i], ‘deletePoundIds’, [])vue 数据与方法 vue 对象更改检测注意事项Vue 不能检测以下变动的数组:当你利用索引直接设置一个项时,例如:vm.items[indexOfItem] = newValue当你修改数组的长度时,例如:vm.items.length = newLengthvue 数组更新检测持续触发事件的优化持续触发某个事件可以用函数的节流和防抖来做性能优化//防抖function(){ … clearTimeout(timeoutId); timeoutId = setTimeout(function () { console.log(‘content’, content); player(content.msgTypeId, comId) }, 500); … }// 节流var canRun = true;document.getElementById(“throttle”).onscroll = function(){ if(!canRun){ // 判断是否已空闲,如果在执行中,则直接return return; } canRun = false; setTimeout(function(){ console.log(“函数节流”); canRun = true; }, 300);};javaScript的Throttling(节流)和Debouncing(防抖)nextTick在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。get() { this.$http.get(’/api/article’).then(function (res) { this.list = res.data.data.list // ref list 引用了ul元素,我想把第一个li颜色变为红色 document.querySelectorAll(’li’)[0].style.color = ‘red’ //这里会报错-,因为还没有 li this.$nextTick( ()=> { document.querySelectorAll(’li’)[0].style.color = ‘red’ }) })},Vue.nextTick 的原理和用途音频文件自动播放报错谷歌浏览器(Chrome 66)音频自动播放报错DOMException: play() failed because the user didn’t interact with the document first.解决方案:AudioContext// Chromerequest.get(’/baseConfig/messageAudio/play’, { params: { “comId”: Cookies.get(‘comId’), “msgTypeId”: id }, responseType: ‘arraybuffer’ // 这里需要设置xhr response的格式为arraybuffer }) .then(req => { … let context = new (window.AudioContext || window.webkitAudioContext)(); context.decodeAudioData(req.data, decode => play(context, decode)); function play (context, decodeBuffer) { sourceadio = context.createBufferSource(); sourceadio.buffer = decodeBuffer; sourceadio.connect(context.destination); // 从0s开始播放 sourceadio.start(0); } … })Chrome 66禁止声音自动播放之后 [AudioContext](https://developer.mozilla.org… [AudioContext.decodeAudioData()](https://developer.mozilla.org...vuex使用vuex修改state的方法和区别可以直接使用 this.$store.state.变量 = xxx;this.$store.dispatch(actionType, payload) 或者 this.$store.commit(commitType, payload)相同点:能够修改state里的变量,并且是响应式的(能触发视图更新) 不同点: 若将vue创建 store 的时候传入 strict: true, 开启严格模式,那么任何修改state的操作,只要不经过 mutation的函数,vue就会报如下错throw error : [vuex] Do not mutate vuex store state outside mutation handlers。使用commit提交到mutation修改state的优点:vuex能够记录每一次state的变化记录,保存状态快照,实现时间漫游/回滚之类的操作。dispatch 和 commit的区别在于: 使用dispatch是异步操作, commit是同步操作, 所以 一般情况下,推荐直接使用commit,即 this.$store.commit(commitType, payload),以防异步操作会带来的延迟问题。vuex strict vuex Mutation vuex actionsvuex到底是什么const store = new Vuex.Store({ state: { count: 0 }, mutations: { increment (state) { state.count++ } }, actions: { increment (context) { context.commit(‘increment’) } }})==vuex中的state本质上是没有template的隐藏的vue组件。==vuex工作原理详解axios兼容问题Edge 41.16299.15.0 post请求会自动转为getmicrosoft-edge/platform/issues vue 使用axios 在EDGE浏览器上面post请求变成了get请求取消请求场景:每次请求在拦截器中添加token,后台判断token是否过期;当进入一个页面时触发多次请求,且正好token已经过期。这个时候需要第一次请求完成之后知道token已经过期则弹出提示、页面跳转到登录、停止之后的请求;否则会因为多次请求和axios响应拦截而多次弹框提示。解决方案 axios自带cancelToken 取消方法,然后在路由跳转后停止之前的请求// 请求拦截中 添加 cancelTokenaxios.interceptors.request.use(config => { config.cancelToken = store.source.token return config}, err => { return Promise.reject(err)}) // 路由跳转中进行判断router.beforeEach((to, from, next) => { const CancelToken = axios.CancelToken store.source.cancel && store.source.cancel() store.source = CancelToken.source() next()})//sort文件/state: { source: { token: null, cancel: null } }axios api 路由变化时使用axios取消所有请求 vue项目中 axios请求拦截器与取消pending请求功能存在问题: 如果 token过期提示弹框为二次确认弹框,再次确认之后才会进行页面跳转,那么在点击确认之前,页面中之前的请求还是会继续进行; 解决方案:给弹窗添加全局状态,根据状态判断是否需要弹出弹框预检请求CORS跨域 CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,==IE浏览器不能低于IE10。== 浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。简单请求请求方法是以下三种方法之一: HEAD GET POSTHTTP的头信息不超出以下几种字段:Accept Accept-Language Content-Language Last-Event-ID Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain简单请求不会触发 CORS 预检请求。非简单请求当请求满足下述任一条件时,即为非简单请求:使用了下面任一 HTTP 方法: PUT DELETE CONNECT OPTIONS TRACE PATCH人为设置了对 CORS 安全的首部字段集合之外的其他首部字段。该集合为:AcceptAccept-LanguageContent-LanguageContent-Type (but note the additional requirements below)DPRDownlinkSave-DataViewport-WidthWidthContent-Type 的值不属于下列之一: application/x-www-form-urlencoded multipart/form-data text/plain请求中的XMLHttpRequestUpload 对象注册了任意多个事件监听器。请求中使用了ReadableStream对象。HTTP访问控制(CORS)预检请求非简单请求,会在正式通信之前,增加一次HTTP OPTIONS方法发起的查询请求,称为"预检"请求(preflight)。以获知服务器是否允许该实际请求。“预检请求“的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。 ==该方法不会对服务器资源产生影响==优化方案Access-Control-Max-Age: <delta-seconds> //单位是秒。表示 preflight request (预检请求)的返回结果(即 Access-Control-Allow-Methods 和Access-Control-Allow-Headers 提供的信息) 可以被缓存多久Vue Routerpush、replace的区别push会向history添加新的记录,replace只是替换掉原来的记录,不会添加新的记录;这就导致了用replace方法跳转的页面是无法回到之前的页面的;(类似window.history)vue Router 编程式的导航路由懒加载为了提升页面加载速度,实现按需加载,也就是当路由访问时才加载对应组件,我们可以结合vue的异步组件和webpack的代码分割功能来实现路由的懒加载。{ path: ‘/iov/login’, name: ‘登录’, component: resolve => require([’@/views/login/login’], resolve),},{ path:’/iov/organization’, name:‘组织管理’, component:() => import(’@/views/enterprise/organization’)}vue Router 路由懒加载 vue 异步组件 vue + vue-router 懒加载 import / resolve+requireelementUI表单弹窗中 elementform 清除验证残留提示给表单添加不同的 ref (REFNAME),如果有相同的ref 会导致残留验证提示清除失败 this.dialogTranspor = true //弹窗打开后 dom 没有生成,所有要用 this.$nextTick this.$nextTick(function () { this.$refs.REFNAME.resetFields(); })页码数无法正常显示场景:列表页在跳到详情或其他页面后再返回列表页,无法正常显示对应页数(页码数放在state中),但请求的数据时正常的; 解决方案:页码数、总页数必须要在同一个对象中,否则当前页码数不能正常显示data() { return { //完成查询条件 searchComplate: { “comId”: Cookies.get(‘comId’), “transportOrderCode”: null, “orderCode”: null, “customerId”: null, “abnormal”: 2, “startTime”: null, “endTime”: null, “pageNum”: 1, “pageSize”: 20, total: 0, currentRow: ‘’, dataArea: [’’, ‘’], activeName: ‘’, expands: [] }, }}动态多级表单验证<li v-for="(item,index) in transportAddData.serviceOrderList”> <template v-for="(subItem,subIndex) in item.serviceOrderItems"> <tr > <td> <el-form-item :prop="‘serviceOrderList.’+index+’.serviceOrderItems.’ + subIndex + ‘.addressName’" :rules="{required: true, message: ‘卸货地不能为空’, trigger: ‘blur’}"> <el-input v-model=“subItem.addressName” placeholder=“地址”></el-input> </el-form-item> </td> <td> <el-form-item :prop="‘serviceOrderList.’+index+’.serviceOrderItems.’ + subIndex + ‘.planTonnage’" :rules="[{required: true, message: ‘必填项’, trigger: ‘blur’},{pattern: /^((([1-9]+(\d+)?)(.\d+)?)|(0.\d+))$/, message: ‘必须为正数且不为0’}]"> <el-input v-model=“subItem.planTonnage” placeholder=“预卸吨数”></el-input> </el-form-item> </td> … </tr> </template></li> ...

March 7, 2019 · 3 min · jiezi

vue+elementUi 实现密码显示/隐藏+小图标变化(js一共三行代码,其中一行为了美观)

【效果图】【html】// 前后代码【略】<el-form-item label=“密码” prop=“pwd”> <el-input v-model=“ruleForm.pwd” :type=“pwdType” placeholder=“请输入密码” @keyup.enter.native=“login”> <i slot=“suffix” class=“el-icon-view” @click=“showPwd”></i> </el-input></el-form-item>【js】showPwd () { this.pwdType === ‘password’ ? this.pwdType = ’’ : this.pwdType = ‘password’; let e = document.getElementsByClassName(’el-icon-view’)[0]; this.pwdType == ’’ ? e.setAttribute(‘style’, ‘color: #409EFF’) : e.setAttribute(‘style’, ‘color: #c0c4cc’);},

March 1, 2019 · 1 min · jiezi

vue工程全局设置ajax的等待动效

最近在做vue的项目,使用了element-ui作为ui组件库,采用vuex进行状态管理,与后台的请求交互采用axios库实现,原本做的页面,ajax请求个数也只有三个,将等待动画的显示和隐藏通过mutation去控制,但是项目越来越大,请求也越来越多,能否将这个等待动画与ajax的请求相关联呢?实现等待动效在element-ui中,提供了两个方法进行调用loading组件,一个是在需要遮罩的div容器中添加v-loading指令,另一种就是以服务形式调用:Loading.service(options),在本项目中,在加载数据时,进行全局遮罩,所以设置let loadingInstance = Loading.service({fullscreen:true})。关闭服务形式的调用:loadingInstance.close();axios的拦截器个人对拦截器的作用的理解是,在请求发送前和响应处理前,对该ajax请求进行一定的设置或者处理,方便对工程内的ajax请求进行统一处理,减少重复代码。 //请求拦截器 axios.interceptors.request.use((config) => { // 在发送请求之前做些什么 return config; }, (error) => { // 对请求错误做些什么 return Promise.reject(error); }); // 添加响应拦截器 axios.interceptors.response.use((response) => { // 对响应数据做点什么 return response; }, (error) => { // 对响应错误做点什么 return Promise.reject(error); });有了拦截器,我们可以想到,在ajax请求发送前开启loading动画,在接收响应后关闭loading动画。但是要对每个ajax都开关一下loading动画吗?其实不必。只需要实现一个ajax的计数器,在个数大于0时,开启动画,在个数为0的时候,关闭动画。计数器实现let loadingInstance;let loadCounter = { count:0, show:() => { if(count < 0){ this.count ++; loadingInstance = Loading.service({fullscreen:true}); } }, hide:()=>{ if(count > 0){ this.count –; this.$nextTick(()=>{//以服务的方式调用的 Loading 需要异步关闭 loadingInstance.close(); }); } }}使用方法 //请求拦截器 axios.interceptors.request.use((config) => { loadCounter.show(); return config; }, (error) => { return Promise.reject(error); }); // 添加响应拦截器 axios.interceptors.response.use((response) => { loadCounter.hide(); return response; }, (error) => { return Promise.reject(error); }); ...

February 22, 2019 · 1 min · jiezi

element-ui上传下载excel(超详细der)

上传 EXCEL Upload组件 点击跳转到该组件官方文档用到的upload组件参数参数说明类型可选默认值action必选参数,上传的地址string——file-list上传的文件列表array—[]accept接受上传的文件类型string——http-request覆盖默认的上传行为function——limit最大允许上传个数number——on-exceed文件超出个数限制时的钩子function(files, fileList)——组件代码html<el-upload style=“display: inline; margin-left: 10px;margin-right: 10px;” action="" :http-request=“uploadFile” :limit=1 :on-exceed=“fileExceed” accept=“application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-excel” :file-list=“uploadList” ref=“fileupload”></el-upload>–说明–style: 按项目需要添加,请按需保留action:必需,自定义上传直接给空串;非自定义,将地址赋给action配合属性headers、on-success、on-error等http-request:自定义方法,根据请求的响应手动实现on-success、on-errorfile-list:本项目有清空需要【代码略】ref:该上传组件放置dialog中,本项目有置空需求【代码略】,请按需保留jsimport HTTP_API from ‘@/httpApi’ // 封装好的axios:get post请求(含headers和拦截器等【代码略】)// methodsfileExceed () { this.$message.error(‘别贪心!一次只能上传一个哦~’);}, // 请求成功importSuccess (res) { // 后端的返回码–上传成功 if (res.code == xxxxx) { // 显示√图标 let e = document.getElementsByClassName(’el-upload-list__item-status-label’); e[0].setAttribute(‘style’, ‘display:block !important’) // 成功提示 this.$message.success(‘上传成功’); // 重新调用列表请求(代码略) this.getList(); // 后端的返回码–上传失败 } else { // 隐藏√图标 let e = document.getElementsByClassName(’el-upload-list__item-status-label’); e[0].setAttribute(‘style’, ‘display:none !important’) // 失败提示–及后端返回的失败详情 this.$message.error({ dangerouslyUseHTMLString: true, duration: 4500, message: res.remark+’,<br/>请删除上传失败的文件,修改后重新上传’ }); }}, // 请求失败importError (err) { this.$message.error({ dangerouslyUseHTMLString: true, duration: 4500, message: ‘上传出现异常,请稍后重试’+’,<br/><br/>异常原因:’+err });}, // 自定义上传uploadFile (item) { const form = new FormData() form.append(‘file’, item.file) HTTP_API.xlsx_upload(form).then(res => { this.importSuccess(res) }).catch(err => { this.importError(err) })} 2. 下载 EXCEL button组件组件代码html<el-button type=“primary” @click=“downTemplate” round>下载模板</el-button>jsimport axios from ‘axios’// methods// 导出模板downTemplate () { axios({ method: ‘get’, url:‘xxx相对地址xxx’, responseType: ‘blob’ }).then(res => this.downloads(res.data, res.headers.filename))},// 创建模板下载链接downloads (data, name){ if(!data){ return } let url = window.URL.createObjectURL(new Blob([data])) let link = document.createElement(‘a’) link.style.display =‘none’ link.href = url link.setAttribute(‘download’, 前端拼接后端返回的名字${name}.xlsx) document.body.appendChild(link) link.click()}, 3. 结束语 上传action必须有,空串也好,随便写点也行,反正得有element的提示允许html代码,但是要设置dangerouslyUseHTMLString为true上传的请求成功(状态码200)不等于上传成功,element的√图标,需要用js实现显示隐藏下载时文件名称为动态时,请求后端协助在响应的头部增加filename字段(filename字段中含文字会有问题,后端给我日期数字,相同的文字部分前端拼接)

February 13, 2019 · 1 min · jiezi

Tooltip组件拆解

Element的组件拆解之Tooltipelement ui的中的 toolpic组件 在 packages/tooltip目录下。这个组件核心部分是 toolpic 分别涉略了。《main.js vue-popper.js popup.js vdom.js dom.js》js文件核心用到到js差不多就是main.js,vue-popper.js其他都是element封装好都调用都公共方法vdom.js是找到this.$slots.default中都vode因为this.$slots.default默认返回都是一个数组dom.js是添加样式,检测是否有这个样式,获取样式等一些方法, 对ie上对兼容等 有兴趣可以看看main.js:代码//main.jsimport Popper from ’element-ui/src/utils/vue-popper’;export default {mixins: [Popper],render (h) { // 初始化beforeCreate中vue的html this.newVue.node = ( <div ref=“popper” onMouseenter={() => { this.show() }} onMouseLeave={() => { this.hide() }} v-show={this.showPopper} > {this.$slots.default} </div> ) // 抛出自定义内容做固定展示在html上 return this.$slots.default[0]},beforeCreate () { // 创建一个新的Vue对象 this.newVue = new Vue({ data: {node: ‘’}, render(h){ return this.node }, }).$mount()},mounted(){ this.referenceElm = this.$el; this.referenceElm.addEventListener(‘mouseenter’,()=>{this.show()}) this.referenceElm.addEventListener(‘mouseleave’,()=>{this.hide()})},methods: { show(){ console.log(‘经过啦’) this.showPopper = true }, hide() { console.log(‘离开啦’) this.showPopper = false }}}在生命周期beforeCreate中创建一个新对vue对象,通过render函数初始化HTML 然后 return 一个 对象例如:<el-tooltip class=“item” effect=“dark” content=“Top Left 提示文字” placement=“top-start”> <el-button>上左</el-button> </el-tooltip>render中return的就是<el-button>上左</el-button>这快内容vue-popper.js代码: 在这个文件的代码核心部分,elementUI也是用npm库里面的popper.js去完成 官方api再此 popper.js 我这边已经黏贴了飞机票简化后的vue-popper.js。import popperJs from ‘popper.js’;export default {data() { return { showPopper: false }},watch: { showPopper(val) { val ? this.createpopper() : this.destroyPopper(); }},methods: { createpopper(){ document.body.appendChild(this.$refs.popper); new popperJs(this.referenceElm,this.$refs.popper) }}}这是最简单调用方式实现了一个toolpic鼠标经过和离开会展示这个toolpic总结1.通过main.js中生成HTML并在mounted生命周期中添加各种鼠标事件,改变showPopper的值2.同时把当前对this.$el赋值给this.referenceElm3.在vue-popper.js对showPopper的值进行监听,一旦showPopper的值为true时执行this.createpopper(),反之则摧毁4.在this.createpopper()函数中popper组件需要2个参数然后把 this.$refs.popper和 this.referenceElm传过去就好了PC:原声组件对很多细节做了处理我只是简单对还原对组件功能,若想知细节请去官网下载elementUI ...

January 23, 2019 · 1 min · jiezi

ElementUI 实现表格可编辑 Editable,增删改查编辑表格Grid

查看 Github实现思路:使用 solt 处理编辑和显示切换已经自定义组件渲染,100%兼容 ElTable 所有参数。Vue + ElementUI 扩展的可编辑表格组件,完全支持任意组件渲染。实现功能:支持单列编辑支持整行编辑支持单击、双击编辑模式支持渲染任意组件支持动态列支持显示编辑状态支持增删改查数据获取支持还原更改数据支持 ElTable 所有功能APIEditable Attributes<el-editable ref=“editable” edit-config="{trigger: ‘click’, mode: ‘cell’}"> <el-editable-column prop=“name” label=“名字” edit-render="{name: ‘ElInput’}"></el-editable-column> <el-editable-column prop=“age” label=“年龄” edit-render="{name: ‘ElInput’}"></el-editable-column></el-editable>属性描述类型可选值默认值trigger触发方式Stringmanual(手动方式) / click(点击触发编辑) / dblclick(双击触发编辑)clickmode编辑方式Stringcell(列编辑模式) / row(行编辑模式)cellshowIcon是否显示列头编辑图标Boolean—trueshowStatus是否显示列的编辑状态Boolean—trueEditable Methods方法名描述参数reload(datas)初始化加载数据datasrevert()还原修改之前数据 insert(record)新增一行新数据recordinsertAt(record, rowIndex)指定位置新增一行新数据,如果是-1则从底部新增新数据record, rowIndexremove(record)根据数据删除一行数据recordremoves(records)根据数据删除多行数据recordsremoveRow(rowIndex)根据行号删除一行数据rowIndexremoveRows(rowIndexs)根据行号删除多行数据rowIndexsremoveSelecteds()删除选中行数据 clear()清空所有数据 clearActive()清除所有活动行列为不可编辑状态 setActiveRow(rowIndex)设置活动行为可编辑状态(只对mode=‘row’有效)rowIndexupdateStatus(scope)更新列状态(当使用自定义组件时,值发生改变时需要调用来更新列状态),如果不传参数则更新整个表格scopegetRecords()获取表格数据集 getAllRecords()获取表格所有数据 getInsertRecords()获取新增数据 getRemoveRecords()获取已删除数据 getUpdateRecords()获取已修改数据 Editable-Column Attributes<el-editable-column prop=“name” label=“名字” edit-render="{name: ‘ElInput’, type: ‘default’}"></el-editable-column>属性描述类型可选值默认值name渲染的组件名称StringElInput / ElSelect / ElCascader / ElDatePicker / ElInputNumber / ElSwitchElInputtype渲染类型Stringdefault(组件激活后才可视) / visible(组件一直可视)defaultattrs渲染组件附加属性Object—{}optionAttrs下拉组件选项附加属性(只对name=‘ElSelect’有效)Object—{}options下拉组件选项列表(只对name=‘ElSelect’有效)Array—[]Editable-Column Scoped Slotname说明—自定义渲染显示内容,参数为 { row, column, $index }type自定义渲染组件,参数为 { row, column, $index }head自定义表头的内容,参数为 { column, $index }Editable.vue<template> <el-table ref=“refElTable” :class="[’editable’, {’editable–icon’: showIcon}]" :data=“datas” :height=“height” :maxHeight=“maxHeight” :stripe=“stripe” :border=“border” :size=“size” :fit=“fit” :showHeader=“showHeader” :highlightCurrentRow=“highlightCurrentRow” :currentRowKey=“currentRowKey” :rowClassName=“rowClassName” :rowStyle=“rowStyle” :headerRowClassName=“headerRowClassName” :headerRowStyle=“headerRowStyle” :headerCellClassName=“headerCellClassName” :headerCellStyle=“headerCellStyle” :rowKey=“rowKey” :emptyText=“emptyText” :defaultExpandAll=“defaultExpandAll” :expandRowKeys=“expandRowKeys” :defaultSort=“defaultSort” :tooltipEffect=“tooltipEffect” :showSummary=“showSummary” :sumText=“sumText” :summaryMethod="_summaryMethod" :selectOnIndeterminate=“selectOnIndeterminate” :spanMethod="_spanMethod" @select="_select" @select-all="_selectAll" @selection-change="_selectionChange" @cell-mouse-enter="_cellMouseEnter" @cell-mouse-leave="_cellMouseLeave" @cell-click="_cellClick" @cell-dblclick="_cellDBLclick" @row-click="_rowClick" @row-contextmenu="_rowContextmenu" @row-dblclick="_rowDBLclick" @header-click="_headerClick" @header-contextmenu="_headerContextmenu" @sort-change="_sortChange" @filter-change="_filterChange" @current-change="_currentChange" @header-dragend="_headerDragend" @expand-change="_expandChange"> <slot></slot> </el-table></template><script>import XEUtils from ‘xe-utils’import { mapGetters } from ‘vuex’export default { name: ‘ElEditable’, props: { editConfig: Object, data: Array, height: [String, Number], maxHeight: [String, Number], stripe: Boolean, border: Boolean, size: { type: String, default: ‘small’ }, fit: { type: Boolean, default: true }, showHeader: { type: Boolean, default: true }, highlightCurrentRow: Boolean, currentRowKey: [String, Number], rowClassName: [Function, String], rowStyle: [Function, Object], cellClassName: [Function, String], cellStyle: [Function, Object], headerRowClassName: [Function, String], headerRowStyle: [Function, Object], headerCellClassName: [Function, String], headerCellStyle: [Function, Object], rowKey: [Function, String], emptyText: String, defaultExpandAll: Boolean, expandRowKeys: Array, defaultSort: Object, tooltipEffect: { type: String, default: ‘dark’ }, showSummary: Boolean, sumText: { type: String, default: ‘合计’ }, summaryMethod: Function, selectOnIndeterminate: { type: Boolean, default: true }, spanMethod: Function }, data () { return { editProto: {}, datas: [], initialStore: [], deleteRecords: [], lastActive: null } }, computed: { …mapGetters([ ‘globalClick’ ]), showIcon () { return this.editConfig ? !(this.editConfig.showIcon === false) : true }, showStatus () { return this.editConfig ? !(this.editConfig.showStatus === false) : true }, mode () { return this.editConfig ? (this.editConfig.mode || ‘cell’) : ‘cell’ } }, watch: { globalClick (evnt) { if (this.lastActive) { let target = evnt.target let { cell } = this.lastActive while (target && target.nodeType && target !== document) { if (this.mode === ‘row’ ? target === cell.parentNode : target === cell) { return } target = target.parentNode } this.clearActive() } }, datas () { this.updateStatus() } }, created () { this._initial(this.data, true) }, methods: { _initial (datas, isReload) { if (isReload) { this.initialStore = XEUtils.clone(datas, true) } this.datas = (datas || []).map(item => this._toData(item)) }, _toData (item, status) { return item.editable && item._EDITABLE_PROTO === this.editProto ? item : { _EDITABLE_PROTO: this.editProto, data: item, store: XEUtils.clone(item, true), editStatus: status || ‘initial’, editActive: null, editable: { size: this.size, showIcon: this.showIcon, showStatus: this.showStatus, mode: this.mode } } }, _updateData () { this.$emit(‘update:data’, this.datas.map(item => item.data)) }, _select (selection, row) { this.$emit(‘select’, selection.map(item => item.data), row.data) }, _selectAll (selection) { this.$emit(‘select-all’, selection.map(item => item.data)) }, _selectionChange (selection) { this.$emit(‘selection-change’, selection.map(item => item.data)) }, _cellMouseEnter (row, column, cell, event) { this.$emit(‘cell-mouse-enter’, row.data, column, cell, event) }, _cellMouseLeave (row, column, cell, event) { this.$emit(‘cell-mouse-leave’, row.data, column, cell, event) }, _cellClick (row, column, cell, event) { if (!this.editConfig || this.editConfig.trigger === ‘click’) { this._triggerActive(row, column, cell) } this.$emit(‘cell-click’, row.data, column, cell, event) }, _cellDBLclick (row, column, cell, event) { if (this.editConfig && this.editConfig.trigger === ‘dblclick’) { this._triggerActive(row, column, cell) } this.$emit(‘cell-dblclick’, row.data, column, cell, event) }, _rowClick (row, event, column) { this.$emit(‘row-click’, row.data, event, column) }, _rowContextmenu (row, event) { this.$emit(‘row-contextmenu’, row.data, event) }, _rowDBLclick (row, event) { this.$emit(‘row-dblclick’, row.data, event) }, _headerClick (column, event) { this.$emit(‘header-click’, column, event) }, _headerContextmenu (column, event) { this.$emit(‘header-contextmenu’, column, event) }, _sortChange ({ column, prop, order }) { this.$emit(‘sort-change’, { column, prop, order }) }, _filterChange (filters) { this.$emit(‘filter-change’, filters) }, _currentChange (currentRow, oldCurrentRow) { if (currentRow && oldCurrentRow) { this.$emit(‘current-change’, currentRow.data, oldCurrentRow.data) } else if (currentRow) { this.$emit(‘current-change’, currentRow.data) } else if (oldCurrentRow) { this.$emit(‘current-change’, oldCurrentRow.data) } }, _headerDragend (newWidth, oldWidth, column, event) { this.$emit(‘header-dragend’, newWidth, oldWidth, column, event) }, _expandChange (row, expandedRows) { this.$emit(’expand-change’, row.data, expandedRows) }, _triggerActive (row, column, cell) { this.clearActive() this.lastActive = { row, column, cell } cell.className += editable-col_active row.editActive = column.property this.$nextTick(() => { let inpElem = cell.querySelector(’.el-input>input’) if (!inpElem) { inpElem = cell.querySelector(’.el-textarea>textarea’) if (!inpElem) { inpElem = cell.querySelector(’.editable-custom_input’) } } if (inpElem) { inpElem.focus() } }) }, _updateColumnStatus (trElem, column, tdElem) { if (column.className.split(’ ‘).includes(’editable-col_edit’)) { let classList = tdElem.className.split(’ ‘) if (!classList.includes(’editable-col_dirty’)) { classList.push(’editable-col_dirty’) tdElem.className = classList.join(’ ‘) } } }, _summaryMethod (param) { let { columns } = param let data = param.data.map(item => item.data) let sums = [] if (this.summaryMethod) { sums = this.summaryMethod({ columns, data }) } else { columns.forEach((column, index) => { if (index === 0) { sums[index] = this.sumText return } let values = data.map(item => Number(item[column.property])) let precisions = [] let notNumber = true values.forEach(value => { if (!isNaN(value)) { notNumber = false let decimal = (’’ + value).split(’.’)[1] precisions.push(decimal ? decimal.length : 0) } }) let precision = Math.max.apply(null, precisions) if (!notNumber) { sums[index] = values.reduce((prev, curr) => { let value = Number(curr) if (!isNaN(value)) { return parseFloat((prev + curr).toFixed(Math.min(precision, 20))) } else { return prev } }, 0) } else { sums[index] = ’’ } }) } return sums }, _spanMethod ({ row, column, rowIndex, columnIndex }) { let rowspan = 1 let colspan = 1 let fn = this.spanMethod if (XEUtils.isFunction(fn)) { var result = fn({ row: row.data, column: column, rowIndex: rowIndex, columnIndex: columnIndex }) if (XEUtils.isArray(result)) { rowspan = result[0] colspan = result[1] } else if (XEUtils.isPlainObject(result)) { rowspan = result.rowspan colspan = result.colspan } } return { rowspan: rowspan, colspan: colspan } }, clearActive () { this.lastActive = null this.datas.forEach(item => { item.editActive = null }) Array.from(this.$el.querySelectorAll(’.editable-col_active.editable-column’)).forEach(elem => { elem.className = elem.className.split(’ ‘).filter(name => name !== ’editable-col_active’).join(’ ‘) }) }, setActiveRow (rowIndex) { setTimeout(() => { let row = this.datas[rowIndex] if (row && this.mode === ‘row’) { let column = this.$refs.refElTable.columns.find(column => column.property) let trElemList = this.$el.querySelectorAll(’.el-table__body-wrapper .el-table__row’) let cell = trElemList[rowIndex].children[0] this._triggerActive(row, column, cell) } }, 5) }, reload (datas) { this.deleteRecords = [] this.clearActive() this._initial(datas, true) this._updateData() }, revert () { this.reload(this.initialStore) }, clear () { this.deleteRecords = [] this.clearActive() this._initial([]) this._updateData() }, insert (record) { this.insertAt(record, 0) }, insertAt (record, rowIndex) { let recordItem = {} let len = this.datas.length this.$refs.refElTable.columns.forEach(column => { if (column.property) { recordItem[column.property] = null } }) recordItem = this._toData(Object.assign(recordItem, record), ‘insert’) if (rowIndex) { if (rowIndex === -1 || rowIndex >= len) { rowIndex = len this.datas.push(recordItem) } else { this.datas.splice(rowIndex, 0, recordItem) } } else { rowIndex = 0 this.datas.unshift(recordItem) } this._updateData() }, removeRow (rowIndex) { let items = this.datas.splice(rowIndex, 1) items.forEach(item => { if (item.editStatus === ‘initial’) { this.deleteRecords.push(item) } }) this._updateData() }, removeRows (rowIndexs) { XEUtils.lastEach(this.datas, (item, index) => { if (rowIndexs.includes(index)) { this.removeRow(index) } }) }, remove (record) { this.removeRow(XEUtils.findIndexOf(this.datas, item => item.data === record)) }, removes (records) { XEUtils.lastEach(this.datas, (item, index) => { if (records.includes(item.data)) { this.removeRow(index) } }) }, removeSelecteds () { this.removes(this.$refs.refElTable.selection.map(item => item.data)) }, getRecords (datas) { return (datas || this.datas).map(item => item.data) }, getAllRecords () { return { records: this.getRecords(), insertRecords: this.getInsertRecords(), removeRecords: this.getRemoveRecords(), updateRecords: this.getUpdateRecords() } }, getInsertRecords () { return this.getRecords(this.datas.filter(item => item.editStatus === ‘insert’)) }, getRemoveRecords () { return this.getRecords(this.deleteRecords) }, getUpdateRecords () { return this.getRecords(this.datas.filter(item => item.editStatus === ‘initial’ && !XEUtils.isEqual(item.data, item.store))) }, updateStatus (scope) { if (this.showStatus) { if (arguments.length === 0) { this.$nextTick(() => { let trElems = this.$el.querySelectorAll(’.el-table__row’) if (trElems.length) { let columns = this.$refs.refElTable.columns this.datas.forEach((item, index) => { let trElem = trElems[index] if (trElem.children.length) { if (item.editStatus === ‘insert’) { columns.forEach((column, cIndex) => this._updateColumnStatus(trElem, column, trElem.children[cIndex])) } else { columns.forEach((column, cIndex) => { let tdElem = trElem.children[cIndex] if (tdElem) { if (XEUtils.isEqual(item.data[column.property], item.store[column.property])) { let classList = tdElem.className.split(’ ‘) tdElem.className = classList.filter(name => name !== ’editable-col_dirty’).join(’ ‘) } else { this._updateColumnStatus(trElem, column, trElem.children[cIndex]) } } }) } } }) } }) } else { this.$nextTick(() => { let { $index, _row, column, store } = scope let trElems = store.table.$el.querySelectorAll(’.el-table__row’) if (trElems.length) { let trElem = trElems[$index] let tdElem = trElem.querySelector(.${column.id}) if (tdElem) { let classList = tdElem.className.split(’ ‘) if (XEUtils.isEqual(_row.data[column.property], _row.store[column.property])) { tdElem.className = classList.filter(name => name !== ’editable-col_dirty’).join(’ ‘) } else { this._updateColumnStatus(trElem, column, tdElem) } } } }) } } } }}</script>EditableColumn.vue<template> <el-table-column v-if=“type === ‘selection’ || group” v-bind=“attrs”> <slot></slot> </el-table-column> <el-table-column v-else-if=“type === ‘index’” v-bind=“attrs”> <template slot=“header” slot-scope=“scope”> <slot name=“head” v-bind="{$index: scope.$index, column: scope.column, store: scope.store}">#</slot> </template> <slot></slot> </el-table-column> <el-table-column v-else-if=“type === ’expand’” v-bind=“attrs”> <template slot=“header” slot-scope=“scope”> <slot name=“head” v-bind="{$index: scope.$index, column: scope.column, store: scope.store}"></slot> </template> <template slot-scope=“scope”> <slot v-bind="{$index: scope.$index, row: scope.row.data, column: scope.column, store: scope.store, _row: scope.row}"></slot> </template> </el-table-column> <el-table-column v-else-if=“editRender” v-bind=“attrs”> <template slot=“header” slot-scope=“scope”> <slot name=“head” v-bind="{$index: scope.$index, column: scope.column, store: scope.store}"> <i class=“el-icon-edit-outline editable-header-icon”></i>{{ scope.column.label }} </slot> </template> <template slot-scope=“scope”> <template v-if=“editRender.type === ‘visible’"> <slot name=“edit” v-bind=”{$index: scope.$index, row: scope.row.data, column: scope.column, store: scope.store, _row: scope.row}"> <template v-if=“editRender.name === ‘ElSelect’"> <el-select v-model=“scope.row.data[scope.column.property]” v-bind=“getRendAttrs(scope)” @change=“changeEvent(scope)"> <el-option v-for="(item, index) in editRender.options” :key=“index” :value=“item.value” :label=“item.label” v-bind=“editRender.optionAttrs”></el-option> </el-select> </template> <template v-else-if=“editRender.name === ‘ElCascader’"> <el-cascader v-model=“scope.row.data[scope.column.property]” v-bind=“getRendAttrs(scope)” @change=“changeEvent(scope)"></el-cascader> </template> <template v-else-if=“editRender.name === ‘ElTimePicker’"> <el-time-picker v-model=“scope.row.data[scope.column.property]” v-bind=“getRendAttrs(scope)” @change=“changeEvent(scope)"></el-time-picker> </template> <template v-else-if=“editRender.name === ‘ElDatePicker’"> <el-date-picker v-model=“scope.row.data[scope.column.property]” v-bind=“getRendAttrs(scope)” @change=“changeEvent(scope)"></el-date-picker> </template> <template v-else-if=“editRender.name === ‘ElInputNumber’"> <el-input-number v-model=“scope.row.data[scope.column.property]” v-bind=“getRendAttrs(scope)” @change=“changeEvent(scope)"></el-input-number> </template> <template v-else-if=“editRender.name === ‘ElSwitch’"> <el-switch v-model=“scope.row.data[scope.column.property]” v-bind=“getRendAttrs(scope)” @change=“changeEvent(scope)"></el-switch> </template> <template v-else-if=“editRender.name === ‘ElRate’"> <el-rate v-model=“scope.row.data[scope.column.property]” v-bind=“getRendAttrs(scope)” @change=“changeEvent(scope)"></el-rate> </template> <template v-else-if=“editRender.name === ‘ElColorPicker’"> <el-color-picker v-model=“scope.row.data[scope.column.property]” v-bind=“getRendAttrs(scope)” @change=“changeEvent(scope)"></el-color-picker> </template> <template v-else-if=“editRender.name === ‘ElSlider’"> <el-slider v-model=“scope.row.data[scope.column.property]” v-bind=“getRendAttrs(scope)” @change=“changeEvent(scope)"></el-slider> </template> <template v-else> <el-input v-model=“scope.row.data[scope.column.property]” @change=“changeEvent(scope)"></el-input> </template> </slot> </template> <template v-else> <template v-if=“scope.row.editable.mode === ‘row’ ? scope.row.editActive : scope.row.editActive === scope.column.property”> <slot name=“edit” v-bind=”{$index: scope.$index, row: scope.row.data, column: scope.column, store: scope.store, _row: scope.row}"> <template v-if=“editRender.name === ‘ElSelect’"> <el-select v-model=“scope.row.data[scope.column.property]” v-bind=“getRendAttrs(scope)” @change=“changeEvent(scope)"> <el-option v-for="(item, index) in editRender.options” :key=“index” :value=“item.value” :label=“item.label” v-bind=“editRender.optionAttrs”></el-option> </el-select> </template> <template v-else-if=“editRender.name === ‘ElCascader’"> <el-cascader v-model=“scope.row.data[scope.column.property]” v-bind=“getRendAttrs(scope)” @change=“changeEvent(scope)"></el-cascader> </template> <template v-else-if=“editRender.name === ‘ElTimePicker’"> <el-time-picker v-model=“scope.row.data[scope.column.property]” v-bind=“getRendAttrs(scope)” @change=“changeEvent(scope)"></el-time-picker> </template> <template v-else-if=“editRender.name === ‘ElDatePicker’"> <el-date-picker v-model=“scope.row.data[scope.column.property]” v-bind=“getRendAttrs(scope)” @change=“changeEvent(scope)"></el-date-picker> </template> <template v-else-if=“editRender.name === ‘ElInputNumber’"> <el-input-number v-model=“scope.row.data[scope.column.property]” v-bind=“getRendAttrs(scope)” @change=“changeEvent(scope)"></el-input-number> </template> <template v-else-if=“editRender.name === ‘ElSwitch’"> <el-switch v-model=“scope.row.data[scope.column.property]” v-bind=“getRendAttrs(scope)” @change=“changeEvent(scope)"></el-switch> </template> <template v-else-if=“editRender.name === ‘ElRate’"> <el-rate v-model=“scope.row.data[scope.column.property]” v-bind=“getRendAttrs(scope)” @change=“changeEvent(scope)"></el-rate> </template> <template v-else-if=“editRender.name === ‘ElColorPicker’"> <el-color-picker v-model=“scope.row.data[scope.column.property]” v-bind=“getRendAttrs(scope)” @change=“changeEvent(scope)"></el-color-picker> </template> <template v-else-if=“editRender.name === ‘ElSlider’"> <el-slider v-model=“scope.row.data[scope.column.property]” v-bind=“getRendAttrs(scope)” @change=“changeEvent(scope)"></el-slider> </template> <template v-else> <el-input v-model=“scope.row.data[scope.column.property]” @change=“changeEvent(scope)"></el-input> </template> </slot> </template> <template v-else> <slot v-bind=”{$index: scope.$index, row: scope.row.data, column: scope.column, store: scope.store, _row: scope.row}"> <template v-if=“editRender.name === ‘ElSelect’">{{ getSelectLabel(scope) }}</template> <template v-else-if=“editRender.name === ‘ElCascader’">{{ getCascaderLabel(scope) }}</template> <template v-else-if=“editRender.name === ‘ElTimePicker’">{{ getTimePickerLabel(scope) }}</template> <template v-else-if=“editRender.name === ‘ElDatePicker’">{{ getDatePickerLabel(scope) }}</template> <template v-else>{{ formatter ? formatter(scope.row.data, scope.column, scope.row.data[scope.column.property], scope.$index) : scope.row.data[scope.column.property] }}</template> </slot> </template> </template> </template> </el-table-column> <el-table-column v-else v-bind=“attrs”> <template slot-scope=“scope”> <slot v-bind=”{$index: scope.$index, row: scope.row.data, column: scope.column, store: scope.store, _row: scope.row}">{{ formatter ? formatter(scope.row.data, scope.column, scope.row.data[scope.column.property], scope.$index) : scope.row.data[scope.column.property] }}</slot> </template> </el-table-column></template><script>import XEUtils from ‘xe-utils’export default { name: ‘ElEditableColumn’, props: { group: Boolean, editRender: Object, index: [Number, Function], type: String, label: String, columnKey: String, prop: String, width: String, minWidth: String, fixed: [Boolean, String], sortable: [Boolean, String], sortMethod: Function, sortBy: [String, Array, Function], sortOrders: Array, resizable: { type: Boolean, default: true }, formatter: Function, showOverflowTooltip: Boolean, align: { type: String, default: ’left’ }, headerAlign: String, className: { type: String, default: ’’ }, labelClassName: String, selectable: Function, reserveSelection: Boolean, filters: Array, filterPlacement: String, filterMultiple: { type: Boolean, default: true }, filterMethod: Function, filteredValue: Array }, computed: { attrs () { return { index: this.index, type: this.type, label: this.label, columnKey: this.columnKey, prop: this.prop, width: this.width, minWidth: this.minWidth, fixed: this.fixed, sortable: this.sortable, sortMethod: this.sortMethod ? this.sortMethodEvent : this.sortMethod, sortBy: XEUtils.isFunction(this.sortBy) ? this.sortByEvent : this.sortBy, sortOrders: this.sortOrders, resizable: this.resizable, showOverflowTooltip: this.showOverflowTooltip, align: this.align, headerAlign: this.headerAlign, className: editable-column ${this.editRender ? 'editable-col_edit' : 'editable-col_readonly'}${this.className ? ' ' + this.className : ''}, labelClassName: this.labelClassName, selectable: this.selectable ? this.selectableEvent : this.selectable, reserveSelection: this.reserveSelection, filters: this.filters, filterPlacement: this.filterPlacement, filterMultiple: this.filterMultiple, filterMethod: this.filterMethod ? this.filterMethodEvent : this.filterMethod, filteredValue: this.filteredValue } } }, methods: { getRendAttrs ({ row }) { let size = row.editable.size return Object.assign({ size }, this.editRender.attrs) }, getSelectLabel (scope) { let value = scope.row.data[scope.column.property] let selectItem = this.editRender.options.find(item => item.value === value) return selectItem ? selectItem.label : null }, matchCascaderData (values, index, list, labels) { let val = values[index] if (list && values.length > index) { list.forEach(item => { if (item.value === val) { labels.push(item.label) this.matchCascaderData(values, ++index, item.children, labels) } }) } }, getCascaderLabel (scope) { let values = scope.row.data[scope.column.property] || [] let labels = [] let attrs = this.editRender.attrs || {} this.matchCascaderData(values, 0, attrs.options || [], labels) return labels.join(attrs.separator || ‘/’) }, getTimePickerLabel (scope) { let value = scope.row.data[scope.column.property] let attrs = this.editRender.attrs || {} return XEUtils.toDateString(value, attrs.format || ‘hh:mm:ss’) }, getDatePickerLabel (scope) { let value = scope.row.data[scope.column.property] let attrs = this.editRender.attrs || {} if (attrs.type === ‘datetimerange’) { return XEUtils.toArray(value).map(date => XEUtils.toDateString(date, attrs.format)).join(attrs.rangeSeparator) } return XEUtils.toDateString(value, attrs.format, ‘yyyy-MM-dd’) }, sortByEvent (row, index) { return this.sortBy(row.data, index) }, sortMethodEvent (a, b) { return this.sortMethod(a.data, b.data) }, selectableEvent (row, index) { return this.selectable(row.data, index) }, filterMethodEvent (value, row, column) { return this.filterMethod(value, row.data, column) }, changeEvent ({ $index, row, column, store }) { if (row.editable.showStatus) { this.$nextTick(() => { let trElem = store.table.$el.querySelectorAll(’.el-table__row’)[$index] let tdElem = trElem.querySelector(.${column.id}) let classList = tdElem.className.split(’ ‘) if (XEUtils.isEqual(row.data[column.property], row.store[column.property])) { tdElem.className = classList.filter(name => name !== ’editable-col_dirty’).join(’ ‘) } else { if (!classList.includes(’editable-col_dirty’)) { classList.push(’editable-col_dirty’) tdElem.className = classList.join(’ ‘) } } }) } } }}</script><style lang=“scss”>.editable { &.editable–icon { .editable-header-icon { display: inline-block; } } &.el-table–mini { .editable-column { height: 42px; } } &.el-table–small { .editable-column { height: 48px; } } &.el-table–medium { .editable-column { height: 62px; } } .editable-header-icon { display: none; } .editable-column { height: 62px; padding: 0; &.editable-col_dirty { position: relative; &:before { content: ‘’; top: -5px; left: -5px; position: absolute; border: 5px solid; border-color: transparent #C00000 transparent transparent; transform: rotate(45deg); } } .cell { >.edit-input, >.el-cascader, >.el-autocomplete, >.el-input-number, >.el-date-editor, >.el-select { width: 100%; } } }}</style>使用全局事件需要依赖 vuex 中的 globalClick 变量 (参考store/modules/event.js)将 Editable.vue 和 EditableColumn.vue 复制到自己项目的 components 目录下然后在 main.js 引入组件即可import Editable from ‘@/components/Editable.vue’import EditableColumn from ‘@/components/EditableColumn.vue’Vue.component(Editable.name, Editable)Vue.component(EditableColumn.name, EditableColumn)<template> <div> <el-button type=“primary” @click="$refs.editable.insert({name: ’new1’})">新增</el-button> <el-button type=“danger” @click="$refs.editable.removeSelecteds()">删除选中</el-button> <el-button type=“danger” @click="$refs.editable.clear()">清空所有</el-button> <el-editable ref=“editable” :data.sync=“list”> <el-editable-column type=“selection” width=“55”></el-editable-column> <el-editable-column type=“index” width=“55”></el-editable-column> <el-editable-column prop=“name” label=“名字”></el-editable-column> <el-editable-column prop=“sex” label=“性别” :editRender="{name: ‘ElSelect’, options: sexList}"></el-editable-column> <el-editable-column prop=“age” label=“年龄” :editRender="{name: ‘ElInputNumber’, attrs: {min: 1, max: 200}}"></el-editable-column> <el-editable-column prop=“region” label=“地区” :editRender="{name: ‘ElCascader’, attrs: {options: regionList}}"></el-editable-column> <el-editable-column prop=“birthdate” label=“出生日期” :editRender="{name: ‘ElDatePicker’, attrs: {type: ‘date’, format: ‘yyyy-MM-dd’}}"></el-editable-column> <el-editable-column prop=“date1” label=“选择日期” :editRender="{name: ‘ElDatePicker’, attrs: {type: ‘datetime’, format: ‘yyyy-MM-dd hh:mm:ss’}}"></el-editable-column> <el-editable-column prop=“flag” label=“是否启用” :editRender="{name: ‘ElSwitch’}"></el-editable-column> <el-editable-column prop=“remark” label=“备注” :editRender="{name: ‘ElInput’}"></el-editable-column> <el-editable-column label=“操作”> <template slot-scope=“scope”> <el-button size=“mini” type=“danger” @click=“removeEvent(scope.row, scope.$index)">删除</el-button> </template> </el-editable-column> </el-editable> </div></template><script>import { MessageBox } from ’element-ui’export default { data () { return { sexList: [ { label: ‘男’, value: ‘1’ }, { label: ‘女’, value: ‘0’ } ], regionList: [ { value: ‘bj’, label: ‘北京’, children: [ { value: ‘bjs’, label: ‘北京市’, children: [ { value: ‘dcq’, label: ‘东城区’ } ] } ] }, { value: ‘gds’, label: ‘广东省’, children: [ { value: ‘szs’, label: ‘深圳市’, children: [ { value: ’lhq’, label: ‘罗湖区’ } ] }, { value: ‘gzs’, label: ‘广州市’, children: [ { value: ’thq’, label: ‘天河区’ } ] } ] } ], list: [ { name: ’test11’, height: 176, age: 26, sex: ‘1’, region: null, birthdate: new Date(1994, 0, 1), date1: new Date(2019, 0, 1, 20, 0, 30), remark: ‘备注1’, flag: false }, { name: ’test22’, height: 166, age: 24, sex: ‘0’, region: [‘gds’, ‘szs’, ’lhq’], birthdate: new Date(1992, 0, 1), date1: new Date(2019, 0, 1, 12, 10, 30), remark: ‘备注2’, flag: true }, { name: ’test33’, height: 172, age: 22, sex: ‘1’, region: [‘bj’, ‘bjs’, ‘dcq’], birthdate: new Date(1990, 0, 1), date1: new Date(2019, 0, 1, 0, 30, 50), remark: null, flag: false } ] } }, methods: { removeEvent (row, index) { MessageBox.confirm(‘确定删除该数据?’, ‘温馨提示’, { confirmButtonText: ‘确定’, cancelButtonText: ‘取消’, type: ‘warning’ }).then(() => { this.$refs.editable.removeRow(index) }).catch(e => e) } }}</script> ...

January 20, 2019 · 13 min · jiezi

Element-UI单选框(el-radio-group)点击事情的问题

结合el-radio-group元素和子元素el-radio可以实现单选组: template部分: <el-radio-group v-model="tabPosition" v-for="item in tabs" :key="item.id" @click="toggleTab(item.id)"> <el-radio-button :label="item.name"></el-radio-button></el-radio-group> script部分: export default { data() { return { tabPosition: '英语', tabs: [] }; }, created() { this.getTabs(); }, methods: { getTabs() { this.$http.getData('/categories').then(val => { this.tabs = val.data; }) } } 发现,点击按钮没有效果。查阅文档发现radio-group点击事情使用的是change,而我之前惯性使用了click。 ...

January 16, 2019 · 1 min · jiezi

Element输入框带历史查询记录

需求描述页面的查询框增加一下显示历史查找记录实现及踩坑记录使用Element带输入建议的输入框来实现此需求。用法详见官网1. 坑1:不能直接在querySearch里返回数组,一定要调用回调函数cb来处理数据看了一下例子,建议列表应该是个数组,然后我就在querySearch里直接返回了一个数组: querySearch(queryString, cb) { return JSON.parse(localStorage.getItem(‘srcOrderNoArr’)) },但是回到网页上却发现列表数据加载不出来正确姿势: /** * 建议列表 /querySearch(queryString, cb) { // 调用 callback 返回建议列表的数据 cb(JSON.parse(localStorage.getItem(‘srcOrderNoArr’)))}2. 坑2:数组内数据格式有要求,格式一定要是[{value: ‘’, xxx: ‘’}, {value: ‘’, xxx: ‘’}, …]这个建议列表是根据数组内的value属性值来渲染的,所以数组内的对象一定要有value键值对。比如说是这样的:data () { return { srcOrderNoArr: [{ value: ‘’, // 这个必须要有 type: ’’ }, { value: ‘’, type: ’’ }, { value: ‘’, type: ’’ }] }}methods: { /* * 把搜索记录存入localStorage */ setIntoStorage (type) { let self = this let noArr = [], // 订单号历史记录数组 text = ‘’, value = ’’ switch (type) { case ‘srcOrderNo’: text = ‘srcOrderNoArr’ value = self.srcOrderNo break case ‘jobOrderNo’: text = ‘jobOrderNoArr’ value = self.jobOrderNo break case ‘cntNo’: text = ‘cntNoArr’ value = self.cntNo break default: break } noArr.push({value: value, type: type}) if(JSON.parse(localStorage.getItem(text))) { // 判断是否已有xxx查询记录的localStorage if(localStorage.getItem(text).indexOf(value) > -1 ) { // 判断是否已有此条查询记录,若已存在,则不需再存储 return } if(JSON.parse(localStorage.getItem(text)).length >= 5) { let arr = JSON.parse(localStorage.getItem(text)) arr.pop() localStorage.setItem(text, JSON.stringify(arr)) } localStorage.setItem(text, JSON.stringify(noArr.concat(JSON.parse(localStorage.getItem(text))))) } else { // 首次创建 localStorage.setItem(text, JSON.stringify(noArr)) } }}参考vue中使用localStorage存储信息element-ui带输入建议的input框踩坑 ...

January 14, 2019 · 1 min · jiezi

多级checkbox关联选择

效果图:<template> <app-scroll-box class=“page-clientsManage”> <el-row class=“pa-20”> <el-card class=“table-card mt-20”> <el-row slot=“header” class=“clearfix”> <el-row class=“fl”> <span class=“title”>疫区设置:{{ itemTitle }}</span> </el-row> </el-row> <div class=“deliverySetting”> <div class=“deliverySetting-table” v-for="(item, idx) in countries" :key=“idx”> <div class=“table-body” v-for="(pro, idx2) in item.pros" :key=“idx2”> <div class=“first-col”> <span v-if=“idx2 == 0” style=“display: inline-block; line-height: 30px; cursor: pointer; " > <el-checkbox @change=“handleChecked(‘country’, item)” :label=“item.cname” v-model=“item.checked” :key=“item.cid”>{{ item.cname }}</el-checkbox> </span> </div> <div class=“width120”> <span style=“display: inline-block; line-height: 30px; cursor: pointer; “> <el-checkbox @change=“handleChecked(‘pro’, pro)” :label=“pro.pname” v-model=“pro.checked” :key=“pro.pid”>{{ pro.pname }}</el-checkbox> </span> </div> <div class=“width265”> <el-checkbox v-for=“c in pro.cities” v-model=“c.checked” :label=“c.cityName” :key=“c.cityId” @change=“handleChecked(‘city’, c)” >{{c.cityName}}</el-checkbox> </div> </div> </div> </div> </el-card> </el-row> </app-scroll-box></template><script>export default { name: “deliverySetting”, components: {}, props: {}, data() { return { itemTitle: “”, itemId: 0, countries: [ { cid: 1, cname: “中国”, pid: “1”, pname: “广东”, cityId: 1, cityName: “深圳” }, { cid: 1, cname: “中国”, pid: “1”, pname: “广东”, cityId: 2, cityName: “肇庆” }, { cid: 1, cname: “中国”, pid: “2”, pname: “湖北”, cityId: 3, cityName: “武汉” }, { cid: 1, cname: “中国”, pid: “2”, pname: “湖北”, cityId: 4, cityName: “咸宁” }, { cid: 1, cname: “中国”, pid: “2”, pname: “湖北”, cityId: 5, cityName: “恩施” }, { cid: 2, cname: “美国”, pid: “3”, pname: “usa省州1”, cityId: 6, cityName: “usa-city-1” }, { cid: 2, cname: “美国”, pid: “3”, pname: “usa省州1”, cityId: 7, cityName: “usa-city-2” }, { cid: 2, cname: “美国”, pid: “3”, pname: “usa省州1”, cityId: 8, cityName: “usa-city-3” }, { cid: 2, cname: “美国”, pid: “4”, pname: “usa省州2”, cityId: 9, cityName: “usa-city-4” }, { cid: 2, cname: “美国”, pid: “4”, pname: “usa省州2”, cityId: 10, cityName: “usa-city-5” } ], checkedCities: [1, 4, 9, 10] }; }, computed: {}, methods: { initPage() { let { itemId, itemTitle } = this.$route.query; this.itemId = itemId; this.itemTitle = itemTitle; this.initAreaData(); }, //初始化数据 initAreaData() { this.countries = this.countries.map(item => { let res = item; if (this.checkedCities.includes(item.cityId)) { res.checked = true; } else { res.checked = false; } return res; }); let resCities = []; this.countries.forEach(item => { let obj = {}; let existsCountry = resCities.find(c => { return c.cid == item.cid; }); if (existsCountry) { obj = existsCountry; } else { obj.cid = item.cid; obj.cname = item.cname; } obj.checked = false; if (!obj.pros) { obj.pros = []; } let existsPro = obj.pros.find(c => { return c.pid == item.pid; }); if (!existsPro) { existsPro = { pid: item.pid, pname: item.pname, checked: false }; obj.pros.push(existsPro); } if (!existsPro.cities) { existsPro.cities = []; } existsPro.cities.push({ cityId: item.cityId, cityName: item.cityName, checked: item.checked }); if (!existsCountry) { resCities.push(obj); } }); this.countries = resCities; this.opreationData() }, //操作数据 opreationData() { this.countries.forEach(c => { c.pros.forEach(p => { if(p.cities && p.cities.length > 0){ p.checked = p.cities.every(p => p.checked) } }) if(c.pros && c.pros.length > 0){ c.checked = c.pros.every(p => p.checked) } }) }, handleChecked(level, item) { let isChecked = item.checked switch(level){ case “country”: if(item && item.pros){ item.pros.forEach(p => { if(p.cities){ p.cities.forEach(c => { c.checked = isChecked }) } }) } break; case “pro”: if(item && item.cities){ item.cities.forEach(c => { c.checked = isChecked }) } break; case “city”: break; } this.opreationData() } }, created: function() {}, mounted() { this.initPage(); }, watch: {}};</script><style>.el-checkbox__label { padding-left: 5px;}.first-col { padding-left: 20px; width: 120px;}.deliverySetting { padding: 20px 0; position: relative;}.deliverySetting .el-table thead tr th { font-size: 14px;}.deliverySetting .el-table tbody tr td { vertical-align: baseline;}.deliverySetting .el-table tbody tr td p { line-height: 30px;}.deliverySetting .el-table tbody tr td .el-checkbox-group { display: flex; flex-direction: column;}.deliverySetting .el-table tbody tr td .el-checkbox-group label { line-height: 30px; margin-left: 0;}.deliverySetting .deliverySetting-table { font-size: 14px; color: #333;}.deliverySetting .deliverySetting-table .table-head,.deliverySetting .deliverySetting-table .table-body { display: flex; padding: 10px 0;}.deliverySetting .deliverySetting-table .table-head .selection,.deliverySetting .deliverySetting-table .table-body .selection { /* width: 45px; / text-align: center; line-height: 36px;}.deliverySetting .deliverySetting-table .table-head .width120,.deliverySetting .deliverySetting-table .table-body .width120 { width: 120px;}/ .deliverySetting .deliverySetting-table .table-head .width265,.deliverySetting .deliverySetting-table .table-body .width265 { width: 265px;} /.deliverySetting .deliverySetting-table .table-head { height: 36px; align-items: center; background-color: #e7f2ff;}.deliverySetting .deliverySetting-table .table-body { border-bottom: 1px solid #e4e4e4; color: #666;}.deliverySetting .deliverySetting-table .table-body:hover { background-color: #f5f7fa;}/ .deliverySetting .deliverySetting-table .table-body .width265 { display: flex; flex-direction: column;} */.deliverySetting .deliverySetting-table .table-body .width265 label { line-height: 30px; margin-left: 0; color: #666; padding: 0 5px;}.deliverySetting .deliverySetting-table .table-body p { line-height: 30px;}.deliverySetting .deliverySetting-btn { /width: 100%;/ height: 59px; display: flex; justify-content: flex-end; align-items: center; position: absolute; top: -55px; right: -16px; z-index: 100;}.deliverySetting .deliverySetting-btn .tabs-btn { min-width: 90px; height: 34px; line-height: 32px; padding: 0 10px; color: #2387f7; border: solid 1px #4fa2ff; background-color: #e7f2ff; cursor: pointer;}.deliverySetting .deliverySetting-btn .tabs-btn:nth-of-type(2) { margin: 0 15px;}.deliverySetting .deliverySetting-btn .tabs-btn input { border: none; background: transparent; color: inherit; cursor: inherit; outline: none; margin: 0; padding: 0;}.deliverySetting .deliverySetting-btn .tabs-btn:hover { color: #fff; background-color: #2387f7;}.deliverySetting .setDistributorDailog .el-input { width: 270px;}</style> ...

January 13, 2019 · 4 min · jiezi

记一次解题思路

2019年1月12日 上午11:30 picker-options firstDayofWeek起因昨天下班前开了一个小会,会议中提到了一个问题:element-ui日期组件默认是从周日开始的(如下图),目的是希望能从周一开始。一个成熟的组件,这个基本功能怎么可能没有?!(如果没有,组件是否方便扩展实现)但他们说查阅了文档,没发现该功能。探索查文档本着怀疑一切的态度,会议结束后,我去翻了一下文档。发现文档里是有这个配置的:文档中写了可选值1到7,默认值7,这不正是我们要的吗?只要把这个配置设置为1不就行了吗 ?(这个时候我并没有注意到这个配置的标题是Picker Options)文档上明明写的有,为什么他们说不行呢?那就让我来验证一下吧!验证于是我本地起了个服务,直接给ElDatePicker 组件传入了一个firstDayofWeek:1,<el-date-picker …(省略) :firstDayOfWeek=“1” placeholder=“选择日期”></el-date-picker>咦 ?确实没生效,日期还是从7开始的。我值传错了?回看一下文档类型是Number,没错啊。组件接收到我传的值了吗?排疑通过Vue Devtools查看了一下奇怪,是7也就算了,怎么还是挂在data下面的,为什么不是props?再验证我点击旁边的-号修改了这个值,发现日期组件随着这个值在变化!(只要把值调为1,就是我们希望的结果)得出结论这说明了什么??说明这确实是可配置的!!那么问题就很明显了:要么是我配置错了要么是组件有bug再探索提出疑问于是我又回头看了一下文档Picker Options 上方的表格是 Attributes ?放在Attributes里才是直接通过props传递啊?firstDayOfWeek为什么不放在Attributes里?(由于没用过这个组件,并没仔细去看Attributes里的每一个值,此时还没有发现Attributes表格里面有picker-options,如果能认真一点的话,事情到这里就该结束????)跟随疑问带着一肚子疑问去看了下组件源码当我看到firstDayOfWeek在data里定义,而且当前文件没有对它赋值的时候,居然就天真的认为这是一个bug,为什么把要一个配置的值放在data里写死了7?放在data里我怎么传值?(这个地方的我太草率了!! 这个时候正确的做法应该是:耐心的接着看一下,比如mixins,如果这里仔细点,问题到这里也能解决了)<date-table …(省略部分属性) :first-day-of-week=“firstDayOfWeek”></date-table>data() { return { …(省略) firstDayOfWeek: 7 };}我愚蠢的把这个当成是一个bug,于是打开了github,去issues里搜索了一下看有没有提过这个bug,一搜,还真有。[Bug Report] datepicker week type doesn’t consider firstWeekOfDay下面有一条维护者的回复:“firstDayOfWeek should be nested in picker-options"找到正确使用方法⚡️⚡️⚡️⚡️⚡️(我真蠢,真的)此时才明白上面那个疑问(为什么firstDayOfWeek不放在Attributes)于是这个时候,我才去Attributes下面看了一下,看见picker-options安安静静地躺在里面。改为下面这样,就行了。:picker-options=”{ firstDayOfWeek: 1}“探索答疑事情到了这里,已经结束了吗?不!上面还有一个疑问没解决呢(为什么pickerDayofWeek在data里,那它是怎么改变的?)我这次由外到内,仔细的看了一下组件源码,这才看清楚了const updateOptions = () => { …(省略) for (const option in options) { if (options.hasOwnProperty(option) && // 忽略 time-picker 的该配置项 option !== ‘selectableRange’) { this.picker[option] = options[option]; // 赋值在这里! } }}this.unwatchPickerOptions = this.$watch(‘pickerOptions’, () => updateOptions(), { deep: true });组件的细节这里就不赘述了,有兴趣可以自己去看。最后:这更像是一篇流水账式的叙事文,问题是个小问题,希望大家能从解决问题的思路上有点启发。鸡汤式反思:探索既然已经给出了firstDayOfWeek,那就说明功能是有的,没生效不能武断认为它没提供。认真曾经有两次差点接近真相,但都错过了。这不是一次成功的解题,是走了弯路最后解决的问题。共勉。 ...

January 12, 2019 · 1 min · jiezi

Vue使用element-ui所遇BUG与需求集结(二)

第二版啦(^U^)ノYO由于项目的功能越来越多,开始注重细节的优化和可延展性,主要方向是将复用的代码集成一个组件。以下数据都是在vue(^2.5)+vuex(^2.3)+element-ui(^2.3)+webpack(^3.7)+axios(v0.16)环境下测试。混入(Mixins)由于有很多table都是要请求列表总数,包括请求列表也是千篇一律。一开始用的是Eventbus注册了共用组件,后来感觉对非列表的组件来说是累赘,才改用mixins,既做到了通用,还能复写。虽然这个方法个人还是不太满意。。。求更好ideain @/componens/mixins/TableList.jsexport default{ data(){ return { isLoading: false } }, methods: { getTotal(response){ // 加个loading状态 this.isLoading = !this.isLoading // 开始请求列表 this.getTableList() }, getList(response){ // 结束loading状态 this.isLoading = !this.isLoading }, getTableListCount(total){ this.$http.get(total.api[0],{}).then((response) => { if(response.data && typeof response.data === ‘object’){ // 成功获取数量回调 this.getTotal(response) } }).catch(() => {}) }, getTableList(){ this.$http.get(total.api[1],{}).then((response) => { if(response.data && typeof response.data === ‘object’){ // 成功获取列表回调 this.getList(response) } }).catch(() => {}) } }};在组件内引用:import TableListMX from ‘@/components/mixins/TableList’;export default{ mixins: [TableListMX], methods: { getStart(){ this.getTableListCount({ api: [’list_count’, ’list’] }) } }}自定义指令项目后台当然少不了表单,联系到数据输入,这时候限制输入内容显得尤为必要。如果每个输入框都在输入时做提示就显得累赘,所以用了指令去限制输入。这里举例一个限制小数点位数的自定义指令(参考大佬地址)in @directives/InputNumDigit/* @directive 输入框限制范围:小数点个数 or 整数 @param {data-index} 如果是数组要加入index @param {data-dotrange}/// 寻找当前domlet FindElement = (parent, selector) => { return parent.tagName.toLowerCase() === selector ? parent : parent.querySelector(selector);};// 设置组件中的指定属性的值let setValue = function(exp, value, context) { value = isNaN(value) ? ’’ : value new Function(‘context’, ‘value’, context.${exp} = value)(context, value)};export default{ bind: function(el, { expression }, { context }){ let $input = FindElement(el, ‘input’); el.$input = $input; // 初始化lastValue $input.lastValue = $input.value // 通过dataset 判断是否允许小数点 let allowDot = !!$input.dataset.dotrange, keys = $input.dataset.keys || -1,// 如果是数组则加入索引 dotRange = $input.dataset.dotrange || {0,2}, // 默认 pattern = ^[0-9]+${allowDot ? (.[0-9]${dotRange})? : ''}$, new_expression = expression; if (!expression) { throw new TypeError(‘请绑定expression’) } // 循环 if(keys !== -1){ new_expression = expression.replace(/[.?]/, [${keys}]) } console.log(new_expression) $input.handleInputEvent = function(e) { setTimeout(() => { if (e.target.value === ‘’) { setValue(new_expression, ‘’, context) // 遇到非法数值,则重置 e.target.value = ’’ } else if (e.target.value !== ’’ && !new RegExp(pattern).test(e.target.value)) { setValue(new_expression, parseFloat(e.target.lastValue), context) // 遇到非法数值,则重置为lastValue e.target.value = e.target.lastValue if (allowDot) { $input.title = 小数点后最多${dotRange.replace(/[}{]/g, '').split(',')[1]}位 } } e.target.lastValue = e.target.value }, 0) } $input.addEventListener(‘input’, $input.handleInputEvent, false) }, unbind(el) { el && el.$input.removeEventListener(‘input’, el.$input.handleInputEvent, false) }};全局注册:import InputNumDigit from ‘@/directives/InputNumDigit’Vue.directive(’num-digit’, InputNumDigit)引用:<!– 最多只能输入三位小数 –><el-input v-model.number.trim=“rate” v-num-digit=“setForm.rate” data-dotrange="{0,3}" type=“number”></el-input>有个瑕疵,这个指令只兼容了第一层for循环时的情况,没有考虑到更复杂的情况自定义过滤器比如,将数字转换成千分位let ToThousands = (val) => {// 数字转换成千分位 if(!val || val === 0 || isNaN(val)){ return val }else{ let num = 0; if(val.toString().indexOf (’.’) !== -1){// 带小数点 num = val.toLocaleString() }else{ num = val.toString().replace(/(\d)(?=(?:\d{3})+$)/g, ‘$1,’); } return num; }};Vue.filter(‘ToThousands’, ToThousands);// or 全局通用Vue.prototype.$toThousands = ToThousands;常用表单验证表单验证少不了,列几个在后台常用的,放在store可以随时调用let validator = {// 验证信息 mobile: (rule, value, callback) => {// 手机号码 let reg = /^1[3|4|5|6|7|8|9][0-9]{9}$/; if(!reg.test(value)){ callback(new Error(“请输入正确手机号码”)) }else{ callback() } }, idcard: (rule, value, callback) => {// 身份证号码 let dalu_reg = /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/,// 大陆 xianggang_reg = /[A-Z]{1,2}[0-9]{6}([0-9A])/,// 香港 aomen_reg = /^[1|5|7][0-9]{6}([0-9Aa])/,// 澳门 taiwan_reg = /[A-Z][0-9]{9}/;// 台湾 if(!dalu_reg.test(value) && !xianggang_reg.test(value) && !aomen_reg.test(value) && !taiwan_reg.test(value)){ callback(new Error(“请输入正确身份证号码”)) }else{ callback() } }, noChinese: (rule, value, callback) => {// 英文和数字 let reg = /^[A-Za-z0-9]+$/g; if(!reg.test(value)){ callback(new Error(“不能输入中文!”)) }else{ callback() } }, limitNumber: (rule, value, callback) => {// 限制数字大小 if(value < 0 || value >= 10000000){ callback(new Error(“最多输入7位有效数字”)) }else if(isNaN(value)){ callback(new Error(“请输入数字”)) }else{ callback() } }, limitPercent: (rule, value, callback) => {// 限制百分比 if(value < 0 || value > 100){ callback(new Error(“请输入0100之间的数字”)) }else{ callback() } }};常用日期限制范围let dateLimit = { beforeToday: {// 今天之前:不包含今天 disabledDate(time) { return time.getTime() > Date.now() - 8.64e7; } }, beforeTomorrow: {// 明天之前:包含今天 disabledDate(time) { return time.getTime() > Date.now(); } }, afterToday: {// 今天之后:包含今天 disabledDate(time) { return time.getTime() < Date.now() - 8.64e7; } }, afterTomorrow: {// 明天之后:不包含今天 disabledDate(time) { return time.getTime() < Date.now(); } }};分页自定义页数<!– 通过page-size与输入框同一个参数控制 –><el-pagination class=“sl-page"layout=“prev, pager, next, jumper, total, slot”@current-change=“handleCurrentChange”:current-page.sync=“params.pager”:page-size=“params.count”:total=“TOTAL”> <span class=“resize”> <span>每页记录数:</span> <el-input type=“number” size=“small” v-model.number=“params.count” @change=“handleSearch”></el-input> </span></el-pagination>export defalt{ data(){ return { TOTAL: 100, params: { pager: 1, count: 10 } } }, methods: { handleSearch(){//页数改变 if(this.parmas.count <= 0 || !this.parmas.count || String(this.parmas.count).indexOf(”.") !== -1){ //小于等于零or为空or小数点时不刷新数据 return false; } this.parmas.pager = 1; this.getList() }, handleCurrentChange(){//页码改变 this.getList() } }};.sl-page { text-align: center; padding-top: 20px;}.sl-page .resize { width: 60px;}.sl-page .resize .el-input__inner { height: 28px; padding: 0 5px;}伪Radio · 真 · CheckBox单个radio可以不勾选效果复合:<div class=“fake-checkbox”> <el-radio v-model=“isCheck” :label=“1”>是否勾选</el-radio> <el-checkbox v-model=“isCheck” :false-label=“0” :true-label=“1” @change=“handleSearch”>是否勾选</el-checkbox></div>.fake-checkbox{ position: relative; display: inline-block;}.fake-checkbox .el-checkbox,.fake-checkbox .el-radio{ width: 55px;}.fake-checkbox .el-radio__label{ padding-left: 5px;}.fake-checkbox .el-checkbox{ position: absolute; top: 0; right: 0; opacity: 0;}样式修改1. 下拉箭头位置偏差.el-select-dropdown .popper__arrow { transform: none !important;}2. 表单伪必填.sl-required .el-form-item__label:before { content: ‘*’; color: #fa5555; margin-right: 4px;}<el-form-item class=“sl-required”>…</el-form-item>3. 去除输入框number类型的箭头.non-arrow input[type=“number”]::-webkit-outer-spin-button,.non-arrow input[type=“number”]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0;}<el-input class=“non-arrow” type=“number”></el-input> ...

January 4, 2019 · 3 min · jiezi

vue管理后台

手摸手,带你用vue撸后台 系列四(vueAdmin 一个极简的后台基础模板)基于这篇文章的学习,开始动手写了一个简易的vue管理后台。主要页面如下登录主页 (页面的结构基本和主页相同。左侧导航栏,右侧上方面包屑导航和用户头像,右侧下方大块空白区域对应的是各路由)项目结构views:各业务组件components:公共组件(面包屑,导航开关等等)api: 请求的js文件icons: svg和相关配置文件(此项目的图标是使用svg-sprite-loader来实现的, 之前写过的相关文章链接permission.js 检查权限其他地方与别的项目结构一致,我就不再赘述了。1.实现页面的结构。(因为除了login等特殊页面外,其他的结构都是一致的。所以可以用Layout组件来承载页面的结构)@/views/Layout/的目录结构如下对应到Layout的各个区域如下图。@/router/index.js 路由器。当点击切换路由时,sidebar和navbar不会改变,只有app-main组件的内容会改变。实现思路:左侧导航栏的路由的component为Layout,再通过router的redirect属性控制显示app-main区域的路由。部分代码如下@/Layout/Layout.vue<template> <div class=“app-wrapper” :class=“classObj”> <!–导航 –> <sidebar/> <!–面包屑–> <div class=“main-container”> <!– 头部导航 –> <navbar/> <!– 二级路由 –> <app-main/> </div> </div></template><script>import Sidebar from ‘./components/Sidebar’import Navbar from ‘./components/navbar’import AppMain from ‘./components/AppMain’</script>@/Layou/components/AppMain.vue<template> <section class=“app-main”> <transition name=“fade-transform” mode=“out-in”> <!– 在这里映射不同的路由到Layout中的app-main –> <router-view/> </transition> </section></template>2.左侧导航左侧的导航是通过element-ui的 el-menu组件和循环路由表来实现的。具体效果如下图如果没有子路由,点击直接跳转。有则出现下拉弹框。3.面包屑导航使用的也是element-ui里的el-breadcrumb组件…..未完待续…..

January 2, 2019 · 1 min · jiezi

elementUI对话框ztree显示失败的问题记录

其实这个问题之前已经遇到过了,但是还是在这里踩坑了。趁此机会整理一下,避免再犯。问题描述预期效果:弹出dialog对话框,对话框的内容是由ztree实现的树菜单。实际效果:弹出dialog对话框,对话框内容空白,树菜单显示失败。<template> <el-dialog title=“树菜单” :visible.sync=“dialogVisible”> <div id=“tree-menu” class=“ztree”></div> </el-dialog></template><script>export default { data() { return { dialogVisible: false } }, mounted() { let setting = {view: {showIcon: false}}; let zNodes = [ {id: “1”, name: “nodes1”}, {id: “2”, name: “nodes2”} ]; $.fn.zTree.init($("#tree-menu"), setting, zNodes); }}</script>问题原因使用elementUI 1.4版本的时候就遇到过这个问题了,当时一直以为是自己编码的错误,花了很多时间排查。后来控制台调试的时候发现,没有打开dialog之前是介个样子的:第一次打开dialog之后:基本就能明白,dialog的内容是懒渲染模式。在el-dialog__body未渲染之前是无法获取到其中的DOM元素进行操作的。在elementui最新版本的文档中也有提示出来了:问题解决根据文档提示“如果需要执行 DOM 操作,或通过 ref 获取相应组件,请在 open 事件回调中进行”。但其实在第一次打开dialog的open事件回调执行的时候,仍然无法执行DOM操作,因为这个时候dialog的内容还是未渲染上去。可使用Vue.$nextTick将DOM操作延迟到DOM更新之后执行。因为树菜单的逻辑较为复杂,可复用,所以直接提取成组件。在组件mounted的时候去获取DOM元素来做ztree的初始化操作,可避开dialog懒渲染带来的DOM元素操作问题。

January 1, 2019 · 1 min · jiezi

element-ui如何使用

最近做pc端业务用到了element-ui组件库,从引入到组件改造做个总结Element,一套为开发者、设计师和产品经理准备的基于 Vue 2.0 的桌面端组件库,整个ui风格简约,很实用,使用demo组件可以快速实现体验交互细节,快速开发1.安装,推荐npm方式安装npm i element-ui -S2.引入2.1全局引入,会把组件库里所有的组件和css引入import ElementUI from ’element-ui’; import ’element-ui/lib/theme-chalk/index.css’; Vue.use(ElementUI);2.2局部引入(推荐)由于业务的风格跟默认的颜色不一样,我采用了自定义主题,选择色号生成,在线生成工具(https://elementui.github.io/t…)可以很方便地实时预览主题色改变之后的视觉,同时它还可以基于新的主题色生成完整的样式文件包,供直接下载使用,在vue的入口js里直接引入主题样式element-variables.cssimport ‘@/style/theme/element-variables.scss’import { Message, MessageBox, Loading } from ’element-ui’Vue.use(Loading.directive)Vue.prototype.$loading = Loading.serviceVue.prototype.$msgbox = MessageBoxVue.prototype.$alert = MessageBox.alertVue.prototype.$confirm = MessageBox.confirmVue.prototype.$prompt = MessageBox.promptVue.prototype.$message = Message3.组件使用3.1像message。messagebox弹窗组件是直接挂载全局,因此在 Vue instance 中可以采用本页面中的方式调用 MessageBox。调用参数为:$msgbox(options) $alert(message, title, options) 或 $alert(message, options) $confirm(message, title, options) 或 $confirm(message, options) $prompt(message, title, options) 或 $prompt(message, options)具体使用:图片描述图片描述3.2 官网提供的dialog组件支持内容区更丰富的写法,dialog弹出一个对话框,适合需要定制性更大的场景。官网写法:图片描述这是最基础的使用,在这个基础上我做了个封装,将取消,确定函数挂在组件回调函数上,并且标题,宽度,显隐值都通过属性传递,可以让父组件引用较少的代码父组件:/* * 弹窗组件说明 * @param {object} dialogData: title-标题, width-宽度, dialogVisible-弹窗显隐状态 * @param {function} callback: 点确定位置的回调函数 * @param {function} closeDialog: 弹窗关闭的回调函数 * 示例: <OPdialog :dialog-data=“dialoginfo” @callback=“callback(item.taskinfo.taskDetail.taskid)” @closeDialog=“closeDialog”/> */<OPdialog :dialog-data=“dialoginfo” @callback=“callback(item.taskinfo.taskDetail.taskid)” @closeDialog=“closeDialog”/>子组件:<template> <div> <el-dialog v-if=“dialogData.dialogtype==‘confirm’” :visible.sync=“dialogData.dialogVisible” :modal-append-to-body=“false” :close-on-click-modal=“false” :width=“dialogData.width” top=“33vh” center @close=“closeDialog”> <p class=“dialog-body”>{{ dialogData.title }}</p> <span slot=“footer” class=“dialog-footer”> <el-button size=“medium” type=“primary” @click=“ok()">确定</el-button> <el-button size=“medium” @click=“dialogData.dialogVisible = false”>取 消</el-button> </span> </el-dialog> </div></template> ...

December 29, 2018 · 1 min · jiezi

element 封装form集成校验自定义内容

后台管理系统使用form的频率非常高,并且存在不同的校验,如果不加以封装,大量的ifelse存在,不同人可能写不同的校验规则,大大增加了维护的难度。封装一个form组件 让校验规则全部交由组件处理。封装好解决了很多需求 记录并且分享下接解了校验,让校验由组件来处理,传入相应校验规则即可。使用render让组件可以自定义增加扩展性对比下以下是element的例子 这单单只是js部分加上html的代码量将大大增加封装后的代码demo演示显示github源码及文档说明

December 27, 2018 · 1 min · jiezi

vue+elementUI的图片即时上传

后台管理项目,用到的是vue+elementUI的方式,upload板块,api个人感觉还是不够详尽,现在,来说一下关于立即上传的问题.主要用到的是http-request的覆盖原有上传方式,(因为我需要传token给后台),所以action的值可以写为空串.页面代码:<el-upload class=“upload-demo” name=“usersFile” action="" :on-preview=“handlePreview” :http-request=“uploadFile” :on-remove=“handleRemove” :auto-upload=“true” :on-exceed=“handleExceed” :file-list=“fileList”> <el-button size=“mini” type=“primary”>批量上传</el-button></el-upload>可以看到,http-request对应的方法uploadFile是重点:uploadFile(file){ let formDatas = new FormData(); formDatas.append(‘usersFile’, file.file); formDatas.append(’tokenId’,this.$store.state.user.tokenId); this.$post(’/yourUrl’,formDatas).then(res =>{ if(res.code == 0){ this.$message({ message: res.msg, type: ‘success’ }); }else{ this.$message({ message: res.msg?res.msg:‘操作失败’, type: ’error’ }); } }) },说明:其中字段usersFile就是后台要的key,值就是你选择的文件 ,这样就实现了立即上传.

December 24, 2018 · 1 min · jiezi

el-dialog 模态框拖拽

1.拖拽效果1.1 拖拽前1.2 拖拽后2.拖拽组件。把代码单独拷贝在一个js里即可,这里写在公共文件夹common/js/directives.js里,具体看步骤3示例。参考API https://cn.vuejs.org/v2/api/ 的Vue.directive。import Vue from ‘vue’;// v-dialogDrag: 弹窗拖拽属性 (重点!!! 给模态框添加这个属性模态框就能拖拽了)Vue.directive(‘dialogDrag’, { //属性名称dialogDrag,前面加v- 使用 bind(el, binding, vnode, oldVnode) { const dialogHeaderEl = el.querySelector(’.el-dialog__header’); const dragDom = el.querySelector(’.el-dialog’); //dialogHeaderEl.style.cursor = ‘move’; dialogHeaderEl.style.cssText += ‘;cursor:move;’ dragDom.style.cssText += ‘;top:0px;’ // 获取原有属性 ie dom元素.currentStyle 火狐谷歌 window.getComputedStyle(dom元素, null); const sty = (function() { if (window.document.currentStyle) { return (dom, attr) => dom.currentStyle[attr]; } else{ return (dom, attr) => getComputedStyle(dom, false)[attr]; } })() dialogHeaderEl.onmousedown = (e) => { // 鼠标按下,计算当前元素距离可视区的距离 const disX = e.clientX - dialogHeaderEl.offsetLeft; const disY = e.clientY - dialogHeaderEl.offsetTop; const screenWidth = document.body.clientWidth; // body当前宽度 const screenHeight = document.documentElement.clientHeight; // 可见区域高度(应为body高度,可某些环境下无法获取) const dragDomWidth = dragDom.offsetWidth; // 对话框宽度 const dragDomheight = dragDom.offsetHeight; // 对话框高度 const minDragDomLeft = dragDom.offsetLeft; const maxDragDomLeft = screenWidth - dragDom.offsetLeft - dragDomWidth; const minDragDomTop = dragDom.offsetTop; const maxDragDomTop = screenHeight - dragDom.offsetTop - dragDomheight; // 获取到的值带px 正则匹配替换 let styL = sty(dragDom, ’left’); let styT = sty(dragDom, ’top’); // 注意在ie中 第一次获取到的值为组件自带50% 移动之后赋值为px if(styL.includes(’%’)) { styL = +document.body.clientWidth * (+styL.replace(/%/g, ‘’) / 100); styT = +document.body.clientHeight * (+styT.replace(/%/g, ‘’) / 100); }else { styL = +styL.replace(/\px/g, ‘’); styT = +styT.replace(/\px/g, ‘’); }; document.onmousemove = function (e) { // 通过事件委托,计算移动的距离 let left = e.clientX - disX; let top = e.clientY - disY; // 边界处理 if (-(left) > minDragDomLeft) { left = -(minDragDomLeft); } else if (left > maxDragDomLeft) { left = maxDragDomLeft; } if (-(top) > minDragDomTop) { top = -(minDragDomTop); } else if (top > maxDragDomTop) { top = maxDragDomTop; } // 移动当前元素 dragDom.style.cssText += ;left:${left + styL}px;top:${top + styT}px;; }; document.onmouseup = function (e) { document.onmousemove = null; document.onmouseup = null; }; } }})3.拖拽组件的引入,只要引入了directives.js即可3.1引入import ‘../common/js/directives.js'3.2示例4.使用。给el-dialog添加属性v-dialogDrag即可代码:<el-dialog title=“外币维护” v-dialogDrag size=‘mini’ :close-on-click-modal=“false” :visible.sync=“dialogVisible” width=“400px” @close=“close”> <div class=“mini-font mb-5 balance”> <el-form :model=“form”> <el-form-item label=“外币金额” :label-width=“formLabelWidth”> <!– <el-input v-model=“form.fmMoney” :value=“form.fmMoney | formatMoney1” auto-complete=“off” size=‘mini’></el-input> –> <el-input-number size=‘small’ :precision=“2” :controls=“false” v-model=“form.fmMoney” auto-complete=“off”> </el-input-number> </el-form-item> <el-form-item label=“汇率” :label-width=“formLabelWidth”> <!– <el-input v-model=“form.percent” auto-complete=“off” size=‘mini’></el-input> –> <el-input-number size=‘small’ :precision=“4” :controls=“false” v-model=“form.percent” auto-complete=“off”> </el-input-number> </el-form-item> <el-form-item label=“本位币金额” :label-width=“formLabelWidth”> <!– <el-input v-model=“form.balance” auto-complete=“off” size=‘mini’></el-input> –> <el-input-number size=‘small’ :precision=“2” :controls=“false” v-model=“form.balance” auto-complete=“off”> </el-input-number> </el-form-item> </el-form> </div> <span slot=“footer” class=“dialog-footer”> <el-button type=“primary” size=‘mini’ @click=“sureBtn”>确定</el-button> <el-button type=“primary” size=‘mini’ @click=“close”>取消</el-button> </span></el-dialog>参考:https://www.jianshu.com/p/c3c… ...

December 24, 2018 · 2 min · jiezi

优雅的elementUI table 单元格可编辑实现方法

最近在做可编辑特定列的单元格的elementUI table,看了N多的开源、文章,找到一个很优雅的实现方式,分享给大家。PS:单元格可编辑的table,用英文搜索:Inline editable table with ElementUI 会得到高质量结果。先上效果:APP.vue:<template> <div id=“app”> <div style=“margin-bottom: 30px”> <el-switch style=“display: block” v-model=“editModeEnabled” active-color="#13ce66" inactive-color="#ff4949" active-text=“Edit enabled” inactive-text=“Edit disabled”> </el-switch> </div> <el-table :data=“gridData” style=“width: 100%"> <el-table-column label=“Name” min-width=“180”> <editable-cell slot-scope="{row}” :can-edit=“editModeEnabled” v-model=“row.name”> <span slot=“content”>{{row.name}}</span> </editable-cell> </el-table-column> <el-table-column min-wwidth=“150” label=“Gender”> <editable-cell slot-scope="{row}" editable-component=“el-select” :can-edit=“editModeEnabled” close-event=“change” v-model=“row.gender”> <el-tag size=“medium” :type=“row.gender === ‘M’ ? ‘primary’ : ‘danger’” slot=“content”> {{row.gender === ‘M’ ? ‘Male’: ‘Female’}} </el-tag> <template slot=“edit-component-slot”> <el-option value=“M” label=“Male”></el-option> <el-option value=“F” label=“Female”></el-option> </template> </editable-cell> </el-table-column> <el-table-column label=“Birth Date” min-width=“250”> <editable-cell slot-scope="{row}" :can-edit=“editModeEnabled” editable-component=“el-date-picker” format=“yyyy-MM-dd” value-format=“yyyy-MM-dd” v-model=“row.date”> <span slot=“content”>{{row.date}}</span> </editable-cell> </el-table-column> </el-table> </div></template><script>import EditableCell from “./components/EditableCell.vue”;export default { name: “App”, components: { EditableCell }, data() { return { editModeEnabled: false, gridData: [ { date: “2016-05-03”, name: “Tom”, gender: “M” }, { date: “2016-05-02”, name: “Lisa”, gender: “F” }, { date: “2016-05-04”, name: “Jon”, gender: “M” }, { date: “2016-05-01”, name: “Mary”, gender: “F” } ] }; }};</script><style>.edit-cell { min-height: 35px; cursor: pointer;}</style>EditeableCell.vue:<template> <div @click=“onFieldClick” class=“edit-cell”> <el-tooltip v-if="!editMode && !showInput" :placement=“toolTipPlacement” :open-delay=“toolTipDelay” :content=“toolTipContent”> <div tabindex=“0” class=“cell-content” :class="{’edit-enabled-cell’: canEdit}" @keyup.enter=“onFieldClick”> <slot name=“content”></slot> </div> </el-tooltip> <component :is=“editableComponent” v-if=“editMode || showInput” ref=“input” @focus=“onFieldClick” @keyup.enter.native=“onInputExit” v-on=“listeners” v-bind="$attrs" v-model=“model”> <slot name=“edit-component-slot”></slot> </component> </div></template><script>export default { name: “editable-cell”, inheritAttrs: false, props: { value: { type: String, default: "" }, toolTipContent: { type: String, default: “Click to edit” }, toolTipDelay: { type: Number, default: 500 }, toolTipPlacement: { type: String, default: “top-start” }, showInput: { type: Boolean, default: false }, editableComponent: { type: String, default: “el-input” }, closeEvent: { type: String, default: “blur” }, canEdit: { type: Boolean, default: false } }, data() { return { editMode: false }; }, computed: { model: { get() { return this.value; }, set(val) { this.$emit(“input”, val); } }, listeners() { return { [this.closeEvent]: this.onInputExit, …this.$listeners }; } }, methods: { onFieldClick() { if (this.canEdit) { this.editMode = true; this.$nextTick(() => { let inputRef = this.$refs.input; if (inputRef && inputRef.focus) { inputRef.focus(); } }); } }, onInputExit() { this.editMode = false; }, onInputChange(val) { this.$emit(“input”, val); } }};</script><style>.cell-content { min-height: 40px; padding-left: 5px; padding-top: 5px; border: 1px solid transparent;}.edit-enabled-cell { border: 1px dashed #409eff;}</style>在线查看(可能需要科学上网):https://codesandbox.io/s/2pv1…github:https://github.com/heianxing/…另外一个单元格编辑的例子:App.vue:<template> <div id=“app”> <el-tooltip content=“Click on any of the cells or on the edit button to edit content”> <i class=“el-icon-info”></i> </el-tooltip> <el-table :data=“gridData” style=“width: 100%"> <el-table-column label=“Operations” min-width=“180”> <template slot-scope="{row, index}"> <el-button icon=“el-icon-edit” @click=“setEditMode(row, index)"> </el-button> <el-button type=“success” icon=“el-icon-check” @click=“saveRow(row, index)"> </el-button> </template> </el-table-column> <el-table-column label=“Name” min-width=“180”> <editable-cell :show-input=“row.editMode” slot-scope="{row}” v-model=“row.name”> <span slot=“content”>{{row.name}}</span> </editable-cell> </el-table-column> <el-table-column min-wwidth=“150” label=“Gender”> <editable-cell :show-input=“row.editMode” slot-scope="{row}” editable-component=“el-select” close-event=“change” v-model=“row.gender”> <el-tag size=“medium” :type=“row.gender === ‘M’ ? ‘primary’ : ‘danger’” slot=“content”> {{row.gender === ‘M’ ? ‘Male’: ‘Female’}} </el-tag> <template slot=“edit-component-slot”> <el-option value=“M” label=“Male”></el-option> <el-option value=“F” label=“Female”></el-option> </template> </editable-cell> </el-table-column> <el-table-column label=“Birth Date” min-width=“250”> <editable-cell :show-input=“row.editMode” slot-scope="{row}” editable-component=“el-date-picker” format=“yyyy-MM-dd” value-format=“yyyy-MM-dd” v-model=“row.date”> <span slot=“content”>{{row.date}}</span> </editable-cell> </el-table-column> </el-table> </div></template><script>import EditableCell from “./components/EditableCell.vue”;export default { name: “App”, components: { EditableCell }, data() { return { gridData: [ { date: “2016-05-03”, name: “Tom”, gender: “M” }, { date: “2016-05-02”, name: “Lisa”, gender: “F” }, { date: “2016-05-04”, name: “Jon”, gender: “M” }, { date: “2016-05-01”, name: “Mary”, gender: “F” } ] }; }, methods: { setEditMode(row, index) { row.editMode = true; }, saveRow(row, index) { row.editMode = false; } }, mounted() { this.gridData = this.gridData.map(row => { return { …row, editMode: false }; }); }};</script><style>.edit-cell { min-height: 35px; cursor: pointer;}</style>EditeableCell.vue:<template> <div @click=“onFieldClick” class=“edit-cell”> <el-tooltip v-if="!editMode && !showInput" :placement=“toolTipPlacement” :open-delay=“toolTipDelay” :content=“toolTipContent”> <div tabindex=“0” @keyup.enter=“onFieldClick”> <slot name=“content”></slot> </div> </el-tooltip> <component :is=“editableComponent” v-if=“editMode || showInput” ref=“input” @focus=“onFieldClick” @keyup.enter.native=“onInputExit” v-on=“listeners” v-bind="$attrs" v-model=“model”> <slot name=“edit-component-slot”></slot> </component> </div></template><script>export default { name: “editable-cell”, inheritAttrs: false, props: { value: { type: String, default: "" }, toolTipContent: { type: String, default: “Click to edit” }, toolTipDelay: { type: Number, default: 500 }, toolTipPlacement: { type: String, default: “top-start” }, showInput: { type: Boolean, default: false }, editableComponent: { type: String, default: “el-input” }, closeEvent: { type: String, default: “blur” } }, data() { return { editMode: false }; }, computed: { model: { get() { return this.value; }, set(val) { this.$emit(“input”, val); } }, listeners() { return { [this.closeEvent]: this.onInputExit, …this.$listeners }; } }, methods: { onFieldClick() { this.editMode = true; this.$nextTick(() => { let inputRef = this.$refs.input; if (inputRef) { inputRef.focus(); } }); }, onInputExit() { this.editMode = false; }, onInputChange(val) { this.$emit(“input”, val); } }};</script><style></style>在线查看(可能需要科学上网):https://codesandbox.io/s/mrqq…github:https://github.com/heianxing/…英文来源:https://www.reddit.com/r/vuej… ...

December 22, 2018 · 4 min · jiezi

el-tree 节点动态查找

1.效果图通过在input框里输入值,动态查询树节点。将父节点展开,找到的节点显示在最当前窗口。2.代码2.1 html<template> <div style=“height: 100%; “> <div style=“height: 45px;padding-top:8px;padding-bottom:8px;"> <el-label>快速查询 :</el-label> <el-input style=” width: calc(100% - 210px);height: 28px !important;line-height: 28px !important;” placeholder=“请查找输入内容” v-model=“searchInput” @keyup.native=“search”>//按键结束就触发,下面的查找按钮其实可以删除 </el-input> <el-button size=“mini” type=“primary” @click.native=“search”>查找</el-button> <el-button size=“mini” type=”" @click.native=“next” style=“margin-top: 0px”>下一个</el-button> </div> <div id=“searchtree-eletree” //id便于定位 style=“height: calc(100% - 46px); overflow: auto;” class=“border-grey”> <el-tree :data=“searchTreeDT” node-key=“id” ref=“tree” :highlight-current=“true” expand-on-click-node :props=“defaultProps” :default-expanded-keys=“defaultEexpandedkeys” // 展开节点 > </el-tree> </div> </div></template>2.2 js<script> export default { data() { return { //树 searchTreeData:[], defaultEexpandedkeys: [0], //默认展开一级树节点 defaultProps: { children: ‘children’, label: ’name’ }, //查询栏 searchInput: ‘’, searchIndex: null, searchData: [], }; }, watch: { // 搜索框中内容变化,重置当前搜索结果的索引值 searchInput: function () { this.searchIndex = null }, }, methods: { //查询 search() { this.searchIndex = null; if (this.searchInput) { let searchInput = this.searchInput; this.searchData = this.searchTreeData.filter( function(item) { return item.name.indexOf(searchInput) > -1 //返回符合查询条件的节点 }); if (this.searchData.length) {//存在符合查询条件的节点 this.searchIndex = 0; //展开符合条件节点的父节点(使查询节点显示出来) this.defaultEexpandedkeys = getParentsId(this.searchData[0], this.searchTreeData, ‘id’,‘pId’, 0); this.$nextTick(() => {//显示完成后执行 this.$refs.tree.setCurrentKey(this.searchData[0].id);//高亮查询到的第一个节点 setTimeout(() => { //根据树id 找到高亮的节 let node = document.querySelector(’#searchtree-eletree .is-current’);点 if (node) { setTimeout(() => { // node.scrollIntoView(); //有bug,可尝试 let top = $(node).position().top; //关键代码,将选中节点显示在当前窗口可视区域 $("#searchtree-eletree").scrollTop(top); }, 500); } }, 0); }); } else { //Message需要引入 import {Message} from ’element-ui’ Message.info(‘未找到任何匹配数据!’); } } else { Message.info(‘请输入要查找的内容!’); } }, next(){ if (this.searchIndex !== null) { this.searchIndex += 1; this.searchIndex = this.searchIndex < this.searchData.length ? this.searchIndex : 0; this.defaultEexpandedkeys = getParentsId(this.searchData[this.searchIndex], this.searchTreeData, ‘id’,‘pId’, 0); this.$nextTick(() => { this.$refs.tree.setCurrentKey(this.searchData[this.searchIndex].id); setTimeout(() => { let node = document.querySelector(’#searchtree-eletree .is-current’); if (node) { setTimeout(() => { // node.scrollIntoView(); let top = $(node).position().top; $("#searchtree-eletree").scrollTop(top); }, 500); } }, 0); }); } else { if (this.searchInput) { this.search(); } else { Message.info(‘请输入要查找的内容!’); } } }, } }</script>2.3 在2.2 用到的获取父节点id的方法/** * 找到当前节点的所有祖先节点的idKey * @param {Object} node 当前节点信息 * @param {Array} data 所有节点的数据信息 * @returns {Array} parentsId 祖先节点的idKey */export function getParentsId(node, data = [], idKey=‘id’, pIdKey=‘pid’, rootId = ‘0’, parentsId = [] ){ if(!node) return [rootId, …parentsId]; if(node[pIdKey] == rootId) return [node[pIdKey], …parentsId]; let pNode = data.filter(item => item[idKey] == node[pIdKey]); if(!pNode.length) return parentsId; parentsId.push(pNode[0][idKey]); return getParentsId(pNode[0], data, idKey, pIdKey, rootId, parentsId); }; ...

December 21, 2018 · 2 min · jiezi

el-table 简单编辑功能

效果图获得的表格数据展示第一步:表格数据处理。将每行数据都添加属性editAble,每个0与当前行每一列对应;通过修改对应的editAble[i]的值控制编辑可能,0不可编辑,1可编辑 data.listRemain.forEach( (row,index) => { //editAble 数组长度=表格列数 //可new一个数组,使用editAble.fill(0)填充0, row.editAble = [0,0,0,0,0,0,0,0,0]; });第二步:el-table 列的scope处理。这里是金额列的编辑,所以使用了el-input-number ,可根据需要换成el-input。 <el-table :data=“listRemain” highlight-current-row> <el-table-column label=“年初余额” show-overflow-tooltip> <template slot-scope=“scope”> <el-input-number clearable :precision=“2” //小数位精度 :controls=“false” v-model=“scope.row.balance” :key=“scope.row.chr_id” //根据editAble ,判断是否显示编辑框,edit[0]=1时显示,0是列的index,从0开始 v-if="(scope.row.editAble || [])[0]" v-focus//获取焦点,若不生效可改为v-el-focus,定义方法见文章最后“其他” @focus="$event.target.select()" @blur=“getValue(scope.row, 0,‘balance’)"> </el-input-number> <div class=”" v-else //editAble[0]=0时编辑框隐藏 //双击单元格,将单元格对应列的editAble[0]的值改为1,则编辑框el-input-number 即可显示 @dblclick=“cellClick(scope.row, 0, ‘balance’)"> //formatMoney是金额格式化方法 {{ scope.row.balance | formatMoney(scope.row.balance,0) }} </div> </template> </el-table-column> </el-table>第三步:相关js方法<script> export default { data(){ return{ listRemain:[], } }, directives: { focus: {// v-focus指令的定义 inserted: function (el) { $(el).find(‘input’).focus() } } }, methods:{ //编辑事件(双击触发) cellClick(row, index,prop){ this.inputAbstract(row.editAble,index) }, //将对应列的editAble[index]值改为1 inputAbstract(editAble, index) { editAble.splice(index, 1, 1) }, // 隐藏编辑框 getValue(row, index,prop) { var vm = this; setTimeout(vm => { //将对应列的editAble[index]值改为0 row.editAble.splice(index, 1, 0); }, 150) }, }}</script>其他种类编辑示例:动态列input带按钮,可进行点击按钮跳出选择模态框树等操作 <el-table-column v-for="(col,index) in detailTableHead” :key=‘col.prop’ :prop=“col.prop” :label=“col.label” > <template slot-scope=“scope”> <!– 基本要素 –> <el-input v-focus :trigger-on-focus=false v-if="(scope.row.editAble || [])[index] " v-model=“scope.row[col.prop]” @blur=“getValue(scope.row, index, col.prop)"> <el-button slot=“append” icon=“el-icon-more” @click=“getEle(col)"></el-button> </el-input> <!– 不可编辑 要素 –> <div class=“text-show” v-else v-text=“scope.row[col.prop]” @dblclick=“cellClick(scope.row, index, col.prop)"> </div> </template> </el-table-column>其他:import Vue from ‘vue’// 注册一个全局自定义指令 v-el-focus, 用于element-ui input聚焦,可写在main.js里Vue.directive(’el-focus’, { inserted: function(el) { // 聚焦元素 Vue.nextTick(function() { el.getElementsByTagName(‘input’)[0].focus() }) } ...

December 21, 2018 · 1 min · jiezi

el-input 树型下拉框

1.效果图1.1 input聚焦时显示下拉框,再次点击下拉框或点击其他处下拉框消失,主要靠z-index添加遮罩实现1.2 实时过滤效果2.代码 ( vue.js + element-ui ) 2.1 html <el-form :model=“form” size=“mini” > <el-row> <el-col :span=‘12’> <el-form-item label=“会计主管” > <el-input placeholder=“请选择会计主管” class=“width-220 selectTree-input” v-model=“form.MANAGER_NAME” icon=“caret-bottom” auto-complete=“off” @focus=“focus($event)” @click.native=“changeSelectTree()"> </el-input> <div v-show=“isShowSelect” style=“position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 102;” @click=“cancelManager”> </div> <el-tree v-show=“isShowSelect” empty-text=“暂无数据” :highlight-current = true :default-expand-all = false :expand-on-click-node=“false” :filter-node-method=“filterNode” :data=“userlist” node-key=“chr_id” :props=“defaultProps” @node-click=“selectManage” class=“objectTree” ref=“selectTree”> </el-tree> </el-form-item> </el-col> <el-col :span=‘12’> </el-col> </el-row> </el-form>2.2 JSimport ‘babel-polyfill’//兼容语法 async focusexport default { data(){ return { form: { MANAGER_NAME: ‘’, MANAGER_ID: ‘’, }, isShowSelect: false,// 是否显示会计主管的树状选择器 userlist: [],// 会计主管的选项数据 defaultProps: { // 会计主管 树状选择器 的选项的配置参数 children: ‘children’, label: ‘code_name’, }, } }, watch: { form: {//form.MANAGER_NAME变化时过滤节点 handler(form){ if(this.isShowSelect){ this.$refs.selectTree.filter(form.MANAGER_NAME); } }, deep: true,//深度监听,重要 }, }, methods:{ //下拉框的显示与隐藏 changeSelectTree(){ this.isShowSelect = !this.isShowSelect; }, //input获取焦点事件,初始化树 async focus(e) { let vm = this; vm.$refs.selectTree.filter(”"); vm.$refs.selectTree.setCurrentNode([]); }, // 选择器的树节点 filterNode(value, data) { if (!value) return true; if(!data.code_name){ data.code_name = data.chr_code + " " + data.chr_name } return data.code_name.indexOf(value) !== -1; }, //选择会计主管 selectManage(data, Node) { this.form.MANAGER_NAME = data.code_name;//input赋值 this.form.MANAGER_ID = data.chr_id; this.isShowSelect = false;// 关闭选择器 }, //点击遮罩层,取消选择会计主管 cancelManager(){ this.isShowSelect = false }, }}2.3 css<style lang=“scss”> /下拉框选择树/ .objectTree { position: absolute; overflow: auto; z-index: 100; width: 110%; height: 200px; border: 1px solid #ddd; line-height: normal; z-index: 204; } .selectTree-input { input:focus { z-index: 204;//103 } } .width-220{ width: 220px }</style>2.4 参考数据//示例下拉框数据this.userList = [{ chr_code: “001001051”, chr_id: “9853”, chr_name: “张海舒”, is_leaf: “1”, user_type: “0”,}] ...

December 21, 2018 · 2 min · jiezi

vue 撸后台笔记一

前言本文是以 花裤衩 大佬的 vue-element-admin 项目为模板、结合公司需求开发的后台管理系统的学习笔记。原项目地址:vue-element-admin参考文章:手摸手用 vue 撸后台系列安装与配置新建 vue-cli 项目,相关安装及配置不多做介绍,有需要可自行搜索。接着是安装项目依赖。基本依赖库:Vue-Router Vue.js 官方的路由管理器Axios 基于promise 的 HTTP 库Element-UI 一套为开发者、设计师和产品经理准备的基于 Vue2.0 的桌面端组件库Vuex 一个专为 Vue.js 应用程序开发的状态管理模式扩展依赖库:node-sass css 扩展语言normalize.css 为默认的 HTML 元素样式上提供跨浏览器的高度一致性js-cookie 一款轻量级的 js 操作 cookie 的插件i18n Vue.js 的国际化插件,它可以轻松地将一些本地化特性集成到 Vue 中driver.js 一款轻量级、无需依赖但功能强大的原生 JavaScript,兼容所有主流浏览器,可帮助你将用户的注意力集中在页面上NProgress 细长的全站进度条SVG sprite loader 用于根据导入的 svg 文件自动生成 symbol 标签并插入 htmlSortable 一款轻量级的拖放排序列表的 js 插件ECharts 一款功能强大的图表和可视化库screenfull 一款全屏插件项目结构├── build // 构建相关├── config // 配置相关├── disk // 打包文件├── node_modules // 依赖项├── src // 源代码│ ├── api // 所有请求│ ├── assets // 主题 字体等静态资源│ ├── components // 全局公用组件│ ├── directive // 全局指令│ ├── waves // 水波纹指令│ ├── icons // 项目所有 svg icons│ ├── lang // 国际化 language│ ├── mock // 项目mock 模拟数据│ ├── roter // 路由│ ├── store // 全局 store管理│ ├── styles // 全局样式│ ├── utils // 全局公用方法│ ├── views // views 所有页面│ ├── account // 账户管理│ ├── court // 法院管理│ ├── dashboard // 功能主页│ ├── device // 设备管理│ ├── errorPage // 错误页面│ ├── layout // 整体布局│ ├── login // 登录页面│ ├── redirect // 重定向页面│ ├── statistics // 数据统计页面│ ├── versions // 版本管理页面│ ├── writs // 文书管理页面│ ├── App.vue // 入口页面│ ├── errorLog.js // 错误日志│ ├── main.js // 入口文件 加载组件 初始化等│ ├── permission.js // 权限管理├── static // 第三方不打包资源├── .babelrc // babel-loader 配置├── .eslintrc.js // eslint 配置项├── .gitignore // git 忽略项├── favicon.ico // favicon 图标├── index.html // html 模板├── package.json // 依赖项目录├── README.MD // 说明文档简单讲下 src 文件夹api 与 views根据项目的业务划分 views 页面展示部分,并将 api 接口请求与 views 一一对应,有利于迭代更新与后期维护。components将全局公用的模块与组件存放在 components 文件夹中,页面级的的组件建议还是放在各自的 views 文件夹下。store在 index 入口文件引入 modules 对象,独立封装各个模块状态。axios在 axios 配置档设置基础 URL,根据环境变量动态切换 api,需要在 config/dev.env.js 文件中配置接口路径。lang将中英文语言包各自封装并在入口 index.js 配置导入在 main.js 使用 i18n。 ...

December 21, 2018 · 2 min · jiezi

记录element ui table表格spanMethod动态合并列

先放几个表格的图看看表格一:预览:https://jsfiddle.net/xmwh/2vh…表格二:预览:https://jsfiddle.net/xmwh/s4g…表格三:预览:https://jsfiddle.net/xmwh/vxL…语言有点不太好描述,大家还是看图吧,看图也能大概知道需要合并哪些,这些数据中指标、原材料名称格式都是不一定的,所以合并起来有点困难,不同的列合并的个数也还不一样,之前也提问过,不过没人回答,只好自己google搜索了,找到了该篇文章https://blog.csdn.net/qq_2946…,本文内容实现方法都是参照这个来的。经过改造后可以多列不同行进行动态合并,核心代码如下:getSpanArr(tableData, keyName) { keyName.forEach((kitem, k) =>{ tableData.forEach((data, i) =>{ if (i === 0) { this.mergeData[kitem] = this.mergeData[kitem] || [] this.mergeData[kitem].push(1) this.mergePos[kitem] = 0 } else { // 判断当前元素与上一个元素是否相同 if (data[kitem] === tableData[i - 1][kitem]) { this.mergeData[kitem][this.mergePos[kitem]] += 1 this.mergeData[kitem].push(0) } else { this.mergeData[kitem].push(1) this.mergePos[kitem] = i } } }) }) console.log(this.mergeData) console.log(this.mergePos)}然后利用element table 提供的spanMethod方法进行表格合并操作spanMethod({ row, column, rowIndex, columnIndex }){ if (this.mergeProp.includes(column.property)) { const _row = this.mergeData[column.property + ‘Id’][rowIndex] const _col = _row > 0 ? 1 : 0 return { rowspan: _row, colspan: _col } }}具体使用,请参考上面的预览地址! ...

December 19, 2018 · 1 min · jiezi

element table 合计使用后台传回来的值进行展示

使用element制作表格,要求展示合计,但是合计中的数据比较特殊许多都是计算的,所以数据由后台传回来1.将show-summary设置为true2. 自定义合计方法 :summary-method=“getSummaries"getSummaries(param) { let vm = this; let sums = []; if (this.showSummary) { var selectedColm = param.columns; let newArray=[] selectedColm.forEach(a => { if(vm.total[a.property]){ newArray.push(vm.total[a.property]) }else{newArray.push(’’)} }); sums=newArray; sums[0] = “合计”; return sums; } }vm.total是后台返回来的合计,a.property是表格对应的项

December 18, 2018 · 1 min · jiezi

[Vue warn]: <transition-group> children must be keyed: <ElTag>

如果你也像我一样,在select上面需要绑定整个返回的数据值,但是展示只显示一些name,并且可以多选。那么你可能会遇到我这个一样的问题。el-select绑定值为对象时,报错[Vue warn]: <transition-group> children must be keyed: <ElTag>解决方法:<el-select v-model=“leftContent” multiple value-key=“id” placeholder=“请选择”><el-optionv-for=“item in slaveList”:key=“item.id”:label=“item.name”:value=“item”></el-option></el-select>el-select必须加入value-key属性,且值为item中的一个唯一属性,例如id

December 17, 2018 · 1 min · jiezi

Vue2.0 + ElementUI 手写权限管理系统后台模板(三)——页面搭建

框架布局本章只介绍基础布局,和一些主要的js,页面上基本上都是些交互事件,项目代码上都有注释,不懂的地方debug跑一变就知道了,只是这些事件基本上没有独立存在的,相互之间都有关联框架风格新建页面:/src/views/layout/layout.vue<!– layout.vue –><template> <div id=“loyout”> <el-container> <layoutAside></layoutAside> <el-container> <layoutHeader></layoutHeader> <el-main id=“elmain”> <transition name=“main” mode=“out-in”> <router-view></router-view> </transition> </el-main> <el-footer> <Bottom></Bottom> </el-footer> </el-container> </el-container> </div></template>aside 无限级菜单组件新建页面:/src/views/layout/aside/aside.vue<!– aside.vue –><template> <div> <el-aside id=“asideNav”> <div class=“logo-name”> <p v-if="$store.getters.logoShow">XU</p> <p v-else>vue-xuAdmin后台模板</p> </div> <!– el-menu的属性查看官方文档 –> <el-menu :default-active="$route.path" class=“el-menu-vertical” @select=“selectmenu” :collapse="$store.getters.isCollapse" background-color="#03152A" text-color=“rgba(255,255,255,.7)” active-text-color="#ffffff" :router="$store.getters.uniquerouter" :unique-opened="$store.getters.uniquerouter" :collapse-transition=“true” > <!– 遍历根据权限生成的路由表生成菜单列表 –> <template v-for="(item,index) in $store.getters.routers" v-if="!item.hidden"> <!– 检查是否带有alone属性的一级菜单类似“主页”,还有子菜单的个数 –> <el-submenu v-if="!item.alone && item.children.length>0" :index=“index+’’"> <template slot=“title”> <!– 如果没有设置图标将会采用默认图标‘fa fa-server’ –> <i :class=“item.iconCls?item.iconCls:[fa,fa-server]"></i> <span slot=“title”>{{ $t(routeNmae.${item.name}) }}</span> </template> <!– 子菜单组件 –> <menu-tree :menuData=“item.children”></menu-tree> </el-submenu> <!– 一级菜单 –> <el-menu-item :index=“item.path” v-else> <i :class=“item.iconCls?item.iconCls:[fa,fa-file]"></i> <span slot=“title”>{{ $t(routeNmae.${item.name}) }}</span> </el-menu-item> </template> </el-menu> </el-aside> </div></template>点击菜单// aside.vuewatch: { // 监听浏览器直接输入路由,将此路由添加到tabnavBox ‘$route.path’: function (val) { this.selectmenu(val) } }, // 点击菜单把当前菜单的name和path添加到tabNavBox容器,生成tabNav标签页菜单selectmenu (key) { // 获取当前权限路由表 let router = this.$store.getters.routers let name = ’’ // 查找路由的name属性 let navTitle = function (path, routerARR) { for (let i = 0; i < routerARR.length; i++) { if (routerARR[i].children.length > 0 || routerARR[i].path === path) { if (routerARR[i].path === path && routerARR[i].children.length < 1) { name = routerARR[i].name break } // 递归查找 navTitle(path, routerARR[i].children) } } return name } // tabNavBox添加数据 this.$store.dispatch(‘addTab’, { title: navTitle(key, router), path: key }) }子菜单组件 menu-true新建页面:/src/views/layout/aside/menuTree.vue<!– menuTree.vue –><template> <div> <template v-for="(child,index) in menuData”> <el-submenu v-if=“child.children.length > 0” :index=“child.path”> <template slot=“title”> <i :class=“child.iconCls?child.iconCls:[fa,fa-file]"></i> <span slot=“title”>{{ $t(routeNmae.${child.name}) }}</span> </template> <!– 通过递归 menu-tree 生成无限级菜单 –> <menu-tree :menuData=“child.children”></menu-tree> </el-submenu> <el-menu-item v-else :index=“child.path”> <i :class=“child.iconCls?child.iconCls:[fa,fa-file]"></i> <span slot=“title”>{{ $t(routeNmae.${child.name}) }}</span> </el-menu-item> </template> </div></template>header头部这里没啥好说的,都是html布局,tabnav接下来说, i18n后面会讲新建页面:/src/views/layout/header/header.vue<!– header.vue –><template> <div> <el-header id=“header”> <span class=“hideAside” @click=“collapse”><i class=“fa fa-indent fa-lg”></i></span> <ul class=“personal”> <li class=“fullScreen” @click=“fullScreen”> <el-tooltip class=“item” effect=“dark” content=“全屏” placement=“bottom”><i class=“fa fa-arrows-alt fa-lg”></i></el-tooltip> </li> <li> <langSelect></langSelect> </li> <li>{{ $t(role.${this.$store.getters.info.role}) }}</li> <li> <el-dropdown @command=“handleCommand”> <span class=“el-dropdown-link”> 夏洛克丶旭<i class=“el-icon-arrow-down el-icon–right”></i> </span> <el-dropdown-menu slot=“dropdown”> <el-dropdown-item command=“a”>{{ $t(‘userDropdownMenu.basicInfor’) }}</el-dropdown-item> <el-dropdown-item command=“b”>{{ $t(‘userDropdownMenu.changePassword’) }}</el-dropdown-item> <el-dropdown-item command=“logout” divided>{{ $t(‘userDropdownMenu.logout’) }}</el-dropdown-item> </el-dropdown-menu> </el-dropdown> </li> <li class=“icon”><img :src=“avatar”/></li> </ul> </el-header> <!– tabNav 组件,标签页菜单 –> <tabNav></tabNav> </div></template>tabNav 组件这里的tabNav标签动画和页面的动画是一样的,都是官方的demo稍微改一下,,只不过页面有mode=“out-in"所以动画时间需要快一点新建页面:/src/views/layout/header/tabNav.vue<!– tabNav.vue –><template> <div> <div class=“tabnavBox”> <transition-group name=“list” tag=“ul”> <!– tabnavBox 是存储所有tabNav的数据容器,每次点击左侧菜单就会把数据添加到tabnavBox –> <li v-for="(item, index) in $store.getters.tabnavBox” @contextmenu.prevent=“openMenu(item,$event,index)” :key=“item.title” class=“tabnav” :class=”{ active: $route.path === item.path }"> <router-link :to=“item.path”>{{ $t(routeNmae.${item.title}) }}</router-link> <i @click=“removeTab(item)” class=“el-icon-error” v-if=“index !== 0”></i> </li> </transition-group> </div> <!– 右击菜单 –> <ul v-show=“this.rightMenuShow” :style="{left:this.left+‘px’,top:this.top+‘px’}” class=“menuBox”> <li @click=“removeTab($store.getters.rightNav)"><i class=“fa fa-remove”></i>{{ $t(‘rightMenu.close’) }}</li> <li @click=“removeOtherTab($store.getters.rightNav)">{{ $t(‘rightMenu.closeOther’) }}</li> <li @click=“removeAllTab”>{{ $t(‘rightMenu.closeAll’) }}</li> </ul> </div></template> ...

December 17, 2018 · 2 min · jiezi

Vue2.0 + ElementUI 手写权限管理系统后台模板(二)——权限管理

权限验证页面级别权限路由:默认挂载不需要权限的路由,例如:登录、主页。需要权限的页面通过 router.addRoutes(点击查看官方文档) 动态添加更多的路由规则,404拦截页面需要放在路由表的最后,否则 /404 后面的路由会被404拦截,通过路由元信息meta(点击查看官方文档)记录路由需要的权限。为了菜单列表可以被翻译,路由表的 name 属性值通过 i18n 的英文对照表来获取,也可以直接写英文名称,如 name: routeNmae.builtInIcon 可以直接写成 name: “builtInIcon”,凭个人喜好// src/router/index.jsimport en from ‘../i18n/lang/en’ // 路由名字 name import Vue from ‘vue’import Router from ‘vue-router’import CommerViews from ‘@/views/commerViews’import Login from ‘@/views/login/index’import Layout from ‘@/views/layout/layout’import HomeMain from ‘@/views/index/mainIndex’// 不是必须加载的组件使用懒加载const Icon = () => import(’@/views/icon/index’)const Upload = () => import(’@/views/upload/upload’)const Markdown = () => import(’@/views/markdown/markdownView’)const NotFound = () => import(’@/page404’)Vue.use(Router)let routeNmae = en.routeNmae// 不需要权限的路由let defaultRouter = [ { path: ‘/’, redirect: ‘/index’, hidden: true, children: [] }, { path: ‘/login’, component: Login, name: ‘’, hidden: true, children: [] }, { path: ‘/index’, iconCls: ‘fa fa-dashboard’, // 菜单图标,直接填写字体图标的 class name: routeNmae.home, component: Layout, alone: true, children: [ { path: ‘/index’, iconCls: ‘fa fa-dashboard’, name: ‘主页’, component: HomeMain, children: [] } ] }, { path: ‘/404’, component: NotFound, name: ‘404’, hidden: true, children: [] },]// 需要 addRouters 动态加载的路由 let addRouter = [ { path: ‘/’, iconCls: ‘fa fa-server’, name: routeNmae.multiDirectory, component: Layout, children: [ { path: ‘/erji1’, iconCls: ‘fa fa-server’, name: routeNmae[‘menu2-1’], component: Erji, children: [] }, { path: ‘/erji3’, iconCls: ‘fa fa-server’, name: routeNmae[‘menu2-3’], component: CommerViews, // 无限极菜单的容器 超过三级菜单父级容器需要使用 CommerViews children: [ { path: ‘/sanji2’, iconCls: ‘fa fa-server’, name: routeNmae[‘menu3-2’], component: Sanji2, children: [] }, { path: ‘/sanji3’, iconCls: ‘fa fa-server’, name: routeNmae[‘menu3-3’], component: CommerViews, children: [ { path: ‘/siji’, iconCls: ‘fa fa-server’, name: routeNmae[‘menu4-1’], component: Siji, children: [] }, { path: ‘/siji1’, iconCls: ‘fa fa-server’, name: routeNmae[‘menu4-2’], component: CommerViews, children: [ { path: ‘/wuji’, iconCls: ‘fa fa-server’, name: routeNmae[‘menu5-1’], component: Wuji, children: [] } ] } ] } ] } ] }, { path: ‘/’, iconCls: ’el-icon-edit’, // 图标样式class name: routeNmae.editor, component: Layout, meta: {role: [‘superAdmin’, ‘admin’]}, // 需要权限 ‘superAdmin’, ‘admin’。meta属性可以放在父级,验证父级和所有子菜单,也可以放在子级单独验证某一个子菜单 children: [ { path: ‘/markdown’, iconCls: ‘fa fa-file-code-o’, // 图标样式class name: routeNmae.markdown, component: Markdown, children: [] } ] }, { path: ‘*’, redirect: ‘/404’, hidden: true, children: [] },]export default new Router({ routes: defaultRouter})export {defaultRouter, addRouter}然后通过 token 获取当前登录用户的个人信息,在router被挂载到Vue之前和需要权限的路由表做对比,筛选出当前角色的动态路由表,// main.js// 获取角色信息,根据用户权限动态加载路由router.beforeEach((to, from, next) => { if (store.getters.token) { // 查看 token 是否存在 store.dispatch(‘setToken’, store.getters.token) // 每次操作都重新写入 token,延长有效会话时间 if (to.path === ‘/login’) { next({path: ‘/’}) } else { if (!store.getters.info.role) { // 查看是否有当前用户角色,如果没有则获取角色信息 !async function getAddRouters () { await store.dispatch(‘getInfo’, store.getters.token) // 通过token获取角色信息 await store.dispatch(’newRoutes’, store.getters.info.role) // 通过权限筛选新路由表 await router.addRoutes(store.getters.addRouters) // 动态加载新路由表 next({path: ‘/index’}) }() } else { let is404 = to.matched.some(record => { // 404页面拦截 if(record.meta.role){ // 没有权限的页面,跳转的404页面 return record.meta.role.indexOf(store.getters.info.role) === -1 } }) if(is404){ next({path: ‘/404’}) return false } next() } } } else { if (to.path === ‘/login’) { next() } next({path: ‘/login’}) }})actions: getInfo// src/vuex/modules/role.jsstate: { info: ’’ // 每次刷新都要通过token请求个人信息来筛选动态路由 }, mutations: { getInfo (state, token) { // 省略 axios 请求代码 通过 token 向后台请求用户权限等信息,这里用假数据赋值 state.info = { role: ‘superAdmin’, permissions: ‘超级管理员’ } // 将 info 存储在 sessionStorage里, 按钮指令权限将会用到 sessionStorage.setItem(‘info’, JSON.stringify(store.getters.info)) }, setRole (state, options) { // 切换角色,测试权限管理 state.info = { role: options.role, permissions: options.permissions } sessionStorage.setItem(‘info’, JSON.stringify(store.getters.info)); // 权限切换后要根据新权限重新获取新路由,再走一遍流程 store.dispatch(’newRoutes’, options.role) router.addRoutes(store.getters.addRouters) } }, actions: { getInfo ({commit}, token) { commit(‘getInfo’, token) }, setRole ({commit}, options){// 切换角色,测试权限管理,不需要可以删除 commit(‘setRole’, options) } }actions: newRoutes// src/vuex/modules/routerData.jsimport {defaultRouter, addRouter} from ‘@/router/index’const routerData = {state: { routers: [], addRouters: [] }, mutations: { setRouters: (state, routers) => { state.addRouters = routers // 保存动态路由用来addRouter state.routers = defaultRouter.concat(routers) // 所有有权限的路由表,用来生成菜单列表 } }, actions: { newRoutes ({commit}, role) { // 通过递归路由表,删除掉没有权限的路由 function eachSelect (routers, userRole) { for (let j = 0; j < routers.length; j++) { if (routers[j].meta && routers[j].meta.role.length && routers[j].meta.role.indexOf(userRole) === -1) { // 如果没有权限就删除该路由,如果是父级路由没权限,所有子菜单就更没权限了,所以一并删除 routers.splice(j, 1) j = j !== 0 ? j - 1 : j // 删除掉没有权限的路由后,下标应该停止 +1,保持不变,如果下标是 0的话删除之后依然等于0 } if (routers[j].children && routers[j].children.length) { // 如果包含子元素就递归执行 eachSelect(routers[j].children, userRole) } } } // 拷贝这个数组是因为做权限测试的时候可以从低级切回到高级角色,仅限演示,正式开发时省略这步直接使用 addRouter // 仅限演示 let newArr = […addRouter] eachSelect(newArr, role) commit(‘setRouters’, newArr) // 正式开发 // eachSelect(addRouter, role) // commit(‘setRouters’, addRouter) } }}export default routerData按钮级别权限验证通过自定义指令获取当前按钮所需的有哪些权限,然后和当前用户的权限对比,如果没有权限则删除按钮// btnPermission.jsimport Vue from ‘vue’Vue.directive(‘roleBtn’,{ bind:function (el,binding) { let roleArr = binding.value; // 获取按钮所需权限 let userRole = JSON.parse(sessionStorage.getItem(‘info’)).role // 获取当前用户权限 if (roleArr && roleArr.indexOf(userRole) !== -1) { return false } else { el.parentNode.removeChild(el); } }})export default Vue使用自定义指令权限<el-button type=“primary” plain size=“medium”>查看</el-button><el-button type=“primary” plain size=“medium” v-role-btn="[‘admin’]">添加</el-button><el-button type=“danger” plain size=“medium” v-role-btn="[‘superAdmin’]">删除</el-button><el-button type=“primary” plain size=“medium” v-role-btn="[‘superAdmin’,‘admin’]">修改</el-button> ...

December 17, 2018 · 4 min · jiezi

Vue2.0 + ElementUI 手写权限管理系统后台模板(四)——组件结尾

i18n国际化多语言翻译使用框架采用vue-i18n版本 8.4.0,使用npm安装新建文件夹src/i18n,目录如下i18n.js//i18n.jsimport Vue from ‘vue’import locale from ’element-ui/lib/locale’import VueI18n from ‘vue-i18n’import messages from ‘./lang’Vue.use(VueI18n)const i18n = new VueI18n({ locale: localStorage.lang || ‘cn’, messages})locale.i18n((key, value) => i18n.t(key, value))export default i18ni18n/lang/index.js//index.jsimport en from ‘./en’import cn from ‘./cn’export default { en, cn}i18n/lang/cn.jscn.js和en.js 需要要翻译的内容要一一对照,我这里这是参考示例只写了一部分//cn.jsimport zhLocale from ’element-ui/lib/locale/lang/zh-CN’const cn = { home: ‘主页’, routeNmae: { home: ‘主页’, article: ‘文章管理’, ‘menu2-2’: ‘二级-2’, ‘menu2-3’: ‘二级-3’, }, rightMenu: { close: ‘关闭’, closeOther: ‘关闭其他’, closeAll: ‘全部关闭’ } …zhLocale // 合并element-ui内置翻译}export default cni18n/lang/en.js//en.jsimport enLocale from ’element-ui/lib/locale/lang/en’const en = { home: ‘home’, routeNmae: { home: ‘home’, article: ‘article’, ‘menu2-2’: ‘menu2-2’, ‘menu2-3’: ‘menu2-3’ }, rightMenu: { close: ‘close’, closeOther: ‘closeOther’, closeAll: ‘closeAll’ } …enLocale // 合并element-ui内置翻译}export default en多语言切换组件新建src/components/lang/langSelect.vue<!– langSelect.vue –><template> <el-dropdown class=‘international’ @command=“handleSetLanguage”> <div> <span class=“el-dropdown-link”><i class=“fa fa-language fa-lg”></i>&nbsp;{{language}}<i class=“el-icon-arrow-down el-icon–right”></i> </span> </div> <el-dropdown-menu slot=“dropdown”> <el-dropdown-item command=“cn”>中文</el-dropdown-item> <el-dropdown-item command=“en”>English</el-dropdown-item> </el-dropdown-menu> </el-dropdown></template>main.jsimport Vue from ‘./btnPermission’import ElementUI from ’element-ui’import ’element-ui/lib/theme-chalk/index.css’import ‘font-awesome/css/font-awesome.css’import App from ‘./App.vue’import router from ‘./router’import store from ‘./vuex’import i18n from ‘./i18n/i18n’new Vue({ el: ‘#app’, router, store, i18n, render: h => h(App), components: {App}, template: ‘<App/>’})使用:<!– 翻译使用 –><p>message: {{ $t(‘home’) }}</p><p>message: {{ $t(‘routeNmae.article’) }}</p><!– 多语言切换组件调用 –><langSelect></langSelect>vue中使用ECharts具体使用方法可以查看ECharts官网,需要注意的地方就是响应屏幕大小代码如下,在调用组件的页面 mounted () { this.selfAdaption() }, methods: { // echart自适应 selfAdaption () { let that = this setTimeout(() => { window.onresize = function () { if (that.$refs.echarts) { that.$refs.echarts.chart.resize() } } }, 10) } }编辑器-markdown框架目前只封装了markdown,实时获取markdown,html,text三种格式文本,支持内容回填,默认初始值,可以编辑已发布的文章或者草稿引用的Editor.md,点击查看插件更多的使用方法结束vue-xuAdmin 只注重框架基础功能,这几个组件是我最近用到的,更多的组件内容根据项目需求可以自己去封装。如果你感觉这个框架或者这几篇文章对你有所帮助,请去项目git上给个星点个star,感谢!orz项目地址:github:https://github.com/Nirongxu/v…码云:https://gitee.com/nirongxu/xu… ...

December 17, 2018 · 1 min · jiezi

携带参数隐藏必要参数,瞬间改变浏览器地址

点击详情跳转的时候,有时候有必要隐藏地址栏的必要参数,本次实验是通过sessionStorage存储 定时器刷新浏览器方式来实现的1.获取URL地址栏参数 及参数值function GetUrlParam(paraName) { var url = document.location.toString(); var arrObj = url.split("?"); if (arrObj.length > 1) { var arrPara = arrObj[1].split("&"); var arr; for (var i = 0; i < arrPara.length; i++) { arr = arrPara[i].split("="); if (arr != null && arr[0] == paraName) { return arr[1]; } } return null; } else { return null; }}2.将必要参数缓存到sessionStorage中if (GetUrlParam(‘validKey’) != null ) { sessionStorage.setItem(“validKey”, decodeURIComponent(GetUrlParam(‘validKey’))) }3.重定向带有参数的地址let url = location.href;if (url.indexOf("?") != -1) { url = url.split("?")[0]; location.href = url;}// 通过定时器方式刷新浏览器一次let w1 = setTimeout(() => { location.reload();}, 100);setInterval(() => { clearTimeout(w1);}, 100); ...

December 14, 2018 · 1 min · jiezi

一个完整的增删改查模块(以我们的项目‘危化品库管理’模块为例)

父组件列表页面<!– 危化品库管理 –><template> <div> <!– 添加 –> <div class=“right add” @click=“add”> </div> <!– 搜索 –> <div class=“searchPart”> <div class=“search_row”> <el-form :inline=“true” :model=“form” :rules=“rules” ref=“elform”> <el-form-item label=“危化品名称:” prop=“dangerousname”> <div><input type=“text” @keyup.enter=“handleFilter” v-model.trim=“listQuery.dangerousname” class=“search_input”></div> </el-form-item> <el-form-item label=“CAS号:” prop=“cascode”> <div><input type=“text” @keyup.enter=“handleFilter” v-model.trim=“listQuery.cascode” class=“search_input”></div> </el-form-item> <el-form-item label=“危化品类型:” prop=“dicDangeroustype”> <el-select placeholder=“请选择危化品类型” size=“mini” v-model=“listQuery.dicDangeroustype” @change=“handleFilter”> <el-option v-for=“item in localWord.category” :key=“item.code” :label=“item.codename” :value=“item.code”> </el-option> </el-select> </el-form-item> <el-form-item label=“别名:” prop=“othername”> <div><input type=“text” @keyup.enter=“handleFilter” v-model.trim=“listQuery.othername” class=“search_input”></div> </el-form-item> <el-form-item> <el-button size=“mini” type=“primary” icon=“el-icon-search” @click=“handleFilter”>查询</el-button> <el-button size=“mini” type=“primary” icon=“el-icon-download” @click=“exportData”>导出</el-button> </el-form-item> </el-form> </div> </div> <!– table列表展示 –> <el-row> <el-table :data=“girdData” highlight-current-row :max-height=“gridTableMaxHeight” :header-cell-style="{background:‘rgb(212, 232, 255)’,color:‘rgba(0, 0, 0, 0.85)’}" border style=“width: 100%” @selection-change=“handleSelectionChange”> <el-table-column type=“selection” width=“55”> </el-table-column> <el-table-column property=“serialNumber” label=“序号” min-width=“35” align=“center”> <template slot-scope=“scope”> <span>{{scope.$index+(listQuery.page - 1) * listQuery.rows + 1}}</span> </template> </el-table-column> <el-table-column property=“dangerousname” label=“危化品名称” min-width=“140”></el-table-column> <el-table-column sortable label=“CAS号” property=“cascode” min-width=“120”></el-table-column> <el-table-column label=“别名” property=“othername” min-width=“120”></el-table-column> <el-table-column label=“危化品类型” property=“dicDangeroustypeStr” min-width=“140”></el-table-column> <el-table-column label=“英文名称” property=“englishname” min-width=“120”></el-table-column> <el-table-column label=“分子式” property=“molecularFormula” min-width=“120”></el-table-column> <el-table-column label=“熔点” property=“meltingPoint” min-width=“120”></el-table-column> <el-table-column label=“密度” property=“theDensityOf” min-width=“120”></el-table-column> <el-table-column label=“溶解性” property=“solubility” min-width=“120”></el-table-column> <el-table-column label=“操作” width=“140” align=“center” fixed=“right”> <template slot-scope=“scope”> <el-button type=“text” size=“small” @click=“editCheckBtn(scope.$index, scope.row, ’edit’)"><span class=“icons edit_icon”></span></el-button> <el-button type=“text” size=“small” @click=“editCheckBtn(scope.$index, scope.row, ‘check’)"><span class=“icons check_icon”></span></el-button> <el-button type=“text” size=“small” @click=“del(scope.$index, scope.row)"><span class=“icons delete_icon”></span></el-button> </template> </el-table-column> </el-table> </el-row> <!– 分页 –> <el-row type=“flex” justify=“end” style=“padding:20px” class=“page”> <el-pagination v-show=“total>0” background @size-change=“handleSizeChange” @current-change=“handleCurrentChange” :current-page=“listQuery.page” :page-sizes="[10, 20, 30]” :page-size=“listQuery.rows” layout=“total, sizes, prev, pager, next, jumper” :total=“total”> </el-pagination> </el-row> <router-view></router-view> </div></template><script> import download from ‘js-file-download’ import moment from ‘moment’ import DChemStoreManagementAPI from “@/api/DChemStoreManagementAPI”; export default { data() { return { // 查询条件 listQuery: { page: 1, //当前第几页 rows: 10, //每页显示多少条 pkEntid: “1”, dangerousname: “”, cascode: “”, dicDangeroustype: “”, othername: "” }, total: null, //共多少条数据 girdData: [], // 字典查询 localWord: {}, multipleSelection: [], multipleSelectionIdArr: [], gridTableMaxHeight: document.body.clientHeight - 310, rules: {}, form: { dangerousname: “”, cascode: “”, othername: “”, dicDangeroustype: “”, dicDangeroustypeStr: “”, toxicity: “”, environmentalparameter: “”, englishname: “”, molecularFormula: “”, molecularWeight: “”, meltingPoint: “”, theDensityOf: “”, solubility: “”, purpose: “”, dangerousinfo: “”, testmethod: “”, eliminationmethod: "” } }; }, watch: { “listQuery.dicDangeroustypeStr”(v) { } }, methods: { handleSizeChange(val) { this.listQuery.rows = val; this.initTable(); }, handleCurrentChange(val) { this.listQuery.page = val; this.initTable(); }, handleFilter() { this.listQuery.page = 1; this.initTable(); }, initTable() { DChemStoreManagementAPI.getList(this.listQuery).then(data => { this.total = data.data.total; this.girdData = data.data.rows; this.multipleSelectionIdArr = data.data.rows.map(item=>{ return item.pkDangerid }) }); }, exportData () { DChemStoreManagementAPI.exportData(this.listQuery).then((data)=>{ if(this.listQuery) { download(data, 危化品名称${moment(new Date().getTime()).format('YYYYMMDDHHmmss')}.xls) this.$message({ type: “success”, message: “导出成功!” }); } }) }, handleSelectionChange(row) { this.multipleSelection = row.map(item=>{ return item.pkDangerid }) }, add() { this.$router.push({ name: “dchemstoremanagementform”, query: { status: “add” } }); }, del(index, row) { this.$confirm(“此操作将永久删除该选项, 是否继续?”, “提示”, { confirmButtonText: “确定”, cancelButtonText: “取消”, type: “warning” }) .then(() => { DChemStoreManagementAPI.del(row.pkDangerid).then(()=>{ this.initTable(); }); this.$message({ type: “success”, message: “删除成功!” }); }) .catch(function(response) {}); }, editCheckBtn(index, row, typeBtn) { this.$router.push({ name: “dchemstoremanagementform”, query: { status: typeBtn, pkDangerid: row.pkDangerid } }); } }, created(){ DChemStoreManagementAPI.getSelect().then(data => { this.localWord = data; this.localWord.category.unshift({ code: “”, codeenname: “all”, codename: “全部” }); }); }, mounted() { this.initTable(); let that = this; window.onresize = () => { return (() => { that.gridTableMaxHeight = document.body.clientHeight - 310; })(); }; } }; </script> 子组件 增改查页面<template><div> <el-dialog :title="危化品库管理 - ${formTitle}" :visible=“true” :lock-scrol=“true” width=“780px” @close=“closeDlg” center> <el-row :gutter=“24”> <el-col :span=“24”> <el-form status-icon :model=“form” :inline=“true” :rules=“rules” ref=“elform” label-width=“140px”> <el-row :gutter=“0”> <el-col :span=“12”> <el-form-item label=“危化品名称:” prop=“dangerousname”> <el-input size=“small” v-model=“form.dangerousname” placeholder=“请输入危化品名称” v-if=“status==‘add’|| status==‘edit’"></el-input> <span v-else>{{form.dangerousname}}</span> </el-form-item> </el-col> <el-col :span=“12”> <el-form-item label=“CAS号:” prop=“cascode”> <el-input size=“small” v-model=“form.cascode” placeholder=“请输入CAS号” v-if=“status==‘add’|| status==‘edit’"></el-input> <span v-else>{{form.cascode}}</span> </el-form-item> </el-col> </el-row> <el-row :gutter=“0”> <el-col :span=“12”> <el-form-item label=“别名:” prop=“othername”> <el-input size=“small” v-model=“form.othername” placeholder=“请输入别名” v-if=“status==‘add’|| status==‘edit’"></el-input> <span v-else>{{form.othername}}</span> </el-form-item> </el-col> <el-col :span=“12”> <el-form-item label=“危化品类型:” prop=“dicDangeroustype”> <el-select placeholder=“请选择危化品类型” size=“small” v-model=“form.dicDangeroustype” v-if=“status==‘add’|| status==‘edit’"> <el-option v-for=“item in localWord.category” :key=“item.code” :label=“item.codename” :value=“item.pkCodenum”> </el-option> </el-select> <span v-else>{{form.dicDangeroustypeStr}}</span> </el-form-item> </el-col> </el-row> <el-row :gutter=“0”> <el-col :span=“12”> <el-form-item label=“毒性:” prop=“toxicity”> <el-input size=“small” v-model=“form.toxicity” placeholder=“请输入毒性” v-if=“status==‘add’|| status==‘edit’"></el-input> <span v-else>{{form.toxicity}}</span> </el-form-item> </el-col> <el-col :span=“12”> <el-form-item label=“环境参数:” prop=“environmentalparameter”> <el-input size=“small” v-model=“form.environmentalparameter” placeholder=“请输入环境参数” v-if=“status==‘add’|| status==‘edit’"></el-input> <span v-else>{{form.environmentalparameter}}</span> </el-form-item> </el-col> </el-row> <el-row :gutter=“0”> <el-col :span=“12”> <el-form-item label=“英文名称:” prop=“englishname”> <el-input size=“small” v-model=“form.englishname” placeholder=“请输入英文名称” v-if=“status==‘add’|| status==‘edit’"></el-input> <span v-else>{{form.englishname}}</span> </el-form-item> </el-col> <el-col :span=“12”> <el-form-item label=“分子式:” prop=“molecularFormula”> <el-input size=“small” v-model=“form.molecularFormula” placeholder=“请输入分子式” v-if=“status==‘add’|| status==‘edit’"></el-input> <span v-else>{{form.molecularFormula}}</span> </el-form-item> </el-col> </el-row> <el-row :gutter=“0”> <el-col :span=“12”> <el-form-item label=“分子量:” prop=“molecularWeight”> <el-input size=“small” v-model=“form.molecularWeight” placeholder=“请输入分子量” v-if=“status==‘add’|| status==‘edit’"></el-input> <span v-else>{{form.molecularWeight}}</span> </el-form-item> </el-col> <el-col :span=“12”> <el-form-item label=“熔点:” prop=“meltingPoint”> <el-input size=“small” v-model=“form.meltingPoint” placeholder=“请输入熔点” v-if=“status==‘add’|| status==‘edit’"></el-input> <span v-else>{{form.meltingPoint}}</span> </el-form-item> </el-col> </el-row> <el-row :gutter=“0”> <el-col :span=“12”> <el-form-item label=“密度:” prop=“theDensityOf”> <el-input size=“small” v-model=“form.theDensityOf” placeholder=“请输入密度” v-if=“status==‘add’|| status==‘edit’"></el-input> <span v-else>{{form.theDensityOf}}</span> </el-form-item> </el-col> <el-col :span=“12”> <el-form-item label=“溶解性:” prop=“solubility”> <el-input size=“small” v-model=“form.solubility” placeholder=“请输入溶解性” v-if=“status==‘add’|| status==‘edit’"></el-input> <span v-else>{{form.solubility}}</span> </el-form-item> </el-col> </el-row> <el-row :gutter=“24”> <el-col :span=“24”> <el-form-item label=“用途:” prop=“purpose”> <el-input size=“small” type=“textarea” class=“special-530” :autosize=”{ minRows: 3}” v-model=“form.purpose” placeholder=“请输入用途信息” v-if=“status==‘add’|| status==‘edit’"></el-input> <el-input size=“small” type=“textarea” resize=“none” class=“readonly special-530” :autosize=”{ minRows: 1}” v-model=“form.purpose” placeholder=“请输入用途信息” v-else readonly></el-input> <!– <span v-else>{{form.purpose}}</span> –> </el-form-item> </el-col> </el-row> <el-row :gutter=“24”> <el-col :span=“24”> <el-form-item label=“环境危害:” prop=“dangerousinfo”> <el-input size=“small” type=“textarea” class=“special-530” :autosize=”{ minRows: 3}” v-model=“form.dangerousinfo” placeholder=“请输入环境危害信息” v-if=“status==‘add’|| status==‘edit’"></el-input> <span v-else>{{form.dangerousinfo}}</span> </el-form-item> </el-col> </el-row> <el-row :gutter=“24”> <el-col :span=“24”> <el-form-item label=“检测方法:” prop=“testmethod”> <el-input size=“small” type=“textarea” class=“special-530” :autosize=”{ minRows: 3}” v-model=“form.testmethod” placeholder=“请输入检测方法信息” v-if=“status==‘add’|| status==‘edit’"></el-input> <span v-else>{{form.testmethod}}</span> </el-form-item> </el-col> </el-row> <el-row :gutter=“24”> <el-col :span=“24”> <el-form-item label=“控制消除方法:” prop=“eliminationmethod”> <el-input size=“small” type=“textarea” class=“special-530” :autosize=”{ minRows: 3}” v-model=“form.eliminationmethod” placeholder=“请输入控制消除方法信息” v-if=“status==‘add’|| status==‘edit’"></el-input> <span v-else>{{form.eliminationmethod}}</span> </el-form-item> </el-col> </el-row> <el-row :gutter=“24”> <el-col :span=“24”> <el-form-item label=“危险特性:” prop=“characteristic”> <el-input size=“small” type=“textarea” class=“special-530” :autosize=”{ minRows: 3}” v-model=“form.characteristic” placeholder=“请输入危险特性信息” v-if=“status==‘add’|| status==‘edit’"></el-input> <span v-else>{{form.characteristic}}</span> </el-form-item> </el-col> </el-row> <el-row :gutter=“24” class=“text-center”> <el-col :span=“24”> <el-form-item v-if=“status==‘add’|| status==‘edit’"> <el-button type=“primary” size=“small” @click=“onSubmit”>保存</el-button> <el-button @click=“closeDlg” size=“small”>取消</el-button> </el-form-item> <el-form-item v-else> <el-button type=“primary” size=“small” @click=“closeDlg”>关闭</el-button> </el-form-item> </el-col> </el-row> </el-form> </el-col> </el-row> </el-dialog> </div></template><script>import { mapGetters } from “vuex”;import { validatorRules } from “@/comman/validator”;import DChemStoreManagementAPI from “@/api/DChemStoreManagementAPI”;export default { data() { return { formTitle: ‘添加’, status: this.$route.query.status, localWord: {}, form: { pkEntid: “”, dangerousname: “”, cascode: “”, othername: “”, dicDangeroustype: “”, dicDangeroustypeStr: “”, toxicity: “”, environmentalparameter: “”, englishname: “”, molecularFormula: “”, molecularWeight: “”, meltingPoint: “”, theDensityOf: “”, solubility: “”, purpose: “”, dangerousinfo: “”, testmethod: “”, eliminationmethod: "” }, rules: { dangerousname: [ { required: true, message: “请输入危化品名称”, trigger: “blur” }, validatorRules.shortRules ], cascode: [ { required: true, message: “请输入CAS号”, trigger: “blur” }, validatorRules.shortRules ], othername: [ { required: true, message: “请输入别名”, trigger: “blur” }, validatorRules.shortRules ], dicDangeroustype: [ { required: true, message: “请输入危化品类型”, trigger: “blur” } ], toxicity: [ { required: true, message: “请输入毒性”, trigger: “blur” }, validatorRules.shortRules ], environmentalparameter: [ { required: true, message: “请输入环境参数”, trigger: “blur” }, validatorRules.shortRules ], englishname: [ { required: true, message: “请输入英文名称”, trigger: “blur” }, validatorRules.shortRules ], molecularFormula: [ { required: true, message: “请输入分子式”, trigger: “blur” }, validatorRules.shortRules ], molecularWeight: [ { required: true, message: “请输入分子量”, trigger: “blur” }, validatorRules.shortRules ], meltingPoint: [ { required: true, message: “请输入熔点”, trigger: “blur” }, validatorRules.shortRules ], theDensityOf: [ { required: true, message: “请输入密度”, trigger: “blur” }, validatorRules.shortRules ], solubility: [ { required: true, message: “请输入溶解性”, trigger: “blur” }, { min: 0, max: 30, message: ‘长度在 0 到 30 个字符’, trigger: ‘blur’ } // validatorRules.shortRules ], purpose: [ { required: true, message: “请输入用途”, trigger: “blur” }, { min: 0, max: 200, message: ‘长度在 0 到 200 个字符’, trigger: ‘blur’ } ], dangerousinfo: [ { required: true, message: “请输入环境危害”, trigger: “blur” }, { min: 0, max: 200, message: ‘长度在 0 到 200 个字符’, trigger: ‘blur’ } ], testmethod: [ { required: true, message: “请输入检测方法”, trigger: “blur” }, { min: 0, max: 200, message: ‘长度在 0 到 200 个字符’, trigger: ‘blur’ } ], eliminationmethod: [ { required: true, message: “请输入控制消除方法”, trigger: “blur” }, { min: 0, max: 200, message: ‘长度在 0 到 200 个字符’, trigger: ‘blur’ } ], characteristic: [ { required: true, message: “请输入危险特性”, trigger: “blur” }, { min: 0, max: 200, message: ‘长度在 0 到 200 个字符’, trigger: ‘blur’ } ] } }; }, beforeRouteEnter(to, from, next) { DChemStoreManagementAPI.getSelect().then(data => { next(vm => { console.log(data); vm.localWord = data; }); }); }, methods: { onSubmit() { this.$refs[“elform”].validate(valid => { if (valid) { DChemStoreManagementAPI.add(this.form).then(data => { this.$parent.initTable(); this.$router.back(); }); } else { return false; } }); }, closeDlg() { this.$router.back(); } }, mounted() { var that = this; if(this.status == ’edit’) { this.formTitle = ‘修改’ } else if(this.status == ‘check’) { this.formTitle = ‘详情’ } if (this.$route.query.pkDangerid) { DChemStoreManagementAPI.getById(this.$route.query.pkDangerid).then( obj => { that.form = obj.data; // that.form.dicDangeroustype = obj.data.dicDangeroustype.toString(); } ); } }};</script>APIimport axios from “axios”;import qs from “qs”;let DChemStoreManagementAPI = { getList(params) { return axios.get(”…”, { params }); }, add(params){ return axios({ method: “post”, url: “…/save”, data: qs.stringify(params) }) }, getSelect(params) { return axios(”…”, { params }) }, getById(id) { return axios.get(”…?id="+ id, { }); }, del(id) { return axios.delete("…?id=" + id, { }); }, exportData(params) { return axios.get("…", { responseType: ‘arraybuffer’, params }); }, deleteFile(params) { return axios.delete("…", { params: { filePath: params.filepath } }); }};export default DChemStoreManagementAPI;以上便是模块的增删改查内容,至于上传模块,没有过多的解释,上传用的是封装过的组件,代码太多,不便复制,不过有下载功能^_^,这也是一点小小的总结。 ...

December 14, 2018 · 7 min · jiezi

关于elementUI el-table标签的一个坑

最近发现<el-table>的一个坑,后台返回一个list,我在请求成功后立即为data中的dataList变量赋值(该变量绑定在el-table的data属性中)之后我循环该变量为数组中的每个对象增加isOpen属性,结果也出现在el-table中了,但是我又写了一个toggle方法改变isOpen的时候发现了问题,数据改变了但是view并没有及时渲染反复尝试后发现改变其他的属性,即赋值前后台返回给我的list中的属性,页面会将之前的isOpen改变渲染,再进一步思考是否是因为在首次赋值时,el-table即监听了所有属性,而之后加入的并不会到监听列表中,于是有了以下代码有了中间变量后,达到了想要的效果,也初步印证了我刚才的想法。

December 14, 2018 · 1 min · jiezi

封装框架的实践

最近在尝试着封装一个框架,碍于种种原因,先从简单的入手吧。基于vue和elementUI封装的框架,集成 数据存储localforage、字体图标库font-awesome、css拓展语言scss、网络请求axios等模块,为了让业务开发更专注于数据驱动。项目源码地址:https://gitee.com/g2333/data_…使用场景1. 环境 框架基于vue2.0开发,故开发环境也需要nodejs和vue-cli。2. 拓展和维护 为使框架本身易拓展和维护,项目采用vue-cli封装,在开发和使用过程都不打包,保持程序的可读性,同时也方便在引用该模块时可简单的修改配置文件和源码。3. 便捷使用 在一个全新的vue-cli初始化项目中, 安装模块(在vue项目路径下npm i modulecomponents), 引用模块(在vue项目的main.js中添加import ‘modulecomponents/index.js’) 测试使用(比如使用框架暴露的方法dataTool.alert(‘测试成功’))项目配置1. 依赖模块 框架本身依赖有如下模块: elementUI 框架的主力,用于组件封装和方法的调用、 localforage 数据存储,用于存储前端的大量数据、 font-awesome 字体图标库、 scss css拓展语言、 axios 网络请求2. 设置项目入口 修改package.json文件,添加main字段,指向项目入口(“main”: “mc/index.js”),修改private字段,设置为开源(“private”: false)3. 项目初始化 为了让框架方便引用,故在初始化文件index.js(框架项目开发过程使用indexdsForDev.js),自动引入依赖和全局变量的挂载4. 文件提交 设置项目.gitignore文件忽略node_modules避免在协同开发时因为环境不一致导致的webpack报错 设置项目.npmignore文件忽略发布时非必要的文件,减少模块的体积封装的模块1. 组件 组件基于elementUI封装,项目中封装的组件为避免命名冲突,都以mc-为前缀开头。 计划封装的组件有如下: 表格mc-table、 表单mc-form、 树列表mc-tree、 对话框mc-dialog、 上下文菜单mc-contentmenu、 按钮组mc-btns、 流图mc-flow、 下拉选框mc-select、 附件上传mc-upload//在界面上显示一个表单<mc-form :object=“form”></mc-form>//表单对象,描述表单的结构和数据form: new mc.Form({ structure: [{ label: ‘测试’, name: ’test’, }], data: { test: ‘hello world’, }}) 除框架封装的组件外,依旧支持使用elementUI组件2. 全局方法 为了方便开发,较为常用的方法被挂载在全局变量dataTool的属性中,比如 请求方法:ajax请求httpReq、文件导出exportFile、文件上传uploadFile; 提示类方法:警告弹框alert、边角提示notify、确认输入框confirm、锁屏加载loading等; 调用组件类方法:打开弹窗openDialog、关闭弹窗closeDialog、打开上下文菜单openContextmenu、关闭上下文菜单closeContextmenu等; 数据处理:对象类型的克隆和过滤objClone、时间格式的转化formatTime、cookie的添加setCookie等; 原型链上的方法:获取表格新增的一行数据Array.newTableRow、数组元素位置交换Array.swap等; 事件方法:注册事件addEvent、触发事件emitEvent、取消事件cancelEvent等;//打开上下文菜单,点击导出文件,将请求的内容导出成flow.json文件dataTool.openContextmenu(event,[{ text: ‘导出文件’, icon: ‘fa fa-download’, color: ‘blue’, click: ()=>{ const reqObj = {url:‘http://rap2api.taobao.org/app/mock/22119/FUNC=getFlow’, params: {}, type:‘mock’}; dataTool.httpReq(reqObj).then(res=>{ dataTool.exportFile({fileName: ‘flow.json’,data: JSON.stringify(res.CONTENT)}); }); }}])3. 配置文件 封装的组件各有一份默认配置文件,方便全局调整组件的参数。 封装的组件既支持组件类的默认参数修改,也兼容修改单个实例和继承组件类export default { //表单类的配置文件 btns: [], //表单底部栏按钮 topBtns: [], //表单顶部栏按钮 hiddenRows: [], //隐藏的行 topBtnStyle: ‘’, bottomBtnStyle: ’text-align:right’, dialogEdit: false, //是否开启普通字符串类型的弹窗编辑功能 showRules: true, //是否显示表单规则验证 style: “margin: 10px;”, inline: false, labelWidth: “50px”, labelPosition: “right”, size: “small”, autoComplete: ‘on’, spellcheck: false, readOnly: false, extBtnIcon: ’el-icon-more’, textArea: { size: { minRows: 1, maxRows: 10}, resize: ’none’, }, tag: { input: ‘’, type: ‘warning’, closeTransition: false, appendWord: ’ + New Tag’, }, inputStyle: ‘width:100%’, dataType: { //采用小写,减少枚举数量 bool: [‘bool’,‘boolean’,‘switch’], checkboxGroup: [‘checkboxgroup’,‘checkbox’], radio: [‘radio’], select: [‘singleenum’,‘multiselect’,‘multienum’], time: [’time’], date: [‘date’,‘datetime’,‘datetimerange’,‘daterange’], button: [‘button’,‘btn’], tag: [’tags’,’tag’], input: [’’,‘input’,‘string’,’text’,’textarea’,’number’,‘float’,‘password’,‘double’,‘int’,‘integer’,’long’,‘search’,’extinput’], component: [‘mc-table’], },}开发记录1. 项目结构 整体项目的规划整理在一个xmind文件中,方便记录开发进度和了解项目的整体大纲,这是图片版 http://qpic.cn/dDPbFwEeD (请在复制粘贴到浏览器的地址栏中访问)2. 使用文档 为了记录开发进度和形成规范,项目开发的使用说明和修改会记录在石墨文档https://shimo.im/sheet/K8QPjP…3. 版本控制 使用git作为版本控制,项目的源码托管在码云上https://gitee.com/g2333/data_… 既方便协同开发,也方便代码版本控制框架更新1. 项目更新 修改后的源码在测试成功后,修改package.json中的版本号,将代码推送到码云上,然后通过npm发布新版本2. 模块更新 通过npm update modulecomponents指令更新模块,即可使用最新版功能 ...

December 9, 2018 · 1 min · jiezi

element-ui中cascader同时获取label和value值

关于elementUI中cascader选中值后,能获取value或者label,但不能同时获value和label,这一问题,琢磨出了这么个办法。以新增和编辑城市为例,type: 1 编辑,type: 0 新增1. 配置元素<el-cascader filterable :class="{‘city-cascader’: type==1}" :placeholder=“city || ‘请选择’” :options=“cityLists” :props=“cityProps” v-model=“citySelected” style=“width:300px;” :show-all-levels=“false” @change=“changeCity” ></el-cascader>2. 配置cityPropscityProps: {value: ‘all’, label: ’label’}3. 组装props中的all// cityLists中遍历组装allall: { value: value, label: label}4. 使用此时,点击cascader选择需要的内容后,取出来的citySelected值就是[{value: 选中值的value, label: 选中值的label}]这个方法可以通过配置all获取任意自己想要的值。PS: 关于拿不到默认值的问题,我投机取巧的使用了placeholder。:placeholder=“city || ‘请选择’“然后在cascader上加上样式::class=”{‘city-cascader’: type==1}".city-cascader .el-input__inner::placeholder { color: #333 !important;}ok,完美解决cascader取值问题。

November 16, 2018 · 1 min · jiezi

使用Vue CLI 3将基于element-ui二次封装的组件发布到npm

前言:之前在网上找的好多都是基于vue-cli 2.x的,而使用vue-cli 3的文章比较少,所以我在自己尝试的时候把几篇文章结合了一下,调出来了我想要的模式,也就是Vue CLI 3 + element-ui + 多个二次封装的组件。最终想要的是 element-ui 这种感觉的,很多组件可以在不同项目中复用。安装依赖首先用Vue CLI 3来初始化项目yarn global add @vue/clivue create qiyun-el-uivue ui安装element-ui这里使用官方提供的的插件安装:http://element.eleme.io/#/zh-…https://github.com/ElementUI/…在插件列表搜索element在这里我选的手动导入,图中是全部导入这样在项目中,就会新建一个plugins文件夹,里面有个element.js 文件,如果想手动引入,就在这里添加要依赖的组件,这里是为了调试组件:import Vue from ‘vue’import { Button, Dialog } from ’element-ui’Vue.use(Button)Vue.use(Dialog)由于我们是基于element-ui的部分组件做的二次封装,所以最好还是按需引入所依赖的组件比较好。编写组件在 src 的同级下面新建 packages 目录,在这里添加自己封装的要发布的组件。例如,新建 qe-modal 文件夹,再接着新建 src 文件夹,里面新建 qe-modal.vue,在这里写组件的代码:<template> <el-dialog :title=“title” :visible=“dialogVisible” @close="$emit(‘update:dialogVisible’, false)" :width=“width”> <slot name=“modal-body”></slot> <div slot=“footer” class=“dialog-footer”> <slot name=“modal-footer”> <el-button @click="$emit(‘update:dialogVisible’, false)" size=“small”>取 消</el-button> <el-button type=“primary” @click="$emit(‘confirm’)" size=“small” :disabled=“confirmDisable || beforeSendDisable”>{{ beforeSendDisable? “处理中…” : “确 定” }}</el-button> </slot> </div> </el-dialog></template><script>export default { name: ‘qeModal’, props: { dialogVisible: Boolean, title: String, width: { type: String, default: ‘580px’ }, beforeSendDisable: { type: Boolean, default: false }, confirmDisable: { type: Boolean, default: false } }}</script>在 qe-modal 根目录下新建 index.js ,里面注册单独的该组件,方便使用时可以单独引用:import qeModal from ‘./src/qe-modal’qeModal.install = function(Vue) { Vue.component(qeModal.name, qeModal)}export default qeModal这样一个组件就添加完成了,然后需要在 packages 的根目录下添加一个总的 index.js,这里是全局注册的地方,使用时可以全局引入,其实就跟 element-ui 的两种方式一样:import qeModal from ‘./qe-modal’const components = [ qeModal]const install = function(Vue) { components.forEach(component => { Vue.component(component.name, component); });}if (typeof window !== ‘undefined’ && window.Vue) { install(window.Vue);}export default { install, qeModal}后面再添加组件,在这里也要再注册一下,而element-ui 源码中是动态引入的,我们的项目组件还没那么多,可以先一个个手动引入,如果后面数量多了,不好维护,可以参考 element-ui 的源码实现,我在这里做了一些简单的解释。配置 npm在 package.json 里面的 script 里面加一个 lib选项,方便每次构建:“scripts”: { // …, “lib”: “vue-cli-service build –target lib –name qiyun-el-ui –dest lib ./packages/index.js” },其中 –name 后面是你最后想要生成文件的名字,并用 –dest lib 修改了构建的目录。然后在 package.json 里面添加一些npm包发布的相关信息,比如作者、版本等:其中最重要的是:“main”: “lib/qiyun-el-ui.common.js”,这里的路径要和上面构建出来的目录和文件名对应上。里面的配置项,在网上找了个例子:{ “name”: “maucash”, “description”: “maucash中常用组件抽取”, “version”: “1.0.2”, “author”: “kuangshp <kuangshp@126.com>”, // 开源协议 “license”: “MIT”, // 因为组件包是公用的,所以private为false “private”: false, // 配置main结点,如果不配置,我们在其他项目中就不用import XX from ‘包名’来引用了,只能以包名作为起点来指定相对的路径 “main”: “dist/maucash.js”, “scripts”: { “dev”: “cross-env NODE_ENV=development webpack-dev-server –open –hot”, “build”: “cross-env NODE_ENV=production webpack –progress –hide-modules” }, “dependencies”: { “axios”: “^0.18.0”, “iview”: “^2.14.1”, “style-loader”: “^0.23.1”, “url-loader”: “^1.1.2”, “vue”: “^2.5.11” }, // 指定代码所在的仓库地址 “repository”: { “type”: “git”, “url”: “git+git@git.wolaidai.com:maucash/maucash.git” }, // 指定打包后,包中存在的文件夹 “files”: [ “dist”, “src” ], // 指定关键词 “keywords”: [ “vue”, “maucash”, “code”, “maucash code” ], // 项目官网的地址 “homepage”: “https://github.com/kuangshp/maucash", “browserslist”: [ “> 1%”, “last 2 versions”, “not ie <= 8” ], “devDependencies”: { “babel-core”: “^6.26.0”, “babel-loader”: “^7.1.2”, “babel-plugin-transform-runtime”: “^6.23.0”, “babel-preset-env”: “^1.6.0”, “babel-preset-stage-3”: “^6.24.1”, “cross-env”: “^5.0.5”, “css-loader”: “^0.28.7”, “file-loader”: “^1.1.4”, “node-sass”: “^4.5.3”, “sass-loader”: “^6.0.6”, “vue-loader”: “^13.0.5”, “vue-template-compiler”: “^2.4.4”, “webpack”: “^3.6.0”, “webpack-dev-server”: “^2.9.1” }}发布到npm到这块后面的网上有很多更细致的教程,我就不在这里赘述了。下面给出两个文章的链接,供参考。1、到npm上注册一个账号2、登录npm login3、添加用户信息npm adduser4、发布到远程仓库(npm)上npm publish5、删除远程仓库的包npx force-unpublish package-name ‘原因描述’参考:https://juejin.im/post/5bc441…Vue cli3 库模式搭建组件库并发布到 npm的流程_vue.js_脚本之家 ...

November 6, 2018 · 2 min · jiezi

NSIS 打包 Electron 生成exe安装包

每次文章都从0开始从搭建开始 使用的是electron-vue 毕竟方便一点 如果只想安装electron 请参见我的另一个文章https://segmentfault.com/a/11…开发目录: F:lee`开发环境: windows10IDE: phpstorm安装electronvue init simulatedgreg/electron-vue project3cd project1npm install //第一次安装的伙伴需要翻墙 如何翻墙请参加另一个文章(好像被和谐了 那就+我们的交流群 814270669 吧!)编写一个页面使用IDE打开随便编写一个页面 使用npm 构建安装包npm run build安装程序制作下载NSIS软件,安装下载地址:https://pan.baidu.com/s/1HrZz…下载完毕打开 下一步 下一步 就行了 傻瓜式安装NSIS新建脚本点击软件左上角文件->选择新建脚本(向导)到应用程序信息这里 填写的应用程序名称必须和你package.json里面配置的一样 否则你有自动更新的时候会安装一个另一个程序!这里选择图标就行了这里暂时默认就行了 后面出一个文章详细介绍这里F:\lee\project3\build\win-unpacked\project3.exe主程序就是 buildwin-unpacked的exe文件选择 F:\lee\project3\build\win-unpacked编译脚本终于到了编译脚本了 如果按照上面的步骤执行 到这步会自动编译并且运行 如果没有自动编译点击顶部菜单栏的编译按钮编译过程可能稍微有点长1-3分钟吧 编译完成之后会自动运行安装程序友情提示杀软报毒electron做的软件会被某流氓杀软报毒 没办法解决 在这里给出一个解决办法安装程序检测360是否运行 如果在运行就禁止安装其中使用到一个dll插件 (FindProcDLL.dll)官方下载地址:http://nsis.sourceforge.net/F…作者提供的下载地址:https://pan.baidu.com/s/1EpJa…下载完毕之后 放到NSIS目录下的 VNISEdit\Plugins 目录中如果不知道目录 那就在桌面 右击VNISEdit 编译环境 选择打开所在目录 就可以看到了在脚本最后加一句编译完成后会后些方法:一个是un.onInit ->卸载程序一个是un.onUninstSuccess -> 卸载成功提示.onInit 安装程序初始化# 检测360杀毒软件是否在运行Function .onInitFindProcDLL::FindProc “360tray.exe” Pop $R0 IntCmp $R0 1 0 no_run MessageBox MB_ICONSTOP “安装程序检测到360流氓软件正在运行,请退出程序后重试!” Quit no_run:FunctionEnd由于我电脑没有装360 所以我使用qq 来做演示# 检测电脑管家是否在运行Function .onInitFindProcDLL::FindProc “QQ.exe” Pop $R0 IntCmp $R0 1 0 no_run MessageBox MB_ICONSTOP “安装程序检测到qq流氓软件正在运行,请退出程序后重试!” Quit no_run:FunctionEndNSIS运行必须为管理员请以管理员身份运行VNISEdit 编译环境 不然会终止编译并且有一个警告 好像是需要提级 什么什么的! ...

October 17, 2018 · 1 min · jiezi

10.12 论客科技面试题目

自我介绍2. 研究生为什么想到来做前端3. 哪个项目是最完整的4. 项目中遇到的问题5. vue文件的渲染机制6. 项目中vue的路由有多少层7. 项目支持响应式8. UI用什么框架9. 用ElementUI中遇到什么问题10. 了解vue中style中的scope吗11. 组件中嵌套了其他组件,如何修改样式12. 了解ES6的语法13. Promise实现的原理是什么?可以写出来吗?14. 前端关注哪一方面的东西?15. 了解JS闭包吗?实际用过吗?16. 研究生偏向研究什么算法?17. 前端方面比别人有什么优势?

October 13, 2018 · 1 min · jiezi

Vue:Elementui中的Tag与页面其它元素相互交互的两三事

前言公司系统在用elementui做后台开发,不免遇到一些需要自己去根据原有的功能上,加一些交互的功能。今天来介绍下我在用elementUi里的Tag标签与多选框交互的过程,东西听上去很简单,但就是越简单的东西越容易出一些问题。官方tag文档:elementUi-tag标签效果图:思路一、多选框勾选,出现对应的tag: 1.利用watch监听多选框绑定的值A(数组)的变化;2.根据A的变化,循环拿到勾选多选框的id对应的name,将id以及对应的name组成新的对象数组;3.将上一步得到的对象数组,去重(产品要求,出现的tag里不能有重复的)得到结果B;4.将B赋值给tags,循环展示出来;二、点击tag上的删除按钮,删除当前的tag,并将对应勾选的多选框取消勾选:1.点击tag删除的按钮的时候,拿到当前tag的id C;2.执行方法,去除掉A里的C;3.watch事件重新进入到第一步的方法;总结:监听多选框对应的model A,根据A的变化,取到对应的id与name,赋值给tag作展示,tag的删除事件反过来在去控制A的变化,重新进入watch事件里的方法听起来挺简单,思路大概也明确,先讲上述思路对应的代码,后边再讲遇到的问题、坑代码复制整一块代码到你的elementUi项目里就能看到效果<template><div><el-row type=“flex” justify=“bettwen”><el-col :span=“15”><!– 表单 –><el-form :model=“tempForm” ref=“tempForms”><el-form-item label=“请选择人员”><!– 多选人员 –><el-checkbox-group v-model=“tempForm.checkboxGroup5” size=“small”><el-checkbox border v-for="(item,index) in checkBox" @change=“perChange(item)” :label=“item.id” :key=“index”>{{item.name}}</el-checkbox></el-checkbox-group><!– 多选人员 end–></el-form-item></el-form><!– 表单 end–><!– tag展示区 –><el-row><el-tag class=“tagClass” v-for="(tag,index) in tags" :key=“index” closable @close=“handleClose(tag)” :type=“tag.id”>{{tag.name}}</el-tag><el-button v-if=“tags.length>0” @click=“clearAll” plain>全部删除</el-button></el-row><!– tag展示区 end–></el-col></el-row></div></template><script>export default {name: ‘kk’,mounted() {},data() {return {msg: ‘Welcome to Your Vue.js App’,tags: [],tempForm: {checkboxGroup5: [], //选择的人员},detailData: [],checkBox: [{name: ‘小红’,id: ‘101’},{name: ‘小黄’,id: ‘100’}, {name: ‘小明’,id: ‘102’}, {name: ‘小明’,id: ‘102’}],}},methods: {clearAll() { //全部清空数据this.tags = []this.tempForm.checkboxGroup5 = []},perChange(item) {this.detailData.push(item)},handleClose(tag) { //标签的删除事件// 去掉当前删除的taglet yourChoseTags = this.tempForm.checkboxGroup5this.tempForm.checkboxGroup5 = yourChoseTags.filter(item => {if (tag.id !== item) {return true}})},delRepeat(arr) { //数组对象去重return Object.values(arr.reduce((obj, next) => {var key = JSON.stringify(next);return (obj[key] = next), obj;}, {}),);},moreArr() {let yourChose = this.tempForm.checkboxGroup5let tempTags = []tempTags = this.baseDataDetail(yourChose, this.checkBox, tempTags)this.detailData = tempTags},baseDataDetail(yourChose, baseData, callBack) { //封装的数组方法let temp = callBack// 循环两个数据拿到选择的checkbox的id对应的初始数据yourChose.forEach(item => {baseData.forEach(itemSecond => {if (item === itemSecond.id) {temp.push(itemSecond)}})})return temp},},watch: {detailData() {let tempArr = Object.assign([], this.detailData)tempArr = this.delRepeat(tempArr)// console.log(tempArr)this.tags = tempArr},“tempForm.checkboxGroup5” () {this.moreArr()},}}</script><!– Add “scoped” attribute to limit CSS to this component only –><style scoped>.tempArea {/width: 100%;/}.tagClass{margin-right: 10px;}</style>值得注意的点:1.我在多选框绑定值tempForm.checkboxGroup5的监听事件里的方法的最后,得到了一个可能会有重复数据(重复id跟name),再将这个含有重复数据数组对象赋值给另一个数组detailData,在watch监听这个数组,去完重后,赋值给tags做展示。为什么这样做,是因为,我们的需求里,除了在当前页面多选框选择人员,还有一个选择全公司员工的组件,这样不管从哪个渠道选择的人员都能最后将结果指向detailData,保证渲染正确2.数组对象去重,初始数据里可能会有重id、重名的对象(小明),即便绑定多选框的model值里不会有重复的id,但在 利用id取对应name的时候,还是会检测出多条,这样tag就可能会显示重复的所以利用这个方法,就能保证最后处理好的数据没有重复的,tag不会显示多个一样的,但这个方法有点不灵活的地方就是,你要处理的数据({id:1,name:‘小明’,type:now})必须id、name,type都重复的时候,才会被去重,拓展:可根据你设置的数组对象里的某个属性动态去重//数组对象去重:id、name,type都重复的时候,才会被去重delRepeat(arr) {return Object.values(arr.reduce((obj, next) => {var key = JSON.stringify(next);return (obj[key] = next), obj;}, {}),);}//拓展:根据你设置的数组对象里的name属性动态去重baseDel(arr) {const res = new Map();return arr.filter((item) => !res.has(item.name) && res.set(item.name, 1))},3.我一开始是在多选框的change事件上来做tag的展示逻辑,因为change事件里可以同时拿到当前选择的name和id,但是,change的时候,你不知道这是在勾选还是在取消勾选,这样tags的展示就会出问题;这个逻辑可能不太完美,因为有可能你的人员是从其他组件里选来的,所以当你删除tag的时候,会可能出问题(暂时先不讨论这种情况)欢迎大家来指正和补充,或者你的业务需求以及解决方式,撒花 ...

September 24, 2018 · 1 min · jiezi

基于element的区间选择组件

公司的系统中,产品经理在设计时经常要求对某个字段进行区间阈值设置或者作为筛选条件。但是苦于element中没有非常契合的组件(slider组件并不支持两端均能设定),于是自己动手造了一个。成果最终的展示效果如下:需求这里以项目的需求为例,基本的需求如下:分为左右值,包含左右值,正整数左侧必须大于等于1,右侧不得大于100000,右侧值必须不小于左侧默认:左侧20,右侧100000,均必填失焦校验页面和表单校验结构一样:<el-form ref=“form” :model=“form” :rules=“rules”> <el-form-item prop=“min”> <el-input v-model=“form.min” /> </el-form-item> ~ <el-form-item prop=“max”> <el-input v-model=“form.max” /> </el-form-item></el-form>主要思路单个表单校验:必填项校验、正整数校验、区间校验关联校验:右侧阈值不得小于左侧阈值根据上面的思路,单个表单的校验属于公共校验方法,关联校验需要分别校验(因为对比对象不同,且提示语不同),由此在自定义校验中有了如下定义:rules: { min: [ { required: true, message: ‘必填项,请维护’, trigger: ‘blur’ }, { validator: this.validateCom, trigger: ‘blur’ }, { validator: this.validateMin, trigger: ‘blur’ }, ], max: [ { required: true, message: ‘必填项,请维护’, trigger: ‘blur’ }, { validator: this.validateCom, trigger: ‘blur’ }, { validator: this.validateMax, trigger: ‘blur’ }, ],},公共校验方法:正整数校验、区间校验validateCom(rule, value, callback) { const one = Number(value); if (Number.isInteger(one)) { if (one < MIN_NUMBER) { return callback(new Error(输入值必须大于${MIN_NUMBER})); } else if (one > MAX_NUMBER) { return callback(new Error(输入值必须小于${MAX_NUMBER})); } return callback(); } return callback(new Error(‘输入值必须为正整数’));},注意:input输出的始终是字符串类型,需要转换成数字后进行比对关联校验:validateMin(rule, value, callback) { const one = Number(value); const max = Number(this.form.max); if (!max || one < max) { return callback(); } return callback(new Error(‘输入值不得大于最大阈值’));},validateMax(rule, value, callback) { const one = Number(value); const min = Number(this.form.min); if (!min || one > min) { return callback(); } return callback(new Error(‘输入值不得小于最小阈值’));},大概,你会想,这不就完了吗!so easy!现在真正的坑才开始填坑(重点)根据上面的写法,组件的基本功能实现了,但是有一个坑!如下:很显然,左侧值是小于右侧值的,但是校验提示仍然报错。究其原因,还是关联校验的问题。既然是关联交验,改变其中一个时应该会重新校验两个。很简单,在input改变时,重新校验表单不就OK了吗handleChange() { this.$refs.form.validate();}真实表现正如我们所料,但是当我们打开console的时候,会看到Uncaught (in promise) false,这又是什么鬼,身为优秀的前端工程师,你定不会允许自己的代码里报错,即使不影响正常流程。经查证:Promise报错,Uncaught的意思是代表有reject状态没有被catch。究其原因,改变一个值时,校验整个表单时,改变的那个input会执行两次校验,导致异常。最后做如下修改:handleMinChange() { this.$refs.form.validateField(‘max’);},handleMaxChange() { this.$refs.form.validateField(‘min’);},// 并对外暴露操作方法getFormData() { const ret = {}; this.$refs.form.validate((valid) => { ret.valid = valid; ret.form = this.form; }); return ret;},resetForm() { this.$refs.form.resetFields();},总结input表单输出值为String类型,即使设置了type=number关联交验时需要验证其关联项,且不能重复校验源码链接 ...

September 7, 2018 · 1 min · jiezi