当前位置: 首页 > news >正文

vue项目集成CanvasEditor实现Word在线编辑器

CanvasEditor实现Word在线编辑器
官网文档:https://hufe.club/canvas-editor-docs/guide/schema.html
源码地址:https://github.com/Hufe921/canvas-editor

在这里插入图片描述

前提声明:
由于CanvasEditor目前不支持vue、react
等框架开箱即用版,所以需要我们去Git下载源码,拿到其中两个主要文件,集成到我们自己的项目中

第一步:项目安装CanvasEditor

在你需要集成编辑器的项目中,安装CanvasEditor

npm i CanvasEditor

第二步:主要文件集成

1、下载源码,目录如下:红色框就是我们需要在自己项目中引用的
在这里插入图片描述

2、在你的vue项目项目中,新建一个文件夹,叫CanvasEditor,这个文件夹内,就放集成的相关文件(代码以及目录结构贴在后边)

  1. 新建个index.vue文件,作为后边集成封装的CanvasEditor组件引入的入口
  2. index.vuetemplate的html部分,就把CanvasEditor源码的html文件中粘过来
  3. 结合文档,把main.ts相关的工具函数内容放到你的index.vuescriptmethod部分(某些引入报错就把相关报错的文件找到),并引入到自己的CanvasEditor文件夹下(由于我的项目用的js所以我复用main.ts的相关方法时转换为了js)
  4. 新建componments文件夹,把源码中的dialogsignature文件夹放到里面(记得文件引用的assets文件粘到自己项目中,且更改引用路径,否则会报错)
  5. option.js是参照官网的相关样式默认配置
  6. style.css也是源码同名文件粘过来的
  7. 缺失的相关静态资源,根据报错从源码中粘过来即可,
  8. 封装完成后,自定义传值、调用接口等逻辑,就根据自己需要自主发挥吧

集成组件目录结构:如图
在这里插入图片描述

index.vue文件如下:

