Vue中使用froala富文本编辑器制作打印模板

Posted lee576

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Vue中使用froala富文本编辑器制作打印模板相关的知识,希望对你有一定的参考价值。

参考上一篇 知识开发的一个功能,制作一个打印模板的管理模块,如下(就是保存froala编辑后的html文本,其中包括Vue的Template,这样我们可以利用Vue的模板的优势来动态绑定一些数据源进行HTML的打印,基本上跟过去水晶报表做一个模板再绑定数据源的方法异曲同工)

 在 main.js 里引用 froala 组件

// Import and use Vue Froala lib.
import VueFroala from 'vue-froala-wysiwyg'

// 引入 Froala Editor js file.
require('froala-editor/js/froala_editor.pkgd.min')
// 引入中文语言包
require('froala-editor/js/languages/zh_cn')
// 引入 Froala Editor css files.
require('froala-editor/css/froala_editor.pkgd.min.css')
require('font-awesome/css/font-awesome.css')
require('froala-editor/css/froala_style.min.css')

// 批量导入 froala-editor 插件文件
function importAll (r) 
  r.keys().forEach(r)

importAll(require.context('froala-editor/js/plugins/', false, /\\.js$/))
importAll(require.context('froala-editor/css/plugins/', false, /\\.css$/))

Vue.use(VueFroala)

以上基本是引用了所有的插件,不然工具栏会有很多的按钮不出来

然后把 froala 文本编辑器封装成一个Vue组件,只暴露我们想要的功能

<template>
  <div class="editor-wrap">
    <froala
      :tag="'textarea'"
      :config="config"
      v-model="body.innerHTML"
    />
  </div>
</template>
<script>
import FroalaEditor from 'froala-editor'
export default 
  props: 
    // 显示的工具列表
    placeholder: 
      type: String
    ,
    height: 
      type: Number
    ,
    value: 
      type: String,
      default: null
    ,
    index: 
      type: Number,
      default: 1
    ,
    buttons: 
      type: Array,
      default: () => [
        'undo',
        'redo',
        'clearFormatting',
        'bold',
        'italic',
        'underline',
        'strikeThrough',
        'fontFamily',
        'fontSize',
        'textColor',
        'color',
        'backgroundColor',
        'inlineStyle',
        'paragraphFormat',
        'align',
        'formatOL',
        'formatUL',
        'outdent',
        'indent',
        'quote',
        // 'insertLink',
        'insertImage',
        // 'insertVideo',
        // 'embedly',
        // 'insertFile',
        'insertTable',
        // 'emoticons',
        'specialCharacters',
        'insertHR',
        'selectAll',
        'print',
        'spellChecker',
        'html',
        'help',
        'fullscreen',
        'clear',
        'save'
      ]
    
  ,
  data () 
    const that = this
    return 
      editor: null,
      uploadImage: 
        loading: false,
        previewVisible: false,
        previewImage: '',
        imgFile: ,
        isSize: false,
        isType: false
      ,
      fileList: [],
      body: 
        innerHTML: this.value,
        textLen: 0,
        leftoverLen: 99999999999,
        sumLen: 999999999,
        error_tip: '',
        error_show: false
      ,
      config: 
        codeBeautifierOptions: 
          end_with_newline: true,
          indent_inner_html: true,
          extra_liners: "['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'pre', 'ul', 'ol', 'table', 'dl']",
          brace_style: 'expand',
          indent_char: ' ',
          indent_size: 4,
          wrap_line_length: 0
        ,
        htmlAllowedAttrs: ['.*'],
        toolbarButtons: this.buttons,
        // theme: "dark", //主题
        placeholderText: this.placeholder || '编辑文本',
        language: 'zh_cn', // 国际化
        imageUploadURL: '', // 上传url
        imageUploadParams:  token: '', i: '', ak: '', f: 9 ,
        fileUploadURL: '',
        fileUploadParams:  token: '', i: '', ak: '', f: 9 ,
        videoUploadURL: '',
        videoUploadParams:  token: '', i: '', ak: '', f: 9 ,
        quickInsertButtons: ['image', 'table', 'ul', 'ol', 'hr'], // 快速插入项
        // toolbarVisibleWithoutSelection: true,//是否开启 不选中模式
        // disableRightClick: true,//是否屏蔽右击
        colorsHEXInput: true, // 关闭16进制色值
        toolbarSticky: false, // 操作栏是否自动吸顶,
        // Colors list.
        colorsBackground: [
          '#15E67F',
          '#E3DE8C',
          '#D8A076',
          '#D83762',
          '#76B6D8',
          'REMOVE',
          '#1C7A90',
          '#249CB8',
          '#4ABED9',
          '#FBD75B',
          '#FBE571',
          '#FFFFFF'
        ],
        colorsStep: 6,
        colorsText: [
          '#15E67F',
          '#E3DE8C',
          '#D8A076',
          '#D83762',
          '#76B6D8',
          'REMOVE',
          '#1C7A90',
          '#249CB8',
          '#4ABED9',
          '#FBD75B',
          '#FBE571',
          '#FFFFFF'
        ],
        zIndex: 2501,
        height: this.height || '250',
        // autofocus: true,
        events: 
          initialized: function () 
            that.editor = this
            that.body.innerHTML = that.value
            that.editorChange()
          ,
          blur: () => 
            that.$emit('blur')
          ,
          contentChanged: () => 
            that.editorChange()
          ,
          'image.beforeUpload': function (images) 
            // 自定义上传图片
            that.beforeUpload(images[0])
            return false
          ,
          'file.beforeUpload': function () 
            // Image was uploaded to the server.
            return true
          ,
          'video.beforeUpload': function () 
            // Image was uploaded to the server.
            return true
          
        
      
    
  ,
  watch: 
    value: 
      handler: function (news, old) 
        this.body.innerHTML = news
      ,
      deep: true
    ,
    'body.innerHTML': function (newVal, old) 
      if (old !== newVal) 
        let val = this.getSimpleText(this.editor.html.get(true))
        this.editor.html.set(newVal)
      
    ,
    label: function (newVal, old) 
      if (old !== newVal) 
        this.editor.html.set(newVal)
      
    
  ,
  mounted () 
    // 自定义按钮***********************************************************************
    // 清理
    FroalaEditor.DefineIcon('clear', NAME: 'remove', SVG_KEY: 'remove')
    FroalaEditor.RegisterCommand('clear', 
      title: '清空',
      focus: false,
      undo: true,
      refreshAfterCallback: true,
      callback: function () 
        this.html.set(null)
        this.events.focus()
      
    )
    // 保存
    FroalaEditor.DefineIconTemplate('material_design', '<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="floppy-disk" class="svg-inline--fa fa-floppy-disk" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M433.1 129.1l-83.9-83.9C342.3 38.32 327.1 32 316.1 32H64C28.65 32 0 60.65 0 96v320c0 35.35 28.65 64 64 64h320c35.35 0 64-28.65 64-64V163.9C448 152.9 441.7 137.7 433.1 129.1zM224 416c-35.34 0-64-28.66-64-64s28.66-64 64-64s64 28.66 64 64S259.3 416 224 416zM320 208C320 216.8 312.8 224 304 224h-224C71.16 224 64 216.8 64 208v-96C64 103.2 71.16 96 80 96h224C312.8 96 320 103.2 320 112V208z"></path></svg>')
    FroalaEditor.DefineIcon('saveIcon', NAME: 'save', template: 'material_design')
    FroalaEditor.RegisterCommand('save', 
      title: '保存',
      icon: 'saveIcon',
      callback: () => 
        this.$emit('save', this.body.innerHTML)
      
    )
    // **********************************************************************************
    setTimeout(() => 
      this.setIndex(this.index)
    , 200)
  ,
  methods: 
    saveHtml () 
      this.$emit('save', this.body.innerHTML)
    ,
    // 更改富文本层级
    setIndex (val) 
      this.$nextTick(() => 
        let dv = document.getElementsByClassName('fr-box')
        for (let i in dv) 
          if (!dv[i].style) 
            return
          
          dv[i].style.cssText = 'z-index:' + val
        
      )
    ,
    // 富文本中提取纯文本
    getSimpleText: html => 
      var re1 = new RegExp('<p data-f-id="pbf".+?</p>', 'g') // 匹配html标签的正则表达式,"g"是搜索匹配多个符合的内容
      var msg = html.replace(re1, '') // 执行替换成空字符
      return msg
    ,
    editorChange () 
      if (this.editor == null) return
      this.$emit('change', this.body.innerHTML)
    ,
    beforeUpload (file) 
      this.uploadImage.loading = true
      const formData = new FormData()
      formData.append('formFile', file)
      // this.$store
      //   .dispatch('UploadImg', formData)
      //   .then(res => 
      //     this.uploadImage.loading = false
      //     if (res.code === 200) 
      //       this.uploadImage.imgFile = JSON.parse(res.data)
      //       const url = this.uploadImage.imgFile.data
      //       // 插入图片
      //       this.editor.html.insert(
      //         '<img src=' + this.uploadImage.imgFile.data + '>', // HTML
      //         false // 在插入之前是否应清除HTML
      //       ) // 插入图片
      //      else 
      //       this.fileList = []
      //       this.$message.error(res.msg)
      //     
      //   )
      //   .catch(() => 
      //     this.uploadImage.loading = false
      //     this.$message.error('上传失败')
      //   )
    
  


</script>
<style lang="less" scoped>
.editor-wrap 
  width: 100%;
  height: calc(100% - 30px);
  // 去掉底部工具栏logo
  /deep/ .fr-second-toolbar 
    height: 42.19px;
    #fr-logo >span 
      display: none;
    
    #Layer_1 
      display: none;
    
  
  /deep/ .fr-box 
    height: 100% !important;
    .fr-wrapper 
      height: calc(100% - 93px) !important;
    
  
  /deep/ .fr-code 
    height: 100% !important;
  


.fr-wrapper > div[style*="z-index:9999;"],
.fr-wrapper > div[style*="z-index: 9999;"] 
  height: 0;
  overflow: hidden;
  position: absolute;
  top: -1000000px;
  opacity: 0;

.fr-view
  position: absolute;
  top: 0;

.fr-placeholder
  margin-top: 0;

.fr-box .second-toolbar 
  display: none;

.fr-box .second-toolbar #logo 
  width: 0 !important;
  height: 0 !important;
  overflow: hidden;

.fr-box .fr-toolbar 
  border-radius: 4px 4px 0 0;
  border-color: #dcdfe6;

.fr-box .second-toolbar 
  border-radius: 0 0 4px 4px;
  border-color: #dcdfe6;

.fr-box .fr-wrapper 
  border-color: #dcdfe6 !important;

</style>
<style>
.fa-floppy-disk 
  width: 18px;
  height: 18px;

</style>

具体的用法还要参详一下 froala 官方文档,这里不细表了

后端设计一张数据库表用来保存 html 文本

下面使用刚刚上面封装好的组件对html模板进行保存和更新

<template>
  <div class="page">
    <SplitPane direction="column" :min="10" :max="90" :triggerLength="10" :paneLengthPercent.sync="paneLengthPercent">
     <template v-slot:one>
      <div style="height:100%">
        <div style="height:calc(100% - 40px)">
          <el-table
            stripe
            element-loading-text="数据正在加载中"
            highlight-current-row
            size="small"
            ref="multipleTable"
            border
            :data="tableData"
            height="100%"
            :cell-style=" 'text-align': 'center' "
            :header-cell-style=" 'text-align': 'center' "
            v-on:select="handleSelectedRow" v-on:row-click="handleSelectedRow"
            @cell-click="cellClick">
            <el-table-column type="index" width="48"></el-table-column>
            <el-table-column label="模板名称" width="350px" prop='template_name'>
              <template slot-scope="scope">
                <el-input v-if="tableData.indexOf(scope.row)  === rowIndex && hearderTitle === '模板名称'"
                  v-model="scope.row.template_name" placeholder="请填写模板名称">
                </el-input>
                <span v-else> scope.row.template_name </span>
              </template>
            </el-table-column>
            <template  v-for="item in headerGroup">
              <el-table-column
                :key="item.label"
                v-if="item.show"
                :label="item.label"
                :prop="item.prop"
                :width="item.width"
              ></el-table-column>
            </template>
            <el-table-column label="操作" width="120" fixed="right">
              <template slot="header">
                <el-popover
                  placement="top-start"
                  width="30"
                  trigger="hover"
                  content="点击添加一行">
                  <el-button size="mini" class='el-buttons' type="primary" icon="el-icon-plus" circle slot="reference" @click="renderAddRow"></el-button>
                </el-popover>
              </template>
              <template slot-scope="scope">
                <el-popover width="160" :ref="`popover-$scope.$index`" placement="top">
                  <p>确定删除该项吗?</p>
                  <div style="text-align: right; margin: 0">
                    <el-button type="text" size="mini" @click="scope._self.$refs[`popover-$scope.$index`].doClose()">
                      取消
                    </el-button>
                    <el-button type="danger" size="mini" @click="deleteRow(scope.$index, scope.row, scope._self.$refs[`popover-$scope.$index`])">确定</el-button>
                  </div>
                  <el-button type="danger" class='el-buttons' style="background-color:#F56C6C" slot="reference" icon="el-icon-delete" size="mini" circle></el-button>
                </el-popover>
                <el-popover
                  placement="top-start"
                  width="80px"
                  trigger="hover"
                  content="点击保存这一行">
                  <el-button size="mini" class='el-buttons' style="background-color:#909399" type="info" icon="el-icon-edit" circle slot="reference" @click="addOrUpdateRow(scope.row)"></el-button>
                </el-popover>
              </template>
            </el-table-column>
          </el-table>
        </div>
        <div style="height:40px;padding:5px;">
          <el-pagination
            background
            layout="total, sizes, prev, pager, next"
            :total="GridPageRequest.PageTotal"
            @size-change="handleSizeChange"
            @current-change="handleCurrentChange"
            :page-sizes="[20, 30, 50, 100]"
            :page-size="GridPageRequest.PageSize"
            :current-page.sync="GridPageRequest.PageIndex">
          </el-pagination>
        </div>
      </div>
     </template>
     <template v-slot:two>
       <editor v-model="templateHtml" @save="saveTemplate" @change="changeTemplate"></editor>
     </template>
    </SplitPane>
  </div>
</template>
<script>
import SplitPane from '@/components/draglayhorizontallayouter/SplitPane.vue'
import Editor from '@/components/print/editor.vue'
export default 
  components:  SplitPane, Editor ,
  data () 
    return 
      templateName: null,
      rowIndex: 0,
      hearderTitle: '',
      paneLengthPercent: 40,
      // 表格
      tableData: [],
      // 表头
      headerGroup: [
         label: '主键ID', prop: 'id', show: false, width: '160px' ,
         label: '创建人', prop: 'create_by_name', show: true, width: '160' ,
         label: '创建人', prop: 'create_by', show: false, width: '160' ,
         label: '创建时间', prop: 'create_time', show: true, width: '160' ,
         label: '修改人', prop: 'update_by_name', show: true, width: '160' ,
         label: '修改人', prop: 'update_by', show: false, width: '160' ,
         label: '修改时间', prop: 'update_time', show: true, width: 'auto' 
      ],
      // 分页
      GridPageRequest: 
        PageSize: 20,
        PageIndex: 0,
        PageTotal: 0
      ,
      selectedRows: [],
      templateHtml: ''
    
  ,
  mounted () 
    this.$nextTick(async () => 
      this.onSearch()
    )
  ,
  methods: 
    // 选择行
    handleSelectedRow (selection, row) 
      this.templateHtml = ''
      this.selectedRows = []
      if (Array.isArray(selection)) 
        for (let i = 0; i < selection.length; i++) 
          this.selectedRows.push(selection[i])
        
       else  this.selectedRows.push(selection) 
      this.templateHtml = this.selectedRows[0].template_html
    ,
    cellClick (row, column, cell, event) 
      this.rowIndex = this.tableData.indexOf(row)
      this.hearderTitle = column.label

      this.templateHtml = ''
      this.selectedRows = []
      if (Array.isArray(row)) 
        for (let i = 0; i < row.length; i++) 
          this.selectedRows.push(row[i])
        
       else  this.selectedRows.push(row) 
      this.templateHtml = this.selectedRows[0].template_html
    ,
    renderAddRow () 
      this.tableData.push( template_name: null )
    ,
    addOrUpdateRow (row) 
      if (!row.id) 
        row.create_by = localStorage.getItem('user')
       else 
        row.update_by = localStorage.getItem('user')
      
      if (!row.template_name || row.template_name.length === 0) 
        this.$message.error('请先填写模板名称!')
        return
      
      this.axios.post('BasePrintTemplate/AddOrUpdate', row)
        .then((response) => 
          this.$message.success('操作成功!')
          this.getTableData()
        )
        .catch((error) => 
          this.$message(
            message: error.response.data.Message,
            type: 'warning'
          )
        )
    ,
    deleteRow (index, row, _self) 
      // 关闭气泡提示
      _self.doClose()
      if (!row.id) 
        this.tableData.splice(this.tableData.indexOf(row), 1)
        this.$message.success('操作成功!')
        return
      
      this.axios.post('BasePrintTemplate/Delete', row)
        .then((response) => 
          if (response.data.Data) 
            this.$message.success('操作成功!')
            this.tableData.splice(this.tableData.indexOf(row), 1)
           else 
            this.$message.error('操作失败!')
          
        )
        .catch((error) => 
          this.$message(
            message: error.response.data.Message,
            type: 'warning'
          )
        )
    ,
    getTableData () 
      this.GridPageRequest.Filter = []
      if (this.templateName != null) 
        this.GridPageRequest.Filter.push(FieldName: 'template_name', ConditionalType: '15', FieldValue: this.templateName)
      
      this.axios
        .post('BasePrintTemplate/QueryTake', this.GridPageRequest)
        .then((response) => 
          this.tableData = response.data.Data
          this.GridPageRequest.PageTotal = response.data.TotalRows
          this.radio = ''
        )
        .catch((error) => 
          this.$message(
            message: error.response.data.Message,
            type: 'warning'
          )
        )
    ,
    onSearch () 
      this.getTableData()
    ,
    handleSizeChange (val) 
      this.GridPageRequest.PageSize = val
      this.onSearch()
    ,
    handleCurrentChange (val) 
      this.GridPageRequest.PageIndex = val
      this.onSearch()
    ,
    changeTemplate (template) 
      this.templateHtml = template
    ,
    saveTemplate () 
      if (this.selectedRows.length === 0) 
        this.$message(
          message: '请选择一行数据再保存!',
          type: 'warning'
        )
        return
      
      this.selectedRows[0].template_html = this.templateHtml
      this.axios.post('BasePrintTemplate/AddOrUpdate', this.selectedRows[0])
        .then((response) => 
          this.$message.success('操作成功!')
          this.getTableData()
        )
        .catch((error) => 
          this.$message(
            message: error.response.data.Message,
            type: 'warning'
          )
        )
    
  


</script>
<style lang="less" scoped>
.page 
  height: 100%;
  /deep/ .el-table .el-table__fixed-right .el-table__fixed-header-wrapper .el-table__cell 
    text-align: right !important;
  

</style>
<style src='@/css/el-buttons.css' scoped>
</style>

 绑定Vue的数据源,打印预览效果如下

 

以上是关于Vue中使用froala富文本编辑器制作打印模板的主要内容,如果未能解决你的问题,请参考以下文章

vue使用froala-editor富文本编辑器

vue-froala-wysiwyg 富文本编辑器

VUE使用富文本自定义打印模板

tinymce-vue5富文本的实现

tinymce-vue5富文本的实现

froala富文本编辑器与golangbeego,脱离ueditor苦海