独立完成系统开发四:前端功能优化及插件分享

独立完成系统开发四:前端功能优化及插件分享

在开发前端的时候发现系统里面除了权限需要进行改造以外还发现有很多可以优化的地方,并且由于某些需求还发现了很多好用的插件,所以在这里打算将这些东西分享一下。

原有功能优化及改造

前端导出excel

将数据导出为excel在实际中是很常见的需求,导出既可以通过前端导出也可以通过后端导出,之前我最常用的是通过后端导出的方式,在后端通常都是通过poi来进行导出的,而且他不仅可以直接导出还可以基于模板来进行导出,所以即使很复杂的表格也是可以导出来的。

不过我在看vue-element-admin的是否发现作者提供了一种前端导出的方式,官网, 而他的导出是在前端通过js-xlsx来进行导出的,并且他还对js-xlsx进行了封装,封装之后在项目中为Export2Excel.js, 总体来说还是很好用的,而且功能也很强大,不仅支持普通导出还支持多表头的合并之类的功能。

但是我也发现了一个问题,那就是导出的excel木有样式:

独立完成系统开发四:前端功能优化及插件分享_第1张图片

没有样式真滴不怎么好看哈,表头不注意看都区分不出来。

之后我去官网找了一下看看能不能给他加上样式,不过官网上说社区版本不支持样式如果要添加样式得使用专业版本的,而专业版是要收费的。这就尴尬了,难道这个不能用了嘛,后面我通过查找相关资料发现,虽然js-xlsx要专业版本才能够添加样式,不过我们可以使用xlsx-style,他其实也是js-xlsx的一个分支:

独立完成系统开发四:前端功能优化及插件分享_第2张图片

所以xlsx-style和js-xlsx的使用方式是一样的,并且他还支持样式。

然后我就跑去安装,但是安装后启动项目发现项目报错了:

This relative module was not found: ./cptable in ./node_modules/[email protected]@xlsx-style/dist/cpexcel.js 说在xlsx-style/dist/cpexcel.js中找不到cptable。

为什么会报这个错呢,如果你仔细看官方文档会发现这样一段话:

独立完成系统开发四:前端功能优化及插件分享_第3张图片

意思概括就是说cpexcelods模块是可选的,但是这些模块的尺寸很大,并且仅在特殊情况下才需要,因此它们不随核心库一起发布,所以在我们安装的时候他就不会安装这些依赖。并且在实际中我们的确不需要使用也就没有必要安装了,但是怎么处理报错的问题呢。

经过查找相关bug的解决方法以及去官网上找issue,最终找到了两个解决方案,其目的都是将cptable去掉:

  • 我们可以直接修改源码:在\node_modules\xlsx-style\dist\cpexcel.js 807行 的 var cpt = require(’./cpt’ + ‘able’); 改成 var cpt = cptable;

  • 在webpack打包的时候,不对cptable进行打包,这样在项目运行的时候就不会要求提供cptable依赖,就不会报错了。在webpack打包的时候不对某些依赖库进行打包我们可以使用externals配置项进行配置(externals可以在webpack打包的时候将某个模块排除,不让webpack打包)externals相关使用,所以可以在webpack的配置中添加:相关issue ,所以大佬还是很多的哈

    • configureWebpack: {
      	// xlsx-style需要依赖于cptable,但是这个很大而且只有特殊情况才会使用,所以我们可以在打包的时候排除他
          externals: {
            './cptable': 'var cptable'
          }
      }
      

这两种方案,我强烈推荐第二种,因为第一种直接去改源码是很不好的,因为当你改了源码之后,npm中的源码并没有改,当你下次在重新安装依赖那么你又得改一次,很麻烦。而第二种对webpack配置一次就可以了。

改造

这个问题解决之后接下来就是对作者的Export2Excel.js进行改造了在导出的时候给导出的excel添加样式。

具体逻辑不多说了有点多,有兴趣可以自己去研究哈,简单的概括就是在创建单元格的时候将样式加上去。js-xlsx的使用参考官网, 下面直接给出我改造后的Export2Excel.js:

/* eslint-disable */
import { saveAs } from 'file-saver'
// import XLSX from 'xlsx'
import XLSX from 'xlsx-style'