说明:
1、有很多注释的代码,是因为我不需要而已,自己需要的话 自己放开注释即可;
2、为了更好的作为子组件引入,我更改了样式在style标签内,可自行根据需要修改
3、由于我的项目用的js所以我复用main.ts的相关方法时转换为了js

 
<style lang="scss" scoped>
.footer {position: static;
}
.menu {position: absolute;top: 0;left: 0;
}
.el-footer {background-color: #f2f4f7;text-align: center;line-height: 30px!important; /* 调整footer高度 */
}
.content {position: static;display: flex;flex-direction: column;height: calc(100% - 90px); /* 减去header和footer的高度 */background-color: #f2f4f7;overflow-y: auto; /* 仅让Main区域可滚动 */flex-grow: 1; /* 让Main区域占据剩余空间 */
}
@media (max-width: 1220px) {.menu {display: flex;align-items: center;flex-wrap: wrap;justify-content: flex-start;}
}
#canvasEditor {display: flex;justify-content: center;background: #f2f4f7;
}
.el-header {position: relative;
}
.canvas-container {height: 100%;margin-top: 10px;
}
@import url('./components/dialog/dialog.css');
@import url('./components/signature/signature.css');
@import url('./style.css');</style>
<template><div class="canvas-container"><el-container style="height: 100%;"><el-header><div class="menu" editor-component="menu"><div class="menu-item"><div class="menu-item__save" title="保存"><i class="el-icon-s-claim" style="color:#646464"></i></div><div class="menu-item__undo"><i></i></div><div class="menu-item__redo"><i></i></div><div class="menu-item__painter" title="格式刷(双击可连续使用)"><i></i></div><div class="menu-item__format" title="清除格式"><i></i></div></div><div class="menu-divider"></div><div class="menu-item"><div class="menu-item__font"><span class="select" title="字体">微软雅黑</span><div class="options"><ul><li data-family="Microsoft YaHei" style="font-family:'Microsoft YaHei';">微软雅黑</li><li data-family="宋体" style="font-family:'宋体';">宋体</li><li data-family="黑体" style="font-family:'黑体';">黑体</li><li data-family="仿宋" style="font-family:'仿宋';">仿宋</li><li data-family="楷体" style="font-family:'楷体';">楷体</li><li data-family="等线" style="font-family:'等线';">等线</li><!-- <li data-family="华文琥珀" style="font-family:'华文琥珀';">华文琥珀</li><li data-family="华文楷体" style="font-family:'华文楷体';">华文楷体</li><li data-family="华文隶书" style="font-family:'华文隶书';">华文隶书</li><li data-family="华文新魏" style="font-family:'华文新魏';">华文新魏</li><li data-family="华文行楷" style="font-family:'华文行楷';">华文行楷</li><li data-family="华文中宋" style="font-family:'华文中宋';">华文中宋</li><li data-family="华文彩云" style="font-family:'华文彩云';">华文彩云</li> --><li data-family="Arial" style="font-family:'Arial';">Arial</li><li data-family="Segoe UI" style="font-family:'Segoe UI';">Segoe UI</li><li data-family="Ink Free" style="font-family:'Ink Free';">Ink Free</li><li data-family="Fantasy" style="font-family:'Fantasy';">Fantasy</li></ul></div></div><div class="menu-item__size"><span class="select" title="字体">小四</span><div class="options"><ul><li data-size="56">初号</li><li data-size="48">小初</li><li data-size="34">一号</li><li data-size="32">小一</li><li data-size="29">二号</li><li data-size="24">小二</li><li data-size="21">三号</li><li data-size="20">小三</li><li data-size="18">四号</li><li data-size="16">小四</li><li data-size="14">五号</li><li data-size="12">小五</li><li data-size="10">六号</li><li data-size="8">小六</li><li data-size="7">七号</li><li data-size="6">八号</li></ul></div></div><div class="menu-item__size-add"><i></i></div><div class="menu-item__size-minus"><i></i></div><div class="menu-item__bold"><i></i></div><div class="menu-item__italic"><i></i></div><div class="menu-item__underline"><i></i></div><div class="menu-item__strikeout" title="删除线(Ctrl+Shift+X)"><i></i></div><div class="menu-item__superscript"><i></i></div><div class="menu-item__subscript"><i></i></div><div class="menu-item__color" title="字体颜色"><i></i><span></span><input type="color" id="color" /></div><div class="menu-item__highlight" title="高亮"><i></i><span></span><input type="color" id="highlight"></div></div><div class="menu-divider"></div><div class="menu-item"><div class="menu-item__title"><i></i><span class="select" title="切换标题">正文</span><div class="options"><ul><li style="font-size:16px;">正文</li><li data-level="first" style="font-size:26px;">标题1</li><li data-level="second" style="font-size:24px;">标题2</li><li data-level="third" style="font-size:22px;">标题3</li><li data-level="fourth" style="font-size:20px;">标题4</li><li data-level="fifth" style="font-size:18px;">标题5</li><li data-level="sixth" style="font-size:16px;">标题6</li></ul></div></div><div class="menu-item__left"><i></i></div><div class="menu-item__center"><i></i></div><div class="menu-item__right"><i></i></div><div class="menu-item__alignment"><i></i></div><div class="menu-item__row-margin"><i title="行间距"></i><div class="options"><ul><li data-rowmargin='1'>1</li><li data-rowmargin="1.25">1.25</li><li data-rowmargin="1.5">1.5</li><li data-rowmargin="1.75">1.75</li><li data-rowmargin="2">2</li><li data-rowmargin="2.5">2.5</li><li data-rowmargin="3">3</li></ul></div></div><div class="menu-item__list"><i></i><div class="options"><ul><li><label>取消列表</label></li><li data-list-type="ol" data-list-style='decimal'><label>有序列表:</label><ol><li>________</li></ol></li><li data-list-type="ul" data-list-style='disc'><label>实心圆点列表:</label><ul style="list-style-type: disc;"><li>________</li></ul></li><li data-list-type="ul" data-list-style='circle'><label>空心圆点列表:</label><ul style="list-style-type: circle;"><li>________</li></ul></li><li data-list-type="ul" data-list-style='square'><label>空心方块列表:</label><ul style="list-style-type: square;"><li>________</li></ul></li></ul></div></div></div><div class="menu-divider"></div><div class="menu-item"><!-- <div class="menu-item__table"><i title="表格"></i></div> --><!-- <div class="menu-item__table__collapse"><div class="table-close">×</div><div class="table-title"><span class="table-select">插入</span><span>表格</span></div><div class="table-panel"></div></div> --><!-- <div class="menu-item__image"><i title="图片"></i><input type="file" id="image" accept=".png, .jpg, .jpeg, .svg, .gif"></div> --><div class="menu-item__hyperlink"><i title="超链接"></i></div><div class="menu-item__separator"><i title="分割线"></i><div class="options"><ul><li data-separator='0,0'><i></i></li><li data-separator="1,1"><i></i></li><li data-separator="3,1"><i></i></li><li data-separator="4,4"><i></i></li><li data-separator="7,3,3,3"><i></i></li><li data-separator="6,2,2,2,2,2"><i></i></li></ul></div></div><!-- <div class="menu-item__watermark"><i title="水印(添加、删除)"></i><div class="options"><ul><li data-menu="add">添加水印</li><li data-menu="delete">删除水印</li></ul></div></div><div class="menu-item__codeblock" title="代码块"><i></i></div><div class="menu-item__page-break" title="分页符"><i></i></div><div class="menu-item__control"><i title="控件"></i><div class="options"><ul><li data-control='text'>文本</li><li data-control="select">列举</li><li data-control="checkbox">复选框</li></ul></div></div><div class="menu-item__checkbox" title="复选框"><i></i></div><div class="menu-item__latex" title="LateX"><i></i></div><div class="menu-item__date"><i title="日期"></i><div class="options"><ul><li data-format="yyyy-MM-dd"></li><li data-format="yyyy-MM-dd hh:mm:ss"></li></ul></div></div><div class="menu-item__block" title="内容块"><i></i></div> --></div><div class="menu-divider"></div><div class="menu-item"><div class="menu-item__search" data-menu="search"><i></i></div><div class="menu-item__search__collapse" data-menu="search"><div class="menu-item__search__collapse__search"><input type="text" /><label class="search-result"></label><div class="arrow-left"><i></i></div><div class="arrow-right"><i></i></div><span>×</span></div><div class="menu-item__search__collapse__replace"><input type="text"><button>替换</button></div></div><!-- <div class="menu-item__print" data-menu="print"><i></i></div> --></div></div></el-header><el-main class="content"><!-- <div class="catalog" editor-component="catalog"><div class="catalog__header"><span>目录</span><div class="catalog__header__close"><i></i></div></div><div class="catalog__main"></div></div> --><div id="canvasEditor" class="canvas-editor" editor-component="main"></div><!-- <div class="comment" editor-component="comment"></div> --></el-main><el-footer style="height: 30px;"> <div class="footer" editor-component="footer"><div><!-- <div class="catalog-mode" title="目录"><i></i></div> --><div class="page-mode"><i title="页面模式(分页、连页)"></i><div class="options"><ul><li data-page-mode="paging" class="active">分页</li><li data-page-mode="continuity">连页</li></ul></div></div><span>可见页码:<span class="page-no-list">1</span></span><span>页面:<span class="page-no">1</span>/<span class="page-size">1</span></span><span>字数:<span class="word-count">0</span></span></div><div class="editor-mode" title="编辑模式(编辑、清洁、只读、表单)">编辑模式</div><div><div class="page-scale-minus" title="缩小(Ctrl+-)"><i></i></div><span class="page-scale-percentage" title="显示比例(点击可复原Ctrl+0)">100%</span><div class="page-scale-add" title="放大(Ctrl+=)"><i></i></div><div class="paper-size"><i title="纸张类型"></i><div class="options"><ul><li data-paper-size="794*1123" class="active">A4</li><li data-paper-size="1593*2251">A2</li><li data-paper-size="1125*1593">A3</li><li data-paper-size="565*796">A5</li><li data-paper-size="412*488">5号信封</li><li data-paper-size="450*866">6号信封</li><li data-paper-size="609*862">7号信封</li><li data-paper-size="862*1221">9号信封</li><li data-paper-size="813*1266">法律用纸</li><li data-paper-size="813*1054">信纸</li></ul></div></div><div class="paper-direction"><i title="纸张方向"></i><div class="options"><ul><li data-paper-direction="vertical" class="active">纵向</li><li data-paper-direction="horizontal">横向</li></ul></div></div><div class="paper-margin" title="页边距"><i></i></div><div class="fullscreen" title="全屏显示"><i></i></div></div></div></el-footer></el-container></div>
</template><script>
import Editor from '@hufe921/canvas-editor'
import { Dialog } from './components/dialog/Dialog'
import { Signature } from './components/signature/Signature'
import  {IEditorOption, ITableOption,IHeader, IFooter } from './options'import {BlockType,Command,ControlType,EditorMode,EditorZone,ElementType,IBlock,ICatalogItem,IElement,KeyMap,ListStyle,ListType,PageMode,PaperDirection,RowFlex,TitleLevel,splitText } from '@hufe921/canvas-editor'export default {data() {return {editorRef: null,isApple: typeof navigator !== 'undefined' && /Mac OS X/.test(navigator.userAgent),// 编辑模式modeList: [{mode: EditorMode.READONLY,name: '只读模式'},{mode: EditorMode.EDIT,name: '编辑模式'}],header: [],// 页眉配置main: [],// 主要编辑内容footer: [],// 页脚信息options: IEditorOption,// 批注 TODOcommentList: []};},props: {// 编辑模式editMode: {type: String},// html数据htmlData: {type: String},// 后端传过来的保存html的JSON数据(用于回显)docJson: {type: String}},watch: {// 监听父组件传过来的编辑模式,设置模式editMode: {handler (val) {if(this.editorRef) {this.editorRef.command.executeMode(val)// 设置模式const modeElement = document.querySelector('.editor-mode')modeElement.innerText = this.modeList.filter((item) => item.mode == val).map((data) => data.name) || ''// 设置菜单栏权限视觉反馈const isReadonly = val === EditorMode.READONLYconst enableMenuList = ['search', 'print']document.querySelectorAll('.menu-item>div').forEach(dom => {const menu = dom.dataset.menuisReadonly && (!menu || !enableMenuList.includes(menu))? dom.classList.add('disable'): dom.classList.remove('disable')})}},deep: true}},methods:{debounce(func, delay) {let timer;return function(...args) {if (timer) {window.clearTimeout(timer);}timer = window.setTimeout(() => {func.apply(this, args);}, delay);};}},mounted () {const isApple = typeof navigator !== 'undefined' && /Mac OS X/.test(navigator.userAgent)const instance =  new Editor(document.querySelector('.canvas-editor'),{header: this.header,main: this.main,footer: this.footer},this.options)this.editorRef = instance;// cypress使用Reflect.set(window, 'editor', instance)// 回显编辑器数据if(this.docJson !== null) {// 通过getValue来的数据回显页面(因为用html回显页面会丢掉font-family,官网git issue有解释)instance.command.executeSetValue({main: JSON.parse(this.docJson)})} else {// 处理后端返回的html字符串// 先替换 \r\n 为 空格,以统一处理空格问题let step1 = this.htmlData.replace(/\r\n/g, ' ');// 然后替换 \\\" 为 \" ,确保样式字符串内的引号正确let step2 = step1.replace(/\\\"/g, '"');// 接着替换 \\ 为 空字符,去掉其他不必要的转义let cleanedHtml = step2.replace(/\\+/g, '');// 设置Word模板数据instance.command.executeSetHTML({main: cleanedHtml})}// 菜单弹窗销毁window.addEventListener('click',evt => {const visibleDom = document.querySelector('.visible')if (!visibleDom || visibleDom.contains(evt.target)) returnvisibleDom.classList.remove('visible')},{capture: true})/*工具栏方法*/// 1.保存(自定义)const saveDom = document.querySelector('.menu-item__save');saveDom.title = `保存(${this.isApple ? '⌘' : 'Ctrl'}+S)`;saveDom.onclick = () => {const value = instance.command.getValue(this.options)const htmlVal = instance.command.getHTML()this.$emit('save', htmlVal)// 保存数据传给父组件};// 快捷键保存instance.listener.saved = (payload) => {console.log('elementList: ', payload)this.$emit('save', htmlVal)// 保存数据传给父组件}// 2. | 撤销 | 重做 | 格式刷 | 清除格式 |const undoDom = document.querySelector('.menu-item__undo')undoDom.title = `撤销(${isApple ? '⌘' : 'Ctrl'}+Z)`undoDom.onclick = function () {console.log('undo')instance.command.executeUndo()}const redoDom = document.querySelector('.menu-item__redo')redoDom.title = `重做(${isApple ? '⌘' : 'Ctrl'}+Y)`redoDom.onclick = function () {console.log('redo')instance.command.executeRedo()}const painterDom = document.querySelector('.menu-item__painter')painterDom.onclick = function () {console.log('painter')instance.command.executePainter({isDblclick: false})}painterDom.ondblclick = function () {console.log('painter')instance.command.executePainter({isDblclick: true})}document.querySelector('.menu-item__format').onclick =function () {console.log('format')instance.command.executeFormat()}// 3. | 字体 | 字体变大 | 字体变小 | 加粗 | 斜体 | 下划线 | 删除线 | 上标 | 下标 | 字体颜色 | 背景色 |const fontDom = document.querySelector('.menu-item__font')const fontSelectDom = fontDom.querySelector('.select')const fontOptionDom = fontDom.querySelector('.options')fontDom.onclick = function () {console.log('font')fontOptionDom.classList.toggle('visible')}fontOptionDom.onclick = function (evt) {const li = evt.targetinstance.command.executeFont(li.dataset.family)}const sizeSetDom = document.querySelector('.menu-item__size')const sizeSelectDom = sizeSetDom.querySelector('.select')const sizeOptionDom = sizeSetDom.querySelector('.options')sizeSetDom.title = `设置字号`sizeSetDom.onclick = function () {console.log('size')sizeOptionDom.classList.toggle('visible')}sizeOptionDom.onclick = function (evt) {const li = evt.targetinstance.command.executeSize(Number(li.dataset.size))}const sizeAddDom = document.querySelector('.menu-item__size-add')sizeAddDom.title = `增大字号(${this.isApple ? '⌘' : 'Ctrl'}+[)`sizeAddDom.onclick = function () {console.log('size-add')instance.command.executeSizeAdd()}const sizeMinusDom = document.querySelector('.menu-item__size-minus')sizeMinusDom.title = `减小字号(${this.isApple ? '⌘' : 'Ctrl'}+])`sizeMinusDom.onclick = function () {console.log('size-minus')instance.command.executeSizeMinus()}const boldDom = document.querySelector('.menu-item__bold')boldDom.title = `加粗(${this.isApple ? '⌘' : 'Ctrl'}+B)`boldDom.onclick = function () {console.log('bold')instance.command.executeBold()}const italicDom =document.querySelector('.menu-item__italic')italicDom.title = `斜体(${this.isApple ? '⌘' : 'Ctrl'}+I)`italicDom.onclick = function () {console.log('italic')instance.command.executeItalic()}const underlineDom = document.querySelector('.menu-item__underline')underlineDom.title = `下划线(${this.isApple ? '⌘' : 'Ctrl'}+U)`underlineDom.onclick = function () {console.log('underline')instance.command.executeUnderline()}const strikeoutDom = document.querySelector('.menu-item__strikeout')strikeoutDom.onclick = function () {console.log('strikeout')instance.command.executeStrikeout()}const superscriptDom = document.querySelector('.menu-item__superscript')superscriptDom.title = `上标(${this.isApple ? '⌘' : 'Ctrl'}+Shift+,)`superscriptDom.onclick = function () {console.log('superscript')instance.command.executeSuperscript()}const subscriptDom = document.querySelector('.menu-item__subscript')subscriptDom.title = `下标(${this.isApple ? '⌘' : 'Ctrl'}+Shift+.)`subscriptDom.onclick = function () {console.log('subscript')instance.command.executeSubscript()}const colorControlDom = document.querySelector('#color')colorControlDom.oninput = function () {instance.command.executeColor(colorControlDom.value)}const colorDom = document.querySelector('.menu-item__color')const colorSpanDom = colorDom.querySelector('span')colorDom.onclick = function () {console.log('color')colorControlDom.click()}const highlightControlDom =document.querySelector('#highlight')highlightControlDom.oninput = function () {instance.command.executeHighlight(highlightControlDom.value)}const highlightDom = document.querySelector('.menu-item__highlight')const highlightSpanDom = highlightDom.querySelector('span')highlightDom.onclick = function () {console.log('highlight')highlightControlDom?.click()}const titleDom = document.querySelector('.menu-item__title')const titleSelectDom = titleDom.querySelector('.select')const titleOptionDom = titleDom.querySelector('.options')titleOptionDom.querySelectorAll('li').forEach((li, index) => {li.title = `Ctrl+${this.isApple ? 'Option' : 'Alt'}+${index}`})titleDom.onclick = function () {console.log('title')titleOptionDom.classList.toggle('visible')}titleOptionDom.onclick = function (evt) {const li = evt.targetconst level = li.dataset.levelinstance.command.executeTitle(level || null)}const leftDom = document.querySelector('.menu-item__left')leftDom.title = `左对齐(${this.isApple ? '⌘' : 'Ctrl'}+L)`leftDom.onclick = function () {console.log('left')instance.command.executeRowFlex(RowFlex.LEFT)}const centerDom =document.querySelector('.menu-item__center')centerDom.title = `居中对齐(${this.isApple ? '⌘' : 'Ctrl'}+E)`centerDom.onclick = function () {console.log('center')instance.command.executeRowFlex(RowFlex.CENTER)}const rightDom = document.querySelector('.menu-item__right')rightDom.title = `右对齐(${this.isApple ? '⌘' : 'Ctrl'}+R)`rightDom.onclick = function () {console.log('right')instance.command.executeRowFlex(RowFlex.RIGHT)}const alignmentDom = document.querySelector('.menu-item__alignment')alignmentDom.title = `两端对齐(${this.isApple ? '⌘' : 'Ctrl'}+J)`alignmentDom.onclick = function () {console.log('alignment')instance.command.executeRowFlex(RowFlex.ALIGNMENT)}const rowMarginDom = document.querySelector('.menu-item__row-margin')const rowOptionDom = rowMarginDom.querySelector('.options')rowMarginDom.onclick = function () {console.log('row-margin')rowOptionDom.classList.toggle('visible')}rowOptionDom.onclick = function (evt) {const li = evt.targetinstance.command.executeRowMargin(Number(li.dataset.rowmargin))}const listDom = document.querySelector('.menu-item__list')listDom.title = `列表(${this.isApple ? '⌘' : 'Ctrl'}+Shift+U)`const listOptionDom = listDom.querySelector('.options')listDom.onclick = function () {console.log('list')listOptionDom.classList.toggle('visible')}listOptionDom.onclick = function (evt) {const li = evt.targetconst listType = li.dataset.listType || nullconst listStyle = (li.dataset.listStyle)instance.command.executeList(listType, listStyle)}// 4. | 表格 | 图片 | 超链接 | 分割线 | 水印 | 代码块 | 分隔符 | 控件 | 复选框 | LaTeX | 日期选择器// const tableDom = document.querySelector('.menu-item__table')// const tablePanelContainer = document.querySelector(//   '.menu-item__table__collapse'// )// const tableClose = document.querySelector('.table-close')// const tableTitle = document.querySelector('.table-select')// const tablePanel = document.querySelector('.table-panel')// // 绘制行列// const tableCellList = []// for (let i = 0; i < 10; i++) {//   const tr = document.createElement('tr')//   tr.classList.add('table-row')//   const trCellList = []//   for (let j = 0; j < 10; j++) {//     const td = document.createElement('td')//     td.classList.add('table-cel')//     tr.append(td)//     trCellList.push(td)//   }//   tablePanel.append(tr)//   tableCellList.push(trCellList)// }// let colIndex = 0// let rowIndex = 0// // 移除所有格选择// function removeAllTableCellSelect() {//   tableCellList.forEach(tr => {//     tr.forEach(td => td.classList.remove('active'))//   })// }// // 设置标题内容// function setTableTitle(payload) {//   tableTitle.innerText = payload// }// // 恢复初始状态// function recoveryTable() {//   // 还原选择样式、标题、选择行列//   removeAllTableCellSelect()//   setTableTitle('插入')//   colIndex = 0//   rowIndex = 0//   // 隐藏panel//   tablePanelContainer.style.display = 'none'// }// tableDom.onclick = function () {//   console.log('table')//   tablePanelContainer.style.display = 'block'// }// tablePanel.onmousemove = function (evt) {//   const celSize = 16//   const rowMarginTop = 10//   const celMarginRight = 6//   const { offsetX, offsetY } = evt//   // 移除所有选择//   removeAllTableCellSelect()//   colIndex = Math.ceil(offsetX / (celSize + celMarginRight)) || 1//   rowIndex = Math.ceil(offsetY / (celSize + rowMarginTop)) || 1//   // 改变选择样式//   tableCellList.forEach((tr, trIndex) => {//     tr.forEach((td, tdIndex) => {//       if (tdIndex < colIndex && trIndex < rowIndex) {//         td.classList.add('active')//       }//     })//   })//   // 改变表格标题//   setTableTitle(`${rowIndex}×${colIndex}`)// }// tableClose.onclick = function () {//   recoveryTable()// }// tablePanel.onclick = function () {//   // 应用选择//   instance.command.executeInsertTable(rowIndex, colIndex)//   recoveryTable()// }// const imageDom = document.querySelector('.menu-item__image')// const imageFileDom = document.querySelector('#image')// imageDom.onclick = function () {//   imageFileDom.click()// }// imageFileDom.onchange = function () {//   const file = imageFileDom.files[0]//   const fileReader = new FileReader()//   fileReader.readAsDataURL(file)//   fileReader.onload = function () {//     // 计算宽高//     const image = new Image()//     const value = String(fileReader.result)//     image.src = value//     image.onload = function () {//       instance.command.executeImage({//         value,//         width: image.width,//         height: image.height//       })//       imageFileDom.value = ''//     }//   }// }const hyperlinkDom = document.querySelector('.menu-item__hyperlink')hyperlinkDom.onclick = function () {console.log('hyperlink')new Dialog({title: '超链接',data: [{type: 'text',label: '文本',name: 'name',required: true,placeholder: '请输入文本',value: instance.command.getRangeText()},{type: 'text',label: '链接',name: 'url',required: true,placeholder: '请输入链接'}],onConfirm: payload => {const name = payload.find(p => p.name === 'name')?.valueif (!name) returnconst url = payload.find(p => p.name === 'url')?.valueif (!url) returninstance.command.executeHyperlink({type: ElementType.HYPERLINK,value: '',url,valueList: splitText(name).map(n => ({value: n,size: 16}))})}})}const separatorDom = document.querySelector('.menu-item__separator')const separatorOptionDom =separatorDom.querySelector('.options')separatorDom.onclick = function () {console.log('separator')separatorOptionDom.classList.toggle('visible')}separatorOptionDom.onmousedown = function (evt) {let payload = []const li = evt.targetconst separatorDash = li.dataset.separator?.split(',').map(Number)if (separatorDash) {const isSingleLine = separatorDash.every(d => d === 0)if (!isSingleLine) {payload = separatorDash}}instance.command.executeSeparator(payload)}// const pageBreakDom = document.querySelector(//   '.menu-item__page-break'// )// pageBreakDom.onclick = function () {//   console.log('pageBreak')//   instance.command.executePageBreak()// }// const watermarkDom = document.querySelector(//   '.menu-item__watermark'// )// const watermarkOptionDom =//   watermarkDom.querySelector('.options')// watermarkDom.onclick = function () {//   console.log('watermark')//   watermarkOptionDom.classList.toggle('visible')// }// watermarkOptionDom.onmousedown = function (evt) {//   const li = evt.target//   const menu = li.dataset.menu//   watermarkOptionDom.classList.toggle('visible')//   if (menu === 'add') {//     new Dialog({//       title: '水印',//       data: [//         {//           type: 'text',//           label: '内容',//           name: 'data',//           required: true,//           placeholder: '请输入内容'//         },//         {//           type: 'color',//           label: '颜色',//           name: 'color',//           required: true,//           value: '#AEB5C0'//         },//         {//           type: 'number',//           label: '字体大小',//           name: 'size',//           required: true,//           value: '120'//         }//       ],//       onConfirm: payload => {//         const nullableIndex = payload.findIndex(p => !p.value)//         if (~nullableIndex) return//         const watermark = payload.reduce((pre, cur) => {//           pre[cur.name] = cur.value//           return pre//         }, {})//         instance.command.executeAddWatermark({//           data: watermark.data,//           color: watermark.color,//           size: Number(watermark.size)//         })//       }//     })//   } else {//     instance.command.executeDeleteWatermark()//   }// }// const codeblockDom = document.querySelector(//   '.menu-item__codeblock'// )// codeblockDom.onclick = function () {//   console.log('codeblock')//   new Dialog({//     title: '代码块',//     data: [//       {//         type: 'textarea',//         name: 'codeblock',//         placeholder: '请输入代码',//         width: 500,//         height: 300//       }//     ],//     onConfirm: payload => {//       const codeblock = payload.find(p => p.name === 'codeblock')?.value//       if (!codeblock) return//       const tokenList = prism.tokenize(codeblock, prism.languages.javascript)//       const formatTokenList = formatPrismToken(tokenList)//       const elementList = []//       for (let i = 0; i < formatTokenList.length; i++) {//         const formatToken = formatTokenList[i]//         const tokenStringList = splitText(formatToken.content)//         for (let j = 0; j < tokenStringList.length; j++) {//           const value = tokenStringList[j]//           const element = {//             value//           }//           if (formatToken.color) {//             element.color = formatToken.color//           }//           if (formatToken.bold) {//             element.bold = true//           }//           if (formatToken.italic) {//             element.italic = true//           }//           elementList.push(element)//         }//       }//       elementList.unshift({//         value: '\n'//       })//       instance.command.executeInsertElementList(elementList)//     }//   })// }// const controlDom = document.querySelector(//   '.menu-item__control'// )// const controlOptionDom = controlDom.querySelector('.options')// controlDom.onclick = function () {//   console.log('control')//   controlOptionDom.classList.toggle('visible')// }// controlOptionDom.onmousedown = function (evt) {//   controlOptionDom.classList.toggle('visible')//   const li = evt.target//   const type = li.dataset.control//   switch (type) {//     case ControlType.TEXT://       new Dialog({//         title: '文本控件',//         data: [//           {//             type: 'text',//             label: '占位符',//             name: 'placeholder',//             required: true,//             placeholder: '请输入占位符'//           },//           {//             type: 'text',//             label: '默认值',//             name: 'value',//             placeholder: '请输入默认值'//           }//         ],//         onConfirm: payload => {//           const placeholder = payload.find(//             p => p.name === 'placeholder'//           )?.value//           if (!placeholder) return//           const value = payload.find(p => p.name === 'value')?.value || ''//           instance.command.executeInsertElementList([//             {//               type: ElementType.CONTROL,//               value: '',//               control: {//                 type,//                 value: value//                   ? [//                       {//                         value//                       }//                     ]//                   : null,//                 placeholder//               }//             }//           ])//         }//       })//       break//     case ControlType.SELECT://       new Dialog({//         title: '列举控件',//         data: [//           {//             type: 'text',//             label: '占位符',//             name: 'placeholder',//             required: true,//             placeholder: '请输入占位符'//           },//           {//             type: 'text',//             label: '默认值',//             name: 'code',//             placeholder: '请输入默认值'//           },//           {//             type: 'textarea',//             label: '值集',//             name: 'valueSets',//             required: true,//             height: 100,//             placeholder: `请输入值集JSON,例:\n[{\n"value":"有",\n"code":"98175"\n}]`//           }//         ],//         onConfirm: payload => {//           const placeholder = payload.find(//             p => p.name === 'placeholder'//           )?.value//           if (!placeholder) return//           const valueSets = payload.find(p => p.name === 'valueSets')?.value//           if (!valueSets) return//           const code = payload.find(p => p.name === 'code')?.value//           instance.command.executeInsertElementList([//             {//               type: ElementType.CONTROL,//               value: '',//               control: {//                 type,//                 code,//                 value: null,//                 placeholder,//                 valueSets: JSON.parse(valueSets)//               }//             }//           ])//         }//       })//       break//     case ControlType.CHECKBOX://       new Dialog({//         title: '复选框控件',//         data: [//           {//             type: 'text',//             label: '默认值',//             name: 'code',//             placeholder: '请输入默认值,多个值以英文逗号分割'//           },//           {//             type: 'textarea',//             label: '值集',//             name: 'valueSets',//             required: true,//             height: 100,//             placeholder: `请输入值集JSON,例:\n[{\n"value":"有",\n"code":"98175"\n}]`//           }//         ],//         onConfirm: payload => {//           const valueSets = payload.find(p => p.name === 'valueSets')?.value//           if (!valueSets) return//           const code = payload.find(p => p.name === 'code')?.value//           instance.command.executeInsertElementList([//             {//               type: ElementType.CONTROL,//               value: '',//               control: {//                 type,//                 code,//                 value: null,//                 valueSets: JSON.parse(valueSets)//               }//             }//           ])//         }//       })//       break//     default://       break//   }// }// const checkboxDom = document.querySelector(//   '.menu-item__checkbox'// )// checkboxDom.onclick = function () {//   console.log('checkbox')//   instance.command.executeInsertElementList([//     {//       type: ElementType.CHECKBOX,//       checkbox: {//         value: false//       },//       value: ''//     }//   ])// }// const latexDom = document.querySelector('.menu-item__latex')// latexDom.onclick = function () {//   console.log('LaTeX')//   new Dialog({//     title: 'LaTeX',//     data: [//       {//         type: 'textarea',//         height: 100,//         name: 'value',//         placeholder: '请输入LaTeX文本'//       }//     ],//     onConfirm: payload => {//       const value = payload.find(p => p.name === 'value')?.value//       if (!value) return//       instance.command.executeInsertElementList([//         {//           type: ElementType.LATEX,//           value//         }//       ])//     }//   })// }// const dateDom = document.querySelector('.menu-item__date')// const dateDomOptionDom = dateDom.querySelector('.options')// dateDom.onclick = function () {//   console.log('date')//   dateDomOptionDom.classList.toggle('visible')//   // 定位调整//   const bodyRect = document.body.getBoundingClientRect()//   const dateDomOptionRect = dateDomOptionDom.getBoundingClientRect()//   if (dateDomOptionRect.left + dateDomOptionRect.width > bodyRect.width) {//     dateDomOptionDom.style.right = '0px'//     dateDomOptionDom.style.left = 'unset'//   } else {//     dateDomOptionDom.style.right = 'unset'//     dateDomOptionDom.style.left = '0px'//   }//   // 当前日期//   const date = new Date()//   const year = date.getFullYear().toString()//   const month = (date.getMonth() + 1).toString().padStart(2, '0')//   const day = date.getDate().toString().padStart(2, '0')//   const hour = date.getHours().toString().padStart(2, '0')//   const minute = date.getMinutes().toString().padStart(2, '0')//   const second = date.getSeconds().toString().padStart(2, '0')//   const dateString = `${year}-${month}-${day}`//   const dateTimeString = `${dateString} ${hour}:${minute}:${second}`//   dateDomOptionDom.querySelector('li:first-child').innerText = dateString//   dateDomOptionDom.querySelector('li:last-child').innerText = dateTimeString// }// dateDomOptionDom.onmousedown = function (evt) {//   const li = evt.target//   const dateFormat = li.dataset.format//   dateDomOptionDom.classList.toggle('visible')//   instance.command.executeInsertElementList([//     {//       type: ElementType.DATE,//       value: '',//       dateFormat,//       valueList: [//         {//           value: li.innerText.trim()//         }//       ]//     }//   ])// }// const blockDom = document.querySelector('.menu-item__block')// blockDom.onclick = function () {//   console.log('block')//   new Dialog({//     title: '内容块',//     data: [//       {//         type: 'select',//         label: '类型',//         name: 'type',//         value: 'iframe',//         required: true,//         options: [//           {//             label: '网址',//             value: 'iframe'//           },//           {//             label: '视频',//             value: 'video'//           }//         ]//       },//       {//         type: 'number',//         label: '宽度',//         name: 'width',//         placeholder: '请输入宽度(默认页面内宽度)'//       },//       {//         type: 'number',//         label: '高度',//         name: 'height',//         required: true,//         placeholder: '请输入高度'//       },//       {//         type: 'textarea',//         label: '地址',//         height: 100,//         name: 'value',//         required: true,//         placeholder: '请输入地址'//       }//     ],//     onConfirm: payload => {//       const type = payload.find(p => p.name === 'type')?.value//       if (!type) return//       const value = payload.find(p => p.name === 'value')?.value//       if (!value) return//       const width = payload.find(p => p.name === 'width')?.value//       const height = payload.find(p => p.name === 'height')?.value//       if (!height) return//       const block = {//         type: null//       }//       if (block.type === BlockType.IFRAME) {//         block.iframeBlock = {//           src: value//         }//       } else if (block.type === BlockType.VIDEO) {//         block.videoBlock = {//           src: value//         }//       }//       const blockElemen = {//         type: ElementType.BLOCK,//         value: '',//         height: Number(height),//         block//       }//       if (width) {//         blockElement.width = Number(width)//       }//       instance.command.executeInsertElementList([blockElement])//     }//   })// }// 5. | 搜索&替换 | 打印 |const searchCollapseDom = document.querySelector('.menu-item__search__collapse')const searchInputDom = document.querySelector('.menu-item__search__collapse__search input')const replaceInputDom = document.querySelector('.menu-item__search__collapse__replace input')const searchDom =document.querySelector('.menu-item__search')searchDom.title = `搜索与替换(${isApple ? '⌘' : 'Ctrl'}+F)`const searchResultDom =searchCollapseDom.querySelector('.search-result')function setSearchResult() {const result = instance.command.getSearchNavigateInfo()if (result) {const { index, count } = resultsearchResultDom.innerText = `${index}/${count}`} else {searchResultDom.innerText = ''}}searchDom.onclick = function () {console.log('search')searchCollapseDom.style.display = 'block'const bodyRect = document.body.getBoundingClientRect()const searchRect = searchDom.getBoundingClientRect()const searchCollapseRect = searchCollapseDom.getBoundingClientRect()if (searchRect.left + searchCollapseRect.width > bodyRect.width) {searchCollapseDom.style.right = '0px'searchCollapseDom.style.left = 'unset'} else {searchCollapseDom.style.right = 'unset'}searchInputDom.focus()}searchCollapseDom.querySelector('span').onclick =function () {searchCollapseDom.style.display = 'none'searchInputDom.value = ''replaceInputDom.value = ''instance.command.executeSearch(null)setSearchResult()}searchInputDom.oninput = function () {instance.command.executeSearch(searchInputDom.value || null)setSearchResult()}searchInputDom.onkeydown = function (evt) {if (evt.key === 'Enter') {instance.command.executeSearch(searchInputDom.value || null)setSearchResult()}}searchCollapseDom.querySelector('button').onclick =function () {const searchValue = searchInputDom.valueconst replaceValue = replaceInputDom.valueif (searchValue && replaceValue && searchValue !== replaceValue) {instance.command.executeReplace(replaceValue)}}searchCollapseDom.querySelector('.arrow-left').onclick =function () {instance.command.executeSearchNavigatePre()setSearchResult()}searchCollapseDom.querySelector('.arrow-right').onclick =function () {instance.command.executeSearchNavigateNext()setSearchResult()}// const printDom = document.querySelector('.menu-item__print')// printDom.title = `打印(${isApple ? '⌘' : 'Ctrl'}+P)`// printDom.onclick = function () {//   console.log('print')//   instance.command.executePrint()// }// 6. 目录显隐 | 页面模式 | 纸张缩放 | 纸张大小 | 纸张方向 | 页边距 | 全屏// async function updateCatalog() {//   const catalog = await instance.command.getCatalog()//   const catalogMainDom =//     document.querySelector('.catalog__main')//   catalogMainDom.innerHTML = ''//   if (catalog) {//     const appendCatalog = (//       parent,//       catalogItems//     ) => {//       for (let c = 0; c < catalogItems.length; c++) {//         const catalogItem = catalogItems[c]//         const catalogItemDom = document.createElement('div')//         catalogItemDom.classList.add('catalog-item')//         // 渲染//         const catalogItemContentDom = document.createElement('div')//         catalogItemContentDom.classList.add('catalog-item__content')//         const catalogItemContentSpanDom = document.createElement('span')//         catalogItemContentSpanDom.innerText = catalogItem.name//         catalogItemContentDom.append(catalogItemContentSpanDom)//         // 定位//         catalogItemContentDom.onclick = () => {//           instance.command.executeLocationCatalog(catalogItem.id)//         }//         catalogItemDom.append(catalogItemContentDom)//         if (catalogItem.subCatalog && catalogItem.subCatalog.length) {//           appendCatalog(catalogItemDom, catalogItem.subCatalog)//         }//         // 追加//         parent.append(catalogItemDom)//       }//     }//     appendCatalog(catalogMainDom, catalog)//   }// }// let isCatalogShow = true// const catalogDom = document.querySelector('.catalog')// const catalogModeDom =//   document.querySelector('.catalog-mode')// const catalogHeaderCloseDom = document.querySelector(//   '.catalog__header__close'// )// const switchCatalog = () => {//   console.log('目录', isCatalogShow)//   isCatalogShow = !isCatalogShow//   if (isCatalogShow) {//     console.log('目录', isCatalogShow)//     catalogDom.style.display = 'block'//     updateCatalog()//   } else {//     catalogDom.style.display = 'none'//   }// }// catalogModeDom.onclick = switchCatalog// catalogHeaderCloseDom.onclick = switchCatalogconst pageModeDom = document.querySelector('.page-mode')const pageModeOptionsDom =pageModeDom.querySelector('.options')pageModeDom.onclick = function () {pageModeOptionsDom.classList.toggle('visible')}pageModeOptionsDom.onclick = function (evt) {const li = evt.targetinstance.command.executePageMode(li.dataset.pageMode)}document.querySelector('.page-scale-percentage').onclick =function () {console.log('page-scale-recovery')instance.command.executePageScaleRecovery()}document.querySelector('.page-scale-minus').onclick =function () {console.log('page-scale-minus')instance.command.executePageScaleMinus()}document.querySelector('.page-scale-add').onclick =function () {console.log('page-scale-add')instance.command.executePageScaleAdd()}// 纸张大小const paperSizeDom = document.querySelector('.paper-size')const paperSizeDomOptionsDom =paperSizeDom.querySelector('.options')paperSizeDom.onclick = function () {paperSizeDomOptionsDom.classList.toggle('visible')}paperSizeDomOptionsDom.onclick = function (evt) {const li = evt.targetconst paperType = li.dataset.paperSizeconst [width, height] = paperType.split('*').map(Number)instance.command.executePaperSize(width, height)// 纸张状态回显paperSizeDomOptionsDom.querySelectorAll('li').forEach(child => child.classList.remove('active'))li.classList.add('active')}// 纸张方向const paperDirectionDom =document.querySelector('.paper-direction')const paperDirectionDomOptionsDom =paperDirectionDom.querySelector('.options')paperDirectionDom.onclick = function () {paperDirectionDomOptionsDom.classList.toggle('visible')}paperDirectionDomOptionsDom.onclick = function (evt) {const li = evt.targetconst paperDirection = li.dataset.paperDirectioninstance.command.executePaperDirection(paperDirection)// 纸张方向状态回显paperDirectionDomOptionsDom.querySelectorAll('li').forEach(child => child.classList.remove('active'))li.classList.add('active')}// 页面边距const paperMarginDom =document.querySelector('.paper-margin')paperMarginDom.onclick = function () {const [topMargin, rightMargin, bottomMargin, leftMargin] =instance.command.getPaperMargin()new Dialog({title: '页边距',data: [{type: 'text',label: '上边距',name: 'top',required: true,value: `${topMargin}`,placeholder: '请输入上边距'},{type: 'text',label: '下边距',name: 'bottom',required: true,value: `${bottomMargin}`,placeholder: '请输入下边距'},{type: 'text',label: '左边距',name: 'left',required: true,value: `${leftMargin}`,placeholder: '请输入左边距'},{type: 'text',label: '右边距',name: 'right',required: true,value: `${rightMargin}`,placeholder: '请输入右边距'}],onConfirm: payload => {const top = payload.find(p => p.name === 'top')?.valueif (!top) returnconst bottom = payload.find(p => p.name === 'bottom')?.valueif (!bottom) returnconst left = payload.find(p => p.name === 'left')?.valueif (!left) returnconst right = payload.find(p => p.name === 'right')?.valueif (!right) returninstance.command.executeSetPaperMargin([Number(top),Number(right),Number(bottom),Number(left)])}})}// 全屏const fullscreenDom = document.querySelector('.fullscreen')fullscreenDom.onclick = toggleFullscreenwindow.addEventListener('keydown', evt => {if (evt.key === 'F11') {toggleFullscreen()evt.preventDefault()}})document.addEventListener('fullscreenchange', () => {fullscreenDom.classList.toggle('exist')})function toggleFullscreen() {console.log('fullscreen')if (!document.fullscreenElement) {document.documentElement.requestFullscreen()} else {document.exitFullscreen()}}// 7. 编辑器使用模式let modeIndex = 0const modeList = [{mode: EditorMode.READONLY,name: '只读模式'},{mode: EditorMode.EDIT,name: '编辑模式'},{mode: EditorMode.CLEAN,name: '清洁模式'},{mode: EditorMode.FORM,name: '表单模式'},{mode: EditorMode.PRINT,name: '打印模式'}]const modeElement = document.querySelector('.editor-mode')// 初始设置只读模式const { name, mode } = modeList[modeIndex]modeElement.innerText = nameinstance.command.executeMode(mode)// 设置菜单栏权限视觉反馈const isReadonly = mode === EditorMode.READONLYconst enableMenuList = ['search', 'print']document.querySelectorAll('.menu-item>div').forEach(dom => {const menu = dom.dataset.menuisReadonly && (!menu || !enableMenuList.includes(menu))? dom.classList.add('disable'): dom.classList.remove('disable')})// modeElement.onclick = function () {//   // 模式选择循环//   modeIndex === modeList.length - 1 ? (modeIndex = 0) : modeIndex++//   // 设置模式//   const { name, mode } = modeList[modeIndex]//   modeElement.innerText = name//   console.log(1212, name)//   instance.command.executeMode(mode)//   // 设置菜单栏权限视觉反馈//   const isReadonly = mode === EditorMode.READONLY//   const enableMenuList = ['search', 'print']//   document.querySelectorAll('.menu-item>div').forEach(dom => {//     const menu = dom.dataset.menu//     isReadonly && (!menu || !enableMenuList.includes(menu))//       ? dom.classList.add('disable')//       : dom.classList.remove('disable')//   })// }// 模拟批注// const commentDom = document.querySelector('.comment')// const updateComment = async() => {//   const groupIds = await instance.command.getGroupIds()//   for (const comment of this.commentList) {//     const activeCommentDom = commentDom.querySelector(//       `.comment-item[data-id='${comment.id}']`//     )//     // 编辑器是否存在对应成组id//     if (!groupIds.includes(comment.id)) {//       // 当前dom是否存在-不存在则追加//       if (!activeCommentDom) {//         const commentItem = document.createElement('div')//         commentItem.classList.add('comment-item')//         commentItem.setAttribute('data-id', comment.id)//         commentItem.onclick = () => {//           instance.command.executeLocationGroup(comment.id)//         }//         commentDom.append(commentItem)//         // 选区信息//         const commentItemTitle = document.createElement('div')//         commentItemTitle.classList.add('comment-item__title')//         commentItemTitle.append(document.createElement('span'))//         const commentItemTitleContent = document.createElement('span')//         commentItemTitleContent.innerText = comment.rangeText//         commentItemTitle.append(commentItemTitleContent)//         const closeDom = document.createElement('i')//         closeDom.onclick = () => {//           instance.command.executeDeleteGroup(comment.id)//         }//         commentItemTitle.append(closeDom)//         commentItem.append(commentItemTitle)//         // 基础信息//         const commentItemInfo = document.createElement('div')//         commentItemInfo.classList.add('comment-item__info')//         const commentItemInfoName = document.createElement('span')//         commentItemInfoName.innerText = comment.userName//         const commentItemInfoDate = document.createElement('span')//         commentItemInfoDate.innerText = comment.createdDate//         commentItemInfo.append(commentItemInfoName)//         commentItemInfo.append(commentItemInfoDate)//         commentItem.append(commentItemInfo)//         // 详细评论//         const commentItemContent = document.createElement('div')//         commentItemContent.classList.add('comment-item__content')//         commentItemContent.innerText = comment.content//         commentItem.append(commentItemContent)//         commentDom.append(commentItem)//       }//     } else {//       // 编辑器内不存在对应成组id则dom则移除//       activeCommentDom?.remove()//     }//   }// }// 8. 内部事件监听instance.listener.rangeStyleChange = function (payload) {// 控件类型payload.type === ElementType.SUBSCRIPT? subscriptDom.classList.add('active'): subscriptDom.classList.remove('active')payload.type === ElementType.SUPERSCRIPT? superscriptDom.classList.add('active'): superscriptDom.classList.remove('active')payload.type === ElementType.SEPARATOR? separatorDom.classList.add('active'): separatorDom.classList.remove('active')separatorOptionDom.querySelectorAll('li').forEach(li => li.classList.remove('active'))if (payload.type === ElementType.SEPARATOR) {const separator = payload.dashArray.join(',') || '0,0'const curSeparatorDom = separatorOptionDom.querySelector(`[data-separator='${separator}']`)if (curSeparatorDom) {curSeparatorDom.classList.add('active')}}// 富文本fontOptionDom.querySelectorAll('li').forEach(li => li.classList.remove('active'))const curFontDom = fontOptionDom.querySelector(`[data-family='${payload.font}']`)if (curFontDom) {fontSelectDom.innerText = curFontDom.innerTextfontSelectDom.style.fontFamily = payload.fontcurFontDom.classList.add('active')}sizeOptionDom.querySelectorAll('li').forEach(li => li.classList.remove('active'))const curSizeDom = sizeOptionDom.querySelector(`[data-size='${payload.size}']`)if (curSizeDom) {sizeSelectDom.innerText = curSizeDom.innerTextcurSizeDom.classList.add('active')} else {sizeSelectDom.innerText = `${payload.size}`}payload.bold? boldDom.classList.add('active'): boldDom.classList.remove('active')payload.italic? italicDom.classList.add('active'): italicDom.classList.remove('active')payload.underline? underlineDom.classList.add('active'): underlineDom.classList.remove('active')payload.strikeout? strikeoutDom.classList.add('active'): strikeoutDom.classList.remove('active')if (payload.color) {colorDom.classList.add('active')colorControlDom.value = payload.colorcolorSpanDom.style.backgroundColor = payload.color} else {colorDom.classList.remove('active')colorControlDom.value = '#000000'colorSpanDom.style.backgroundColor = '#000000'}if (payload.highlight) {highlightDom.classList.add('active')highlightControlDom.value = payload.highlighthighlightSpanDom.style.backgroundColor = payload.highlight} else {highlightDom.classList.remove('active')highlightControlDom.value = '#ffff00'highlightSpanDom.style.backgroundColor = '#ffff00'}// 行布局leftDom.classList.remove('active')centerDom.classList.remove('active')rightDom.classList.remove('active')alignmentDom.classList.remove('active')if (payload.rowFlex && payload.rowFlex === 'right') {rightDom.classList.add('active')} else if (payload.rowFlex && payload.rowFlex === 'center') {centerDom.classList.add('active')} else if (payload.rowFlex && payload.rowFlex === 'alignment') {alignmentDom.classList.add('active')} else {leftDom.classList.add('active')}// 行间距rowOptionDom.querySelectorAll('li').forEach(li => li.classList.remove('active'))const curRowMarginDom = rowOptionDom.querySelector(`[data-rowmargin='${payload.rowMargin}']`)curRowMarginDom.classList.add('active')// 功能payload.undo? undoDom.classList.remove('no-allow'): undoDom.classList.add('no-allow')payload.redo? redoDom.classList.remove('no-allow'): redoDom.classList.add('no-allow')payload.painter? painterDom.classList.add('active'): painterDom.classList.remove('active')// 标题titleOptionDom.querySelectorAll('li').forEach(li => li.classList.remove('active'))if (payload.level) {const curTitleDom = titleOptionDom.querySelector(`[data-level='${payload.level}']`)titleSelectDom.innerText = curTitleDom.innerTextcurTitleDom.classList.add('active')} else {titleSelectDom.innerText = '正文'titleOptionDom.querySelector('li:first-child').classList.add('active')}// 列表listOptionDom.querySelectorAll('li').forEach(li => li.classList.remove('active'))if (payload.listType) {listDom.classList.add('active')const listType = payload.listTypeconst listStyle =payload.listType === ListType.OL ? ListStyle.DECIMAL : payload.listTypeconst curListDom = listOptionDom.querySelector(`[data-list-type='${listType}'][data-list-style='${listStyle}']`)if (curListDom) {curListDom.classList.add('active')}} else {listDom.classList.remove('active')}// 批注// commentDom//   .querySelectorAll('.comment-item')//   .forEach(commentItemDom => {//     commentItemDom.classList.remove('active')//   })// if (payload.groupIds) {//   const [id] = payload.groupIds//   const activeCommentDom = commentDom.querySelector(//     `.comment-item[data-id='${id}']`//   )//   if (activeCommentDom) {//     activeCommentDom.classList.add('active')//     scrollIntoView(commentDom, activeCommentDom)//   }// }}instance.listener.visiblePageNoListChange = function (payload) {const text = payload.map(i => i + 1).join('、')document.querySelector('.page-no-list').innerText = text}instance.listener.pageSizeChange = function (payload) {if(document.querySelector('.page-size')) {document.querySelector('.page-size').innerText = payload.toString()}}instance.listener.intersectionPageNoChange = function (payload) {document.querySelector('.page-no').innerText = `${payload + 1}`}instance.listener.pageScaleChange = function (payload) {document.querySelector('.page-scale-percentage').innerText = `${Math.floor(payload * 10 * 10)}%`}instance.listener.controlChange = function (payload) {const disableMenusInControlContext = ['table','hyperlink','separator','page-break']// 菜单操作权限disableMenusInControlContext.forEach(menu => {const menuDom = document.querySelector(`.menu-item__${menu}`)payload? menuDom.classList.add('disable'): menuDom.classList.remove('disable')})}instance.listener.pageModeChange = function (payload) {const activeMode = pageModeOptionsDom.querySelector(`[data-page-mode='${payload}']`)pageModeOptionsDom.querySelectorAll('li').forEach(li => li.classList.remove('active'))activeMode.classList.add('active')}const handleContentChange = async () => {this.$emit('isSave', true)// 字数const wordCount = await instance.command.getWordCount()document.querySelector('.word-count').innerText = `${wordCount || 0}`// 目录// if (isCatalogShow) {//   this.$nextTick(() => {//     updateCatalog()//   })// }// // 批注// this.$nextTick(() => {//   updateComment()// })}instance.listener.contentChange = this.debounce(handleContentChange, 200)handleContentChange()// 9. 右键菜单注册instance.register.contextMenuList([{name: '批注',when: payload => {return (!payload.isReadonly &&payload.editorHasSelection &&payload.zone === EditorZone.MAIN)},callback: (command) => {new Dialog({title: '批注',data: [{type: 'textarea',label: '批注',height: 100,name: 'value',required: true,placeholder: '请输入批注'}],onConfirm: payload => {const value = payload.find(p => p.name === 'value')?.valueif (!value) returnconst groupId = command.executeSetGroup()if (!groupId) returncommentList.push({id: groupId,content: value,userName: 'Hufe',rangeText: command.getRangeText(),createdDate: new Date().toLocaleString()})}})}},{name: '签名',icon: 'signature',when: payload => {return !payload.isReadonly && payload.editorTextFocus},callback: (command) => {new Signature({onConfirm(payload) {if (!payload) returnconst { value, width, height } = payloadif (!value || !width || !height) returncommand.executeInsertElementList([{value,width,height,type: ElementType.IMAGE}])}})}},{name: '格式整理',icon: 'word-tool',when: payload => {return !payload.isReadonly},callback: (command) => {command.executeWordTool()}}])// 10. 快捷键注册instance.register.shortcutList([{key: KeyMap.P,mod: true,isGlobal: true,callback: (command) => {command.executePrint()}},{key: KeyMap.F,mod: true,isGlobal: true,callback: (command) => {const text = command.getRangeText()searchDom.click()if (text) {searchInputDom.value = textinstance.command.executeSearch(text)setSearchResult()}}},{key: KeyMap.MINUS,ctrl: true,isGlobal: true,callback: (command) => {command.executePageScaleMinus()}},{key: KeyMap.EQUAL,ctrl: true,isGlobal: true,callback: (command) => {command.executePageScaleAdd()}},{key: KeyMap.ZERO,ctrl: true,isGlobal: true,callback: (command) => {command.executePageScaleRecovery()}}])  }
};
</script>

我的实现效果:
在这里插入图片描述
3、组件封装完成后,在其他父组件中使用

import CanvasEditor from '@/components/CanvasEditor/index';<CanvasEditor ref="wordEditor":editMode="type" :htmlData="htmlData" :docJson="docJson" :key="keys" @save="save" @isSave="getSave"></CanvasEditor>
htmlData: '', // 编辑器html数据
docJson: null, // 编辑器getValue数据
keys: new Date().getTime() // 给编辑器赋值不刷新的时候,改变key

我的完整CanvasEditor封装组件源码已附上,仅供参考!!!
可根据以上步骤自定义集成,多查阅官方文档即可。

end~
希望记录的问题能帮助到你~

相关文章:

vue项目集成CanvasEditor实现Word在线编辑器

CanvasEditor实现Word在线编辑器 官网文档&#xff1a;https://hufe.club/canvas-editor-docs/guide/schema.html 源码地址&#xff1a;https://github.com/Hufe921/canvas-editor 前提声明&#xff1a; 由于CanvasEditor目前不支持vue、react 等框架开箱即用版&#xff0c;所以…...

Redis Stream Redisson Stream

目录 一、Redis Stream1.1 场景1&#xff1a;多个客户端可以同时接收到消息1.1.1 XADD - 向stream添加Entry&#xff08;发消息 &#xff09;1.1.2 XREAD - 从stream中读取Entry&#xff08;收消息&#xff09;1.1.3 XRANGE - 从stream指定区间读取Entry&#xff08;收消息&…...

threadX netx 设置IP地址以及获取IP地址

ThreadX 是一个实时操作系统&#xff08;RTOS&#xff09;内核&#xff0c;而 NetX 则是 Express Logic 提供的一个嵌入式 TCP/IP 网络栈&#xff0c;它经常与 ThreadX 一起使用来提供网络功能。在 ThreadX 和 NetX 中设置和获取 IP 地址通常涉及几个步骤。 设置 IP 地址 初始…...

计算机毕业设计hadoop+spark+hive知识图谱医生推荐系统 医生数据分析可视化大屏 医生爬虫 医疗可视化 医生大数据 机器学习 大数据毕业设计

测试过程及结果 本次对于医生推荐系统测试通过手动测试的方式共进行了两轮测试。 &#xff08;1&#xff09;第一轮测试中执行了个20个测试用例&#xff0c;通过16个&#xff0c;失败4个&#xff0c;其中属于严重缺陷的1个&#xff0c;属于一般缺陷的3个。 &#xff08;2&am…...

lammps已经运算结束,有数据忘记算:rerun 命令

需要的文件 1、模拟运算的所有文件&#xff08;模型 、in文件、力场文件&#xff09; 2、模拟计算所得到的dump文件&#xff08;原子轨迹文件&#xff09; rerun命令的使用&#xff08;修改in文件&#xff09; 1、删除or注释掉 输出dump文件的那一行命令 2、加上需要补充计…...

CARLA自动驾驶模拟器基础

CARLA 使用服务器-客户端架构运行&#xff0c;其中 CARLA 服务器运行模拟并由客户端向其发送指令。客户端代码使用 API 与服务器进行通信。要使用 Python API&#xff0c;您必须通过 PIP 安装该模块&#xff1a; pip3 install carla-simulator # Python 3World and client 客…...

华为HCIP Datacom H12-821 卷16

1.判断题 在 VRRP 中,当设备状态变为 Master 后,,会立刻发送免费 ARP 来刷新下游设备的 MAC 表项,从而把用户的流量引到此台设备上来 A、对 B、错 正确答案: A 解析: 2.判断题 路由选择工具 route- policy 能够基于预先定义的条件来进行过滤并设置 BGP...

Python学习打卡:day17

day17 笔记来源于&#xff1a;黑马程序员python教程&#xff0c;8天python从入门到精通&#xff0c;学python看这套就够了 目录 day17121、Python 操作 MySQL 基础使用pymysql创建到 MySQL 的数据库链接执行 SQL 语句执行非查询性质的SQL语句执行查询性质的SQL语句 122、Pyth…...

Spring Cloud Gateway 与 Nacos 的完美结合

在现代微服务架构中&#xff0c;服务网关扮演着至关重要的角色。它不仅负责路由请求到相应的服务&#xff0c;还承担着诸如负载均衡、安全认证、限流熔断等重要功能。Spring Cloud Gateway 作为 Spring Cloud 生态系统中的一员&#xff0c;以其强大的功能和灵活的配置&#xff…...

vue2 element ui 表单 动态增加表单项 表单项值不可重复 select多选

案例 <template><el-form :model"form" ref"form" label-width"70px"><el-form-item><el-button icon"el-icon-plus" type"primary" plain click"add">新增</el-button><el-b…...

[数据集][目标检测]电力场景下电柜箱门把手检测数据集VOC+YOLO格式1167张1类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;1167 标注数量(xml文件个数)&#xff1a;1167 标注数量(txt文件个数)&#xff1a;1167 标注…...

OverTheWire Bandit 靶场通关解析(上)

介绍 OverTheWire Bandit 是一个针对初学者设计的网络安全挑战平台&#xff0c;旨在帮助用户掌握基本的命令行操作和网络安全技能。Bandit 游戏包含一系列的关卡&#xff0c;每个关卡都需要解决特定的任务来获取进入下一关的凭证。通过逐步挑战更复杂的问题&#xff0c;用户可…...

【Python实战因果推断】4_因果效应异质性4

目录 Cumulative Gain Target Transformation Cumulative Gain 如果采用与累积效应曲线完全相同的逻辑&#xff0c;但将每个点乘以累积样本 Ncum/N&#xff0c;就会得到累积增益曲线。现在&#xff0c;即使曲线的起点具有最高的效果&#xff08;对于一个好的模型来说&#x…...

大模型推理知识总结

一、大模型推理概念 大多数流行的only-decode LLM&#xff08;例如 GPT-3&#xff09;都是针对因果建模目标进行预训练的&#xff0c;本质上是作为下一个词预测器。这些 LLM 将一系列tokens作为输入&#xff0c;并自回归生成后续tokens&#xff0c;直到满足停止条件&#xff0…...

[笔记] keytool 导入服务器证书和证书私钥

背景 我当前手头已有一个服务器证书和对应的私钥&#xff0c;现在需要转换为 Java KeyStore 格式使用&#xff0c;找了一大圈才发现 keytool 无法直接导入服务器证书和私钥&#xff0c;当然证书可以直接导入&#xff0c;但是私钥是无法直接导入。找了一大圈发现可以先将服务器…...

【2024-热-办公软件】ONLYOFFICE8.1版本桌面编辑器测评

在今日快速发展的数字化办公环境中&#xff0c;选择一个功能全面且高效的办公软件是至关重要的。最近&#xff0c;我有幸体验了ONLYOFFICE 8.1版本的桌面编辑器&#xff0c;这款软件不仅提供了强大的编辑功能&#xff0c;还拥有众多改进&#xff0c;让办公更加流畅和高效。在本…...

C# 23设计模式备忘

创建型模式&#xff1a;单例&#xff08;Singleton&#xff09;模式&#xff1a;某个类只能生成一个实例&#xff0c;该类提供了一个全局访问点供外部获取该实例&#xff0c;其拓展是有限多例模式。 原型&#xff08;Prototype&#xff09;模式&#xff1a;将一个对象作为原型&…...

STL中的迭代器模式:将算法与数据结构分离

目录 1.概述 2.容器类 2.1.序列容器 2.2.关联容器 2.3.容器适配器 2.4.数组 3.迭代器 4.重用标准迭代器 5.总结 1.概述 在之前&#xff0c;我们讲了迭代器设计模式&#xff0c;分析了它的结构、角色以及优缺点&#xff1a; 设计模式之迭代器模式-CSDN博客 在 STL 中&a…...

TCP、UDP详解

目录 1.区别 1.1 概括 1.2 详解 2.TCP 2.1 内容 2.2 可靠传输 2.2.1 确认应答 2.2.2 超时重传 2.2.3 连接管理 三次握手 四次挥手 2.2.4 滑动窗口 2.2.5 流量控制 2.2.6 拥塞控制 2.2.7 延时应答 2.2.8 捎带应答 2.2.9 面向字节流 2.2.10 异常情况的处理 1.…...

【脚本工具库】批量下采样图像(附源码)

在图像处理领域&#xff0c;我们经常需要对大批量图像进行下采样操作&#xff0c;以便减小图像的尺寸和文件大小&#xff0c;这对于节省存储空间和提高处理速度非常有帮助。手动操作不仅耗时&#xff0c;而且容易出错。为了解决这个问题&#xff0c;我们可以编写一个Python脚本…...

应用升级/灾备测试时使用guarantee 闪回点迅速回退

1.场景 应用要升级,当升级失败时,数据库回退到升级前. 要测试系统,测试完成后,数据库要回退到测试前。 相对于RMAN恢复需要很长时间&#xff0c; 数据库闪回只需要几分钟。 2.技术实现 数据库设置 2个db_recovery参数 创建guarantee闪回点&#xff0c;不需要开启数据库闪回。…...

day52 ResNet18 CBAM

在深度学习的旅程中&#xff0c;我们不断探索如何提升模型的性能。今天&#xff0c;我将分享我在 ResNet18 模型中插入 CBAM&#xff08;Convolutional Block Attention Module&#xff09;模块&#xff0c;并采用分阶段微调策略的实践过程。通过这个过程&#xff0c;我不仅提升…...

大型活动交通拥堵治理的视觉算法应用

大型活动下智慧交通的视觉分析应用 一、背景与挑战 大型活动&#xff08;如演唱会、马拉松赛事、高考中考等&#xff09;期间&#xff0c;城市交通面临瞬时人流车流激增、传统摄像头模糊、交通拥堵识别滞后等问题。以演唱会为例&#xff0c;暖城商圈曾因观众集中离场导致周边…...

【android bluetooth 框架分析 04】【bt-framework 层详解 1】【BluetoothProperties介绍】

1. BluetoothProperties介绍 libsysprop/srcs/android/sysprop/BluetoothProperties.sysprop BluetoothProperties.sysprop 是 Android AOSP 中的一种 系统属性定义文件&#xff08;System Property Definition File&#xff09;&#xff0c;用于声明和管理 Bluetooth 模块相…...

Linux云原生安全:零信任架构与机密计算

Linux云原生安全&#xff1a;零信任架构与机密计算 构建坚不可摧的云原生防御体系 引言&#xff1a;云原生安全的范式革命 随着云原生技术的普及&#xff0c;安全边界正在从传统的网络边界向工作负载内部转移。Gartner预测&#xff0c;到2025年&#xff0c;零信任架构将成为超…...

DeepSeek 技术赋能无人农场协同作业:用 AI 重构农田管理 “神经网”

目录 一、引言二、DeepSeek 技术大揭秘2.1 核心架构解析2.2 关键技术剖析 三、智能农业无人农场协同作业现状3.1 发展现状概述3.2 协同作业模式介绍 四、DeepSeek 的 “农场奇妙游”4.1 数据处理与分析4.2 作物生长监测与预测4.3 病虫害防治4.4 农机协同作业调度 五、实际案例大…...

用机器学习破解新能源领域的“弃风”难题

音乐发烧友深有体会&#xff0c;玩音乐的本质就是玩电网。火电声音偏暖&#xff0c;水电偏冷&#xff0c;风电偏空旷。至于太阳能发的电&#xff0c;则略显朦胧和单薄。 不知你是否有感觉&#xff0c;近两年家里的音响声音越来越冷&#xff0c;听起来越来越单薄&#xff1f; —…...

20个超级好用的 CSS 动画库

分享 20 个最佳 CSS 动画库。 它们中的大多数将生成纯 CSS 代码&#xff0c;而不需要任何外部库。 1.Animate.css 一个开箱即用型的跨浏览器动画库&#xff0c;可供你在项目中使用。 2.Magic Animations CSS3 一组简单的动画&#xff0c;可以包含在你的网页或应用项目中。 3.An…...

Python Einops库:深度学习中的张量操作革命

Einops&#xff08;爱因斯坦操作库&#xff09;就像给张量操作戴上了一副"语义眼镜"——让你用人类能理解的方式告诉计算机如何操作多维数组。这个基于爱因斯坦求和约定的库&#xff0c;用类似自然语言的表达式替代了晦涩的API调用&#xff0c;彻底改变了深度学习工程…...

C语言中提供的第三方库之哈希表实现

一. 简介 前面一篇文章简单学习了C语言中第三方库&#xff08;uthash库&#xff09;提供对哈希表的操作&#xff0c;文章如下&#xff1a; C语言中提供的第三方库uthash常用接口-CSDN博客 本文简单学习一下第三方库 uthash库对哈希表的操作。 二. uthash库哈希表操作示例 u…...