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富文本编辑器制作打印模板的主要内容,如果未能解决你的问题,请参考以下文章