function generateArray(table) {
  var out = [];
  var rows = table.querySelectorAll('tr');
  var ranges = [];
  for (var R = 0; R < rows.length; ++R) {
    var outRow = [];
    var row = rows[R];
    var columns = row.querySelectorAll('td');
    for (var C = 0; C < columns.length; ++C) {
      var cell = columns[C];
      var colspan = cell.getAttribute('colspan');
      var rowspan = cell.getAttribute('rowspan');
      var cellValue = cell.innerText;
      if (cellValue !== "" && cellValue == +cellValue) cellValue = +cellValue;

      //Skip ranges
      ranges.forEach(function (range) {
        if (R >= range.s.r && R <= range.e.r && outRow.length >= range.s.c && outRow.length <= range.e.c) {
          for (var i = 0; i <= range.e.c - range.s.c; ++i) outRow.push(null);
        }
      });

      //Handle Row Span
      if (rowspan || colspan) {
        rowspan = rowspan || 1;
        colspan = colspan || 1;
        ranges.push({
          s: {
            r: R,
            c: outRow.length
          },
          e: {
            r: R + rowspan - 1,
            c: outRow.length + colspan - 1
          }
        });
      };

      //Handle Value
      outRow.push(cellValue !== "" ? cellValue : null);

      //Handle Colspan
      if (colspan)
        for (var k = 0; k < colspan - 1; ++k) outRow.push(null);
    }
    out.push(outRow);
  }
  return [out, ranges];
};

function datenum(v, date1904) {
  if (date1904) v += 1462;
  var epoch = Date.parse(v);
  return (epoch - new Date(Date.UTC(1899, 11, 30))) / (24 * 60 * 60 * 1000);
}

function sheet_from_array_of_arrays(data, headerData, opts) {
  var ws = {};
  var range = {
    s: {
      c: 10000000,
      r: 10000000
    },
    e: {
      c: 0,
      r: 0
    }
  };
  const borderAll = {
    top: {
      style: 'thin'
    },
    bottom: {
      style: 'thin'
    },
    left: {
      style: 'thin'
    },
    right: {
      style: 'thin'
    }
  };
  for (var R = 0; R != data.length; ++R) {
    for (var C = 0; C != data[R].length; ++C) {
      if (range.s.r > R) range.s.r = R;
      if (range.s.c > C) range.s.c = C;
      if (range.e.r < R) range.e.r = R;
      if (range.e.c < C) range.e.c = C;
      var cell = {
        v: data[R][C]
      };
      if (!cell.v){
        cell.v = ''
      }
      var cell_ref = XLSX.utils.encode_cell({
        c: C,
        r: R
      });

      if (typeof cell.v === 'number') cell.t = 'n';
      else if (typeof cell.v === 'boolean') cell.t = 'b';
      else if (cell.v instanceof Date) {
        cell.t = 'n';
        cell.z = XLSX.SSF._table[14];
        cell.v = datenum(cell.v);
      } else cell.t = 's';

      //表头样式设置
      let isHeader
      for(var i=0;i<headerData.length;i++){
        if(headerData[i]!='' && cell.v!='' && headerData[i]==cell.v){
          cell.s = {
            border: borderAll,
            font: {
              sz: 12,           
              bold: true         
            },
            alignment: {
              horizontal: 'center',  
              vertical: 'center'
            },
            fill: {
              fgColor: {
                rgb: 'FFA8A8A8'
              }
            }
          }
          isHeader = true
          headerData.splice(i,1)
          break
        }
      }
      if(!isHeader){
        cell.s = {
          border: borderAll
        }
      }
      ws[cell_ref] = cell;
    }
  }
  if (range.s.c < 10000000) ws['!ref'] = XLSX.utils.encode_range(range);
  return ws;
}

function Workbook() {
  if (!(this instanceof Workbook)) return new Workbook();
  this.SheetNames = [];
  this.Sheets = {};
}

function s2ab(s) {
  var buf = new ArrayBuffer(s.length);
  var view = new Uint8Array(buf);
  for (var i = 0; i != s.length; ++i) view[i] = s.charCodeAt(i) & 0xFF;
  return buf;
}

export function export_table_to_excel(id) {
  var theTable = document.getElementById(id);
  var oo = generateArray(theTable);
  var ranges = oo[1];

  /* original data */
  var data = oo[0];
  var ws_name = "SheetJS";

  var wb = new Workbook(),
    ws = sheet_from_array_of_arrays(data);

  /* add ranges to worksheet */
  // ws['!cols'] = ['apple', 'banan'];
  ws['!merges'] = ranges;

  /* add worksheet to workbook */
  wb.SheetNames.push(ws_name);
  wb.Sheets[ws_name] = ws;

  var wbout = XLSX.write(wb, {
    bookType: 'xlsx',
    bookSST: false,
    type: 'binary'
  });

  saveAs(new Blob([s2ab(wbout)], {
    type: "application/octet-stream"
  }), "test.xlsx")
}

export function export_json_to_excel({
  multiHeader = [],
  header,
  data,
  filename,
  merges = [],
  autoWidth = true,
  //给指定列设置列宽,格式[{ 'colIndex': 列在行中的下标, 'colWidth': 列宽 }, { 'colIndex': 14, 'colWidth': 50 }]
  colsWidth,
  bookType = 'xlsx'
} = {}) {
  /* original data */
  filename = filename || 'excel-list'
  data = [...data]
  data.unshift(header);

  let tmpHearder = [...header];

  for (let i = multiHeader.length - 1; i > -1; i--) {
    data.unshift(multiHeader[i])
    tmpHearder = [...tmpHearder, ...multiHeader[i]]
  }
  tmpHearder = tmpHearder.filter(function(val){
    return val!="";
  });

  var ws_name = "SheetJS";
  var wb = new Workbook(),
  ws = sheet_from_array_of_arrays(data, tmpHearder);

  if (merges.length > 0) {
    if (!ws['!merges']) ws['!merges'] = [];
    merges.forEach(item => {
      ws['!merges'].push(XLSX.utils.decode_range(item))
    })
  }

  if (autoWidth) {
    /*设置worksheet每列的最大宽度*/
    const colWidth = data.map(row => row.map((val, index) => {
      if(colsWidth){
        for(var i=0;i<colsWidth.length;i++){
          if(colsWidth[i].colIndex === index){
            return {
              'wch': colsWidth[i].colWidth
            }
          }
        }
      }
      /*先判断是否为null/undefined*/
      if (val == null) {
        return {
          'wch': 10
        };
      }
      /*再判断是否为中文*/
      else if (val.toString().charCodeAt(0) > 255) {
        return {
          'wch': val.toString().length * 2 + 2
        };
      } else {
        return {
          'wch': val.toString().length + 2
        };
      }
    }))
    /*以第一行为初始值*/
    let result = colWidth[0];
    for (let i = 1; i < colWidth.length; i++) {
      for (let j = 0; j < colWidth[i].length; j++) {
        if (result[j]['wch'] < colWidth[i][j]['wch']) {
          result[j]['wch'] = colWidth[i][j]['wch'];
        }
      }
    }
    ws['!cols'] = result;
  }

   /* add worksheet to workbook */
   wb.SheetNames.push(ws_name);
   wb.Sheets[ws_name] = ws;

  var wbout = XLSX.write(wb, {
    bookType: bookType,
    bookSST: false,
    type: 'binary'
  });
  saveAs(new Blob([s2ab(wbout)], {
    type: "application/octet-stream"
  }), `${filename}.${bookType}`)
}

具体使用方式和作者的Export2Excel.js使用方式一致,不过需要注意的是当有多表头的时候,不管表头是否有跨行跨列,每行表头都得写全,没有数据就直接用空字符串代替。下面是一个实际的调用示例:

handleDownload() {
  this.fullLoading = true
  import('@/vendor/Export2Excel').then(excel => {
    // 如果当前数据的总数大于等于每页数,则说明list中的数据为分页数据,所以需要重新获取所有数据,否则list中存储的就是所有数据
    new Promise((resolve, reject) => {
      if (this.total >= this.listQuery.limit) {
        const tempQuery = Object.assign({}, this.listQuery)
        tempQuery.limit = 99999
        userInfo(tempQuery)
          .then(response => {
          resolve(response.data.result.records)
        })
          .catch(() => {
          this.fullLoading = false
        })
      } else {
        resolve(this.deepClone(this.list)) // 深度clone数组对象
      }
    }).then(list => {
      //多表头
      const multiHeader = [['头部信息', '', '', '', '', '', '', '', '', ''], ['用户名', '1', '2', '3', '4', '5', '6', '7', '8', '9']]
      const tHeader = ['', '账号', '电话', '邮箱', '性别', '状态', '角色', '部门', '修改时间', '备注']
      const filterVal = ['userName', 'userCode', 'phone', 'email', 'sex', 'state', 'roles', 'depts', 'updateTime', 'remark']
      const data = this.formatJson(filterVal, list)
      //跨行跨列
      const merges = ['A1:J1', 'A2:A3']
      excel.export_json_to_excel({
        multiHeader,
        header: tHeader,
        data,
        filename: '用户信息',
        merges
      })
      this.fullLoading = false
      this.notifyMessage()
    })
  })
},
  formatJson(filterVal, jsonData) {
    return jsonData.map(data => filterVal.map(val => {
      if (val === 'sex') {
        data[val] = data[val] === '1' ? '男' : '女'
      } else if (val === 'state') {
        data[val] = data[val] === '1' ? '启用' : '禁用'
      } else if (val === 'roles') {
        data[val] = data[val].map(role => {
          return role.roleName
        }).join(',')
      } else if (val === 'depts') {
        data[val] = data[val].map(dept => {
          return dept.deptName
        }).join(',')
      } else if (val === 'updateTime') {
        data[val] = this.parseTime(data[val], '{y}-{m}-{d} {h}:{i}')
      }
      return data[val]
    }))
  }

这样导出来的效果:
独立完成系统开发四:前端功能优化及插件分享_第4张图片
这样是不是就好看多了呢,哈哈

遮罩图标

在element-ui中默认就提供了Loading 加载组件,使用的时候也很简单直接添加v-loading指令就可以了,虽然他提供了修改加载的图标属性,但是如果我们想使用我们自定义的图标,我们还得将图标添加到element-ui的图标库中,而且在element-ui中引入自定义图标还挺麻烦的。

不过他还提供了给我们自定义类名的属性,因为可以自定义类名,所以我们就可以通过样式来改变遮罩的图标,所以我们就可以通过一张动图来代替遮罩的图标,这样很容易就实现了遮罩图标的自定义了,而且因为可以是动图所以图标的类型就不在仅限于icon了。

例如下面的样式:

<template>
  <div v-loading.fullscreen.lock="fullscreenLoading" class="container" element-loading-background="rgb(0, 0, 0)" element-loading-custom-class="custom-loading-class" element-loading-text="拼命加载中...">
  div>
template>

<style lang="scss">
.custom-loading-class{
  display: flex;
  justify-content: center;
  align-items: center;
  .el-loading-spinner{
    width: 400px;
    height: 400px;
    margin: auto;
    background-image: url("../assets/images.gif");
    background-size: 400px 400px;
    top: unset;
    .circular{
      display: none;
    }
    .el-loading-text{
      bottom: 100px;
      position: absolute;
      text-align: center;
      width: 100%;
      color: #6B8B7E;
      font-size: 16px;
    }
  }
}
style>

其中loading自定义类名为custom-loading-class,还有要注意的是遮罩的背景色要和动图的背景色一致,不然看的会很奇怪

主题切换

在切换主题的时候偶尔会出现主题无法切换,这个问题其实是由于网络引起的。处理这个问题之前我们得先了解主题切换是怎么实现的。

在element中他其实是提供了主题切换的功能的具体可以参考官网 ,不过他给出的方案并不是很灵活只能切换几套事先准备好的主题或者就只能改变一次主题。而vue-element-admin中主题切换方式就很灵活想切换成什么颜色就切换成什么颜色。

vue-element-admin中主题切换的逻辑也挺简单的,因为element-ui的样式由一个统一的css样式文件管理,所以修改主题那么我们只需要获取这份样式然后将里面默认主题颜色对应的一系列颜色值替换为我们所设置的主题颜色所对应的一系列颜色值,那么就生成了一份新的样式,最后在页面上加一个style 标签,把新生成的样式填进去,由于我们的样式在最后加载所以会覆盖之前的样式,这样就实现了主题颜色的切换了,当然这也得益于element样式的规范哈,才可以实现这种骚操作。具体颜色切换可以参考vue-element-admin中的主题切换 或者 element相关issue

然后我们在来说说为什么有时候主题无法切换,首先要想实现主题切换我们需要获取到一份element的原始样式表,然后基于这个样式表进行颜色的替换,而获取这个样式表默认是通过https://unpkg.com/element-ui@${version}/lib/theme-chalk/index.css来获取的(${version}为element的版本)而这个地址有时候会出现访问超时的情况所以有时候就无法获取到element的原始样式这样就导致无法切换样式。

处理这个问题也很容易由于我们不会经常去升级element的版本所以我们可以将他的样式表下载一份放到本地,然后请求的时候直接请求本地的样式文件就可以了,所以我们可以将样式文件下载之后直接放在项目的public 静态资源文件夹下,获取的时候直接请求本地资源就可以了。当然如果使用这种方式当element升级版本后要将对应的样式表更新才行哈

插件分享

下拉树

我们前端项目是在vue-element-admin模板的基础上开发的,而vuel-element-admin他又是基于vue和element-ui开发的,element-ui的组件还是很丰富的,但是在前端的开发中会使用到下拉树,例如在选择部门的时候就有这个需求,不过我却发现在element-ui中竟然木有提供下拉树这个组件,只提供了树和下拉选这些组件。

然后我去element-ui的github中也找了一下,这个需求还是挺普遍的。但是目前element-ui并不支持下拉树,不过后面会添加这组件,相关issue, 所以我们就只能自己去找其他相关的第三方插件了,当然你也可以自己写,直接用他的el-treeel-select组件组合也是可以实现的,不过这个肯定不是最好的选择,毕竟自己写太耗时间了。

然后我去github中找了一下相关的组件,发现了两个比较合适的下拉树:el-tree-select和vue-treeselect

其中el-tree-select是直接基于element-ui开发的,完全跟elementUI兼容,而vue-treeselect则不依赖于element-ui而是直接基于vue开发的。经过对比发现vue-treeselect的功能会强大一下而且star也比较多,但是我们的应用毕竟还是基于element-ui开发的,而且el-tree-select他是通过element-ui中的el-tree和el-select开发的,所以el-tree和el-select中的相关参数和方法都完全支持。最开始我选择了el-tree-select,因为我们是基于element-ui来实现的,而el-tree-select也基于element-ui样式之类都很兼容,并且使用的时候完全可以当做el-tree和el-select的组合来使用,而且element-ui中el-tree和el-select支持的功能是很多的,可以到官网看看,并且由于已经有使用element-UI的基础了所以用起来会简单很多,而使用vue-treeselect还得去好好看一下才行。所以我就选择了el-tree-select,当然如果以后官方提供了下拉树组件还可以在切回去哈。下拉树也不是特别常用,所以切换的时候并不困难。

但是最后我又切换到vue-treeselect ,因为我发现el-tree-select的bug有点多[捂脸],刚使用我发现一个bug提了一个issue处理完了之后,后面我又发现了问题,我不想在踩坑了所以就换成了vue-treeselect,当然作者的处理效率也是很高的哈,也希望vue-treeselect后面的越来越好。

而vue-treeselect的使用其实也挺简单而且功能很强大,具体可以参考官方文档 ,不过由于vue-treeselect使用了一套独立的样式,所以样式看起来跟element有点不是很契合,所以我就对他的样式改了一下有需要的可以参考一下,修改后基本完全兼容element了

下面的样式直接加在全局样式表里面就行:

.vue-treeselect{
  line-height: 22px;
  .vue-treeselect__label{
    font-weight:normal !important;
  }
  .vue-treeselect__control{
    .vue-treeselect__multi-value-item{
      color: #909399;
      background: #f4f4f5;
      border-color: #e9e9eb;
      .vue-treeselect__value-remove{
        color: #909399;
      }
    }
    .vue-treeselect__input-container{
      .vue-treeselect__input{
        font-size: 14px;
      }
    }
  }
}

下拉图标选择

在elementUI中是默认提供了很多icon图标的,不过有时候他提供的图标并不能完全满足我们的需求,所以我们需要引入很多自定义的图标,这些图标可以从iconfont中获取。引入这些自定义图标后为了更方便的使用我们可以将它封装成一个下拉图标选择组件,这样在使用的时候我们只需要引入组件然后就可以直接在页面上选择需要的图标了,而不用自己跑到图标存储的位置一个个去找

组件的具体实现其实也挺简单的,首先我们可以获取到项目中所有的svg,然后拿到每个svg的名称,因此就意味着我们可以将所有的svg图标都显示出来,然后下拉框我们可以基于elementUi的Popover 弹出框实现,在Popover 弹出框中遍历显示所有图标以及添加搜索框。

不过需要注意的是,搜索框中的输入监听事件的触发也就是input事件的触发机制,如果我们在input输入框中使用中文输入法每当我们输入一个字母他都会触发一次并且触发的时候还获取不到输入框中的值。这就很恶心。解决这个问题我们需要通过compositionstart以及compositionend这两个事件来辅助input事件处理中文输入。

  • compositionstart:当浏览器有非直接的文字输入时, compositionstart事件会以同步模式触发。
  • compositionend:当浏览器是直接的文字输入时, compositionend会以同步模式触发

还有经测试发现选词结束的时候input会比compositionend先一步触发,所以要想input事件最后触发那么input事件还得延迟一下才行。

下面是具体组件实现:

<template>
  <div>
    <el-popover  placement="bottom"  width="460"  trigger="click"  @show="reset()" :disabled="disabled">
      <div class="icon-body">
        <el-input v-model="name" style="position: relative;" clearable placeholder="请输入图标名称" @clear="filterIcons" @input.native="filterIcons" @compositionstart.native="inputFlag=false" @compositionend.native="inputFlag=true">
          <i slot="suffix" class="el-icon-search el-input__icon" />
        </el-input>
        <div class="icon-list">
          <div v-for="(item, index) in iconList" :key="index" @click="selectedIcon(item)">
            <svg-icon :icon-class="item" style="height: 30px;width: 16px;" />
            <span>{{ item }}</span>
          </div>
        </div>
      </div>
      <el-input slot="reference" v-model="value" placeholder="点击选择图标" :disabled="disabled" readonly>
        <svg-icon
          v-if="value"
          slot="prefix"
          :icon-class="value"
          class="el-input__icon"
        />
        <i v-else slot="prefix" class="el-icon-search el-input__icon" />
      </el-input>
    </el-popover>
  </div>
</template>

<script>
import icons from './requireIcons'
export default {
  name: 'IconSelect',
  props: {
    // 选中的值
    value: {
      required: true,
      default: undefined
    },
    // 是否禁用
    disabled: {
      type: Boolean,
      required: false,
      default: false
    }
  },
  data() {
    return {
      name: '',
      iconList: icons,
      inputFlag: true
    }
  },
  methods: {
    filterIcons() {
      // 选词结束的时候input会比compositionend先一步触发,此时inputFlag还未调整为true, 并且输入中文时触发input的时候绑定值也未更新,所以需要添加一个延时器
      setTimeout(() => {
        if (this.name) {
          if (this.inputFlag) {
            this.iconList = icons.filter(item => item.includes(this.name))
          }
        } else {
          this.iconList = icons
        }
      }, 0)
    },
    selectedIcon(name) {
      this.$emit('update:value', name)
      document.body.click()
    },
    reset() {
      this.name = ''
      this.iconList = icons
    }
  }
}
</script>

<style lang="scss" scoped>
  .icon-body {
    width: 100%;
    padding: 10px;
    .icon-list {
      height: 200px;
      overflow-y: scroll;
      div {
        height: 30px;
        line-height: 30px;
        margin-bottom: -5px;
        cursor: pointer;
        width: 33%;
        float: left;
      }
      span {
        display: inline-block;
        vertical-align: -0.15em;
        fill: currentColor;
        overflow: hidden;
      }
    }
  }
</style>

获取项目中所有svg图标的名称也就是requireIcons.js

const req = require.context('../../icons/svg', false, /\.svg$/)

const re = /\.\/(.*)\.svg/

const icons = req.keys().map(i => {
  return i.match(re)[1]
})

export default icons

使用的时候直接引入组件然后在组件上添加需要绑定的值就可以了

 <IconSelect  :value.sync="dataform.icon" />

富文本编辑器

富文本编辑器在vue-element-admin中其实已经提供了对应的使用案例,不过他用的是tinymce,而我想要的是一个比较简洁的富文本编辑器并不需要那么复杂的功能,经过对比我选择了wangeditor,这个看起来会简洁很多而且上手也很容易并且由于是国人弄得所以中文文档很完整。为了使用更方便我还将他封装成了一个vue组件,这样使用的时候直接引入就可以了

具体组件代码:

<template>
  <div class="my-editor">
    <div ref="editor" class="editor-wrapper"></div>
  </div>
</template>

<script>
import WEditor from 'wangeditor'
import { getToken } from '@/utils/auth'
import { tokenName } from '@/settings'

// 富文本编辑器
export default {
  name: 'Editor',
  props: {
    value: {
      default: undefined
    },
    isClear: {
      type: Boolean,
      default: false
    },
    disabled: {
      type: Boolean,
      default: false
    }
  },
  data() {
    return {
      editor: undefined,
      editorHtml: undefined
    }
  },
  watch: {
    isClear(val) {
      // 触发清除文本域内容
      if (val) {
        this.editor.txt.clear()
      }
    },
    value(val) {
      if (val !== this.editorHtml) {
        this.editorHtml = val
        if (val) {
          this.editor.txt.html(val)
        } else {
          this.editor.txt.clear()
        }
      }
    },
    disabled(val) {
      this.editor.$textElem.attr('contenteditable', !this.disabled)
    }
  },
  mounted() {
    this.initEditor()
  },
  methods: {
    getText() {
      this.editor.txt.text()
    },
    setText(val) {
      this.editor.txt.html(val)
    },
    getHtml() {
      this.editor.txt.html()
    },
    initEditor() {
      this.editor = new WEditor(this.$refs.editor)
      this.editor.customConfig.uploadImgShowBase64 = false // base 64 存储图片
      this.editor.customConfig.showLinkImg = false// 关闭通过图片地址添加图片
      this.editor.customConfig.uploadImgServer = process.env.VUE_APP_BASE_API + '/editor/editorUploadImage' // 配置上传图片服务器地址
      const header = {}
      header[tokenName] = getToken()
      this.editor.customConfig.uploadImgHeaders = header // 自定义 header
      this.editor.customConfig.uploadFileName = 'image' // 后端接受上传文件的参数名
      this.editor.customConfig.uploadImgMaxSize = 5 * 1024 * 1024 // 将图片大小限制为 5M
      this.editor.customConfig.uploadImgMaxLength = 5 // 限制一次最多上传 5 张图片
      this.editor.customConfig.uploadImgTimeout = 60 * 1000 // 设置超时时间
      this.editor.customConfig.emotions = [
        {
          // tab 的标题
          title: '表情',
          // type -> 'emoji' / 'image'
          type: 'emoji',
          content: [
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '',
            '☝',
            '',
            '✌',
            '',
            '',
            '',
            '',
            '✊',
            '',
            '',
            '',
            '',
            '',
            '',
            ''
          ]
        }
      ]
      // 配置菜单 可根据文档进行添加
      this.editor.customConfig.menus = [
        'head', // 标题
        'bold', // 粗体
        'fontSize', // 字号
        'fontName', // 字体
        'italic', // 斜体
        'underline', // 下划线
        'strikeThrough', // 删除线
        'foreColor', // 文字颜色
        'backColor', // 背景颜色
        'link', // 插入链接
        'list', // 列表
        'justify', // 对齐方式
        'quote', // 引用
        'emoticon', // 表情
        'image', // 插入图片
        'table', // 表格
        // 'video',  // 插入视频
        'code', // 插入代码
        'undo', // 撤销
        'redo' // 重复
      ]

      this.editor.customConfig.uploadImgHooks = {
        fail: (xhr, editor, result) => {
          // 图片上传并返回结果,但图片插入错误时触发
          this.$message({
            message: '图片插入失败!',
            type: 'error'
          })
        },
        error: function(xhr, editor) {
          // 图片上传出错时触发
          // xhr 是 XMLHttpRequst 对象,editor 是编辑器对象
          this.$message({
            message: '图片上传失败!',
            type: 'error'
          })
        },
        timeout: (xhr, editor) => {
          // 图片上传超时时触发
          this.$message({
            message: '图片上传超时,请检查网络!',
            type: 'error'
          })
        },
        customInsert: (insertImg, data, editor) => {
          const url = process.env.VUE_APP_BASE_API + data.result.url
          insertImg(url)
        }
      }
      // 将内容同步到父组件中
      this.editor.customConfig.onchange = html => {
        this.editorHtml = html
        this.$emit('update:value', html)
      }
      this.editor.customConfig.zIndex = 2 // 配置富文本的权重 不然会覆盖其他组件
      // 创建富文本编辑器
      this.editor.create()
      // 初始化值
      if (this.value) {
        this.editorHtml = this.value
        this.editor.txt.html(this.value)
      }
      this.editor.$textElem.attr('contenteditable', !this.disabled)
    }
  }
}
</script>

<style lang="scss" scoped>
.my-editor {
  .editor-wrapper {
    text-align: left;
  }
}
</style>

代码高亮

有时候我们可能需要在页面中展示代码,通常展示代码我们只需要通过

代码
这样来展示代码就可以了,这样会保留代码的格式,不过这样虽然可以正常展示但是代码是没有样式的,看起来感觉不是很好。经过查找我发现highlight 很不错而且他还支持很多种主题(具体有那些安装后可以到node_modules/highlight/styles下面去看)并且使用非常简单,所以我就选择了这个作为代码高亮,同时为了更方便使用我将它封装成了一个指令,需要高亮的代码直接添加指令就行了

具体代码:

import hljs from 'highlight.js'
// import 'highlight.js/styles/a11y-dark.css' // 样式文件
// import 'highlight.js/styles/atelier-savanna-dark.css' // 样式文件
import 'highlight.js/styles/atom-one-dark-reasonable.css' // 样式文件
// import 'highlight.js/styles/hybrid.css' // 样式文件
// import 'highlight.js/styles/lioshi.css' // 样式文件
export default {
  inserted(el, binding, vnode) {
    const blocks = el.querySelectorAll('pre code')
    blocks.forEach((block) => {
      hljs.highlightBlock(block)
    })
  }
}

使用示例:

<div v-highlight>
  <pre class="previewCodes"><code>{{value}}code>pre>
div>

vue-echarts

echarts默认是直接在js中使用的,使用的时候需要指定对应的dom然后进行初始化就行了,在vue中为了更方便的使用我们通常需要将echarts封装成组件,不过自己一个个封装有点麻烦,所以可以直接使用vue-echarts,导入vue-echarts之后我们就可以直接通过组件的方式来使用echarts了。而vue-echarts的使用和直接使用echarts基本一致,并且我们可以少写很多代码。具体的使用可以参考vue-echarts以及echarts官网。

在使用vue-echarts的时候我们可以按需引入ECharts 各模块来减小打包体积,按需导入组件我们第一个要导入的是import ECharts from 'vue-echarts',然后我们用到什么组件在导入什么组件。那么我们到底可以手动引入那些组件呢,这个在vue-echarts哪里他也没有说,我还找了很久最后还是在官网上找到的,可以导入的组件参考ECharts 按需引入模块

地图

地图我用的是高德地图AMap,但是并没有使用原生的AMap而是经过vue封装的vue-amap ,具体怎么用大家可以参考官方文档,官网上写的已经很清楚了。不过有个问题我要提一下,那就是vue-amap的官方文档总是访问不了,因为他的服务器部署在国外dns的解析好像有问题。这种情况和github上的图片加载不出来一样。

处理方法是手动在本地的hosts文件中添加上下面的映射

185.199.108.153    elemefe.github.io
185.199.109.153    elemefe.github.io
185.199.110.153    elemefe.github.io
185.199.111.153    elemefe.github.io

手动添加映射之后就可以正常访问了,而获取某个域名所对应的ip可以到IPAddress.com这个网站上去查询。具体操作可以参考这篇博客

项目地址:github 、gitee、演示环境(账号/密码:admin/123456)

上一篇:独立完成系统开发三:前端权限改造

下一篇:独立完成系统开发五:mybatis-plus及代码生成器使用

你可能感兴趣的:(独立完成系统开发,vue,js,前端,javascript,css)