vue快速集成word在线编辑
金格插件WebOffice2015、chrome浏览器插件、最后选择了和only-office非常功能强大,word、ppt、excel都支持在线编辑预览,还支持协同,又有免费开源版。
word在线编辑
查看一些文档金格插件WebOffice2015、chrome浏览器插件、only-office、UEditor、TinyMCE、CKEditor、wangeditor、canvas-editor
最后选择了only-office和canvas-editor
only-office非常功能强大,word、ppt、excel都支持在线编辑预览,还支持协同,又有免费开源版。
附上本地运行demo:
一、安装docker
二、安装并启动 Onlyoffice 服务:
docker run -i -t -d -p 8701:80 onlyoffice/documentserver:版本号
如果是第 1 次执行这个命令,会先去下载 Onlyoffice,比较慢,约等待 3~10 分钟,网络畅通一点的会快一些。如果是已经安装过则直接进行启动。
三、启动内置服务
先执行 docker ps 查看 Onlyoffice 容器 ID:
# 注意这里要将 id 替换成自己的
docker exec -it f2a3eb675ad1 /bin/bash
然后执行 docker exec -it ID /bin/bash 进入容器,这里将获取到的 ID 替换一下:
# 启动所有的内置服务
supervisorctl restart all
# 退出容器
exit
最后访问 http://IP:8701/example 页面(这里要注意,IP 不能是 localhost 和 127.0.0.1,一定要用真实 IP 来访问)
因为开发周期,后端又比较懒不想花时间去看文档。这一方案被我放弃了
最后选择了canvas-editor
canvas-editor
为什么选它了,开发周期短,界面与word编辑器比较像,可以快速集成到vue,虽然作者没有开箱即用版。
在vue中主要实现方式就是采用开源项目代码。
在组件模块,新建vue文件,html采用开源项目代码,分3个部分,工具栏,侧边菜单,主要内容,底部工具,旁边批注。通过import引入开源样式,注意样式冲突。在onMounted,采用开源main.ts window.onload代码。
<div class="menu" editor-component="menu">
...
</div>
<div class="catalog" editor-component="catalog">
...
</div>
<div id="canvasEditor" class="canvas-editor"></div>
<div class="footer-canvas" editor-component="footer">
...
</div>
const instance = new Editor(
document.querySelector('#canvasEditor'),
{
header: props.header,
main: props.main,
footer: props.footer
},
options
);
console.log('实例', instance);
editorRef.value = instance;
// 工具栏方法 例:
// 2. | 撤销 | 重做 | 格式刷 | 清除格式 |
const undoDom = document.querySelector('.menu-item__undo');
undoDom.title = `撤销(${isApple ? '⌘' : 'Ctrl'}+Z)`;
undoDom.onclick = function () {
console.log('undo');
instance.command.executeUndo();
};
<style lang="scss" scoped>
#canvasEditor {
display: flex;
justify-content: center;
background: #f2f4f7;
}
@import url(@/assets/css/dialog.css);
@import url(@/assets/css/signature.css);
</style>
思路通过富文本编辑器实现在线编辑功能,通过插件提供的api获取图片base64和文本数据,通过接口保存至数据库,通过列表数据复显编辑,后端获取到图片数据转pdf或者其他格式都可以。自己转pdf则是通过html2canvas jspdf
完整示例
自定义组件结构
–editor
- –Dialog.js // 源代码文件
- –index.vue
- –tool.js // 源代码文件
template部分的html代码
<div>
<div class="menu" editor-component="menu">
<div class="menu-item">
<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>
<span class="select"></span>
<div class="options">
<ul>
<li data-decoration-style="solid">
<i></i>
</li>
<li data-decoration-style="double">
<i></i>
</li>
<li data-decoration-style="dashed">
<i></i>
</li>
<li data-decoration-style="dotted">
<i></i>
</li>
<li data-decoration-style="wavy">
<i></i>
</li>
</ul>
</div>
</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 class="menu-divider"></div>
<!-- 自定义按钮 -->
<div class="menu-item">
<p class="btn-save" @click="onSave">保存</p>
<p class="btn-back" @click="onBack">返回</p>
</div>
</div>
<div style="height: 80px; width: 100%"></div>
<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"></div>
<div class="footer-canvas" 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="编辑模式(编辑、清洁、只读、表单)" style="display: none">编辑模式</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>
</div>
使用的是Vue3 组件核心代码
import { ref, reactive, toRefs, onMounted, onUnmounted, nextTick } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import Editor from '@hufe921/canvas-editor';
import {
splitText,
nextTickEd,
debounce,
scrollIntoView,
ControlType,
BlockType,
ListType,
EditorMode,
KeyMap,
ListStyle,
RowFlex,
ElementType
} from './tool.js';
import { Dialog } from './Dialog.js';
export default {
props: {
header: {
type: Array,
default: []
},
main: {
type: Array,
default: []
},
footer: {
type: Array,
default: []
},
options: {
type: Object,
default: {}
},
commentList: {
type: Array,
default: []
}
},
setup(props, ctx) {
const editorRef = ref(null); // 编辑器实例
const isCatalogShow = ref(true); // 目录控件
const commentDomRef = ref(null); // 批注实例
const tableCellList = ref(null); // 表格
const tablePanelDom = ref(null); // 表格Dom
const router = useRouter();
const state = reactive({
canvas: null,
editor: null,
commentList: props.commentList, // 批注
onBack: () => {
router.back();
},
onSave: async () => {
console.log(editorRef.value.command.getHTML());
const value = editorRef.value.command.getValue();
const imgage = await editorRef.value.command.getImage();
const options = editorRef.value.command.getOptions();
console.log(value);
return;
ctx.emit('getCanvasEditorData', { value, imgage, options });
},
// 更新目录
updateCatalog: async () => {
const instance = editorRef.value;
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);
}
isCatalogShow.value = true;
const catalogDom = document.querySelector('.catalog');
const catalogModeDom = document.querySelector('.catalog-mode');
const catalogHeaderCloseDom = document.querySelector('.catalog__header__close');
const switchCatalog = () => {
isCatalogShow.value = !isCatalogShow.value;
if (isCatalogShow.value) {
catalogDom.style.display = 'block';
state.updateCatalog();
} else {
catalogDom.style.display = 'none';
}
};
catalogModeDom.onclick = switchCatalog;
catalogHeaderCloseDom.onclick = switchCatalog;
const pageModeDom = document.querySelector('.page-mode');
const pageModeOptionsDom = pageModeDom.querySelector('.options');
pageModeDom.onclick = function () {
pageModeOptionsDom.classList.toggle('visible');
};
pageModeOptionsDom.onclick = function (evt) {
const li = evt.target;
instance.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();
};
},
// 模拟批注
updateComment: async () => {
const instance = editorRef.value;
const commentDom = document.querySelector('.comment');
commentDomRef.value = commentDom;
const groupIds = await instance.command.getGroupIds();
for (const comment of state.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();
}
}
}
});
onMounted(async () => {
await nextTick(() => {});
const isApple = typeof navigator !== 'undefined' && /Mac OS X/.test(navigator.userAgent);
const options = props.options;
const instance = new Editor(
document.querySelector('#canvasEditor'),
{
header: props.header,
main: props.main,
footer: props.footer
},
options
);
console.log('实例', instance);
editorRef.value = instance;
// 菜单弹窗销毁
window.addEventListener(
'click',
evt => {
const visibleDom = document.querySelector('.visible');
if (!visibleDom || visibleDom.contains(evt.target)) return;
visibleDom.classList.remove('visible');
},
{
capture: true
}
);
// 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');
let isFirstClick = true;
let painterTimeout;
painterDom.onclick = function () {
if (isFirstClick) {
isFirstClick = false;
painterTimeout = window.setTimeout(() => {
console.log('painter-click');
isFirstClick = true;
instance.command.executePainter({
isDblclick: false
});
}, 200);
} else {
window.clearTimeout(painterTimeout);
}
};
painterDom.ondblclick = function () {
console.log('painter-dblclick');
isFirstClick = true;
window.clearTimeout(painterTimeout);
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.target;
instance.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.target;
instance.command.executeSize(Number(li.dataset.size));
};
const sizeAddDom = document.querySelector('.menu-item__size-add');
sizeAddDom.title = `增大字号(${isApple ? '⌘' : 'Ctrl'}+[)`;
sizeAddDom.onclick = function () {
console.log('size-add');
instance.command.executeSizeAdd();
};
const sizeMinusDom = document.querySelector('.menu-item__size-minus');
sizeMinusDom.title = `减小字号(${isApple ? '⌘' : 'Ctrl'}+])`;
sizeMinusDom.onclick = function () {
console.log('size-minus');
instance.command.executeSizeMinus();
};
const boldDom = document.querySelector('.menu-item__bold');
boldDom.title = `加粗(${isApple ? '⌘' : 'Ctrl'}+B)`;
boldDom.onclick = function () {
console.log('bold');
instance.command.executeBold();
};
const italicDom = document.querySelector('.menu-item__italic');
italicDom.title = `斜体(${isApple ? '⌘' : 'Ctrl'}+I)`;
italicDom.onclick = function () {
console.log('italic');
instance.command.executeItalic();
};
const underlineDom = document.querySelector('.menu-item__underline');
underlineDom.title = `下划线(${isApple ? '⌘' : 'Ctrl'}+U)`;
const underlineOptionDom = underlineDom.querySelector('.options');
underlineDom.querySelector('.select').onclick = function () {
underlineOptionDom.classList.toggle('visible');
};
underlineDom.querySelector('i').onclick = function () {
console.log('underline');
instance.command.executeUnderline();
underlineOptionDom.classList.remove('visible');
};
underlineDom.querySelector('ul').onmousedown = function (evt) {
const li = evt.target;
const decorationStyle = li.dataset.decorationStyle;
instance.command.executeUnderline({
style: decorationStyle
});
underlineOptionDom.classList.remove('visible');
};
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 = `上标(${isApple ? '⌘' : 'Ctrl'}+Shift+,)`;
superscriptDom.onclick = function () {
console.log('superscript');
instance.command.executeSuperscript();
};
const subscriptDom = document.querySelector('.menu-item__subscript');
subscriptDom.title = `下标(${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+${isApple ? 'Option' : 'Alt'}+${index}`;
});
titleDom.onclick = function () {
console.log('title');
titleOptionDom.classList.toggle('visible');
};
titleOptionDom.onclick = function (evt) {
const li = evt.target;
const level = li.dataset.level;
instance.command.executeTitle(level || null);
};
const leftDom = document.querySelector('.menu-item__left');
leftDom.title = `左对齐(${isApple ? '⌘' : 'Ctrl'}+L)`;
leftDom.onclick = function () {
console.log('left');
instance.command.executeRowFlex(RowFlex.LEFT);
};
const centerDom = document.querySelector('.menu-item__center');
centerDom.title = `居中对齐(${isApple ? '⌘' : 'Ctrl'}+E)`;
centerDom.onclick = function () {
console.log('center');
instance.command.executeRowFlex(RowFlex.CENTER);
};
const rightDom = document.querySelector('.menu-item__right');
rightDom.title = `右对齐(${isApple ? '⌘' : 'Ctrl'}+R)`;
rightDom.onclick = function () {
console.log('right');
instance.command.executeRowFlex(RowFlex.RIGHT);
};
const alignmentDom = document.querySelector('.menu-item__alignment');
alignmentDom.title = `两端对齐(${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.target;
instance.command.executeRowMargin(Number(li.dataset.rowmargin));
};
const listDom = document.querySelector('.menu-item__list');
listDom.title = `列表(${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.target;
const listType = li.dataset.listType || null;
const 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 = [];
tableCellList.value = [];
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.value.push(trCellList);
}
tablePanelDom.value = tablePanel;
let colIndex = 0;
let rowIndex = 0;
// 移除所有格选择
function removeAllTableCellSelect() {
tableCellList.value.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.value.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 = 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')?.value;
if (!name) return;
const url = payload.find(p => p.name === 'url')?.value;
if (!url) return;
instance.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.target;
const 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: ''
}
]);
};
// LateX文本
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: BlockType.IFRAME
};
if (block.type === BlockType.IFRAME) {
block.iframeBlock = {
src: value
};
} else if (block.type === BlockType.VIDEO) {
block.videoBlock = {
src: value
};
}
const blockElement = {
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 } = result;
searchResultDom.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.value;
const replaceValue = replaceInputDom.value;
if (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 () {
instance.command.executePrint();
};
// 6. 目录显隐 | 页面模式 | 纸张缩放 | 纸张大小 | 纸张方向 | 页边距 | 全屏
state.updateCatalog();
// 页面
const pageModeDom = document.querySelector('.page-mode');
const pageModeOptionsDom = pageModeDom.querySelector('.options');
pageModeDom.onclick = function () {
pageModeOptionsDom.classList.toggle('visible');
};
pageModeOptionsDom.onclick = function (evt) {
const li = evt.target;
instance.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.target;
const paperType = li.dataset.paperSize;
const [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.target;
const paperDirection = li.dataset.paperDirection;
instance.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')?.value;
if (!top) return;
const bottom = payload.find(p => p.name === 'bottom')?.value;
if (!bottom) return;
const left = payload.find(p => p.name === 'left')?.value;
if (!left) return;
const right = payload.find(p => p.name === 'right')?.value;
if (!right) return;
instance.command.executeSetPaperMargin([Number(top), Number(right), Number(bottom), Number(left)]);
}
});
};
// 全屏
const fullscreenDom = document.querySelector('.fullscreen');
fullscreenDom.onclick = toggleFullscreen;
window.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 = 0;
const modeList = [
{
mode: EditorMode.EDIT,
name: '编辑模式'
},
{
mode: EditorMode.CLEAN,
name: '清洁模式'
},
{
mode: EditorMode.READONLY,
name: '只读模式'
},
{
mode: EditorMode.FORM,
name: '表单模式'
},
{
mode: EditorMode.PRINT,
name: '打印模式'
}
];
const modeElement = document.querySelector('.editor-mode');
modeElement.onclick = function () {
// 模式选择循环
modeIndex === modeList.length - 1 ? (modeIndex = 0) : modeIndex++;
// 设置模式
const { name, mode } = modeList[modeIndex];
modeElement.innerText = 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');
});
};
// 模拟批注
state.updateComment();
// 8. 内部事件监听
instance.listener.rangeStyleChange = async 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.innerText;
fontSelectDom.style.fontFamily = payload.font;
curFontDom.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.innerText;
curSizeDom.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.color;
colorSpanDom.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.highlight;
highlightSpanDom.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.innerText;
curTitleDom.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.listType;
const listStyle = payload.listType === ListType.OL ? ListStyle.DECIMAL : payload.listType;
const curListDom = listOptionDom.querySelector(`[data-list-type='${listType}'][data-list-style='${listStyle}']`);
if (curListDom) {
curListDom.classList.add('active');
}
} else {
listDom.classList.remove('active');
}
// 批注
commentDomRef.value?.querySelectorAll('.comment-item').forEach(commentItemDom => {
commentItemDom.classList.remove('active');
});
if (payload.groupIds) {
const [id] = payload.groupIds;
const activeCommentDom = commentDomRef.value?.querySelector(`.comment-item[data-id='${id}']`);
if (activeCommentDom) {
activeCommentDom.classList.add('active');
scrollIntoView(commentDomRef.value, 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) {
document.querySelector('.page-size').innerText = `${payload}`;
};
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 function () {
// 字数
const wordCount = await instance.command.getWordCount();
document.querySelector('.word-count').innerText = `${wordCount || 0}`;
// 目录
if (isCatalogShow.value) {
nextTickEd(() => {
state.updateCatalog();
});
}
// 批注
nextTickEd(() => {
state.updateComment();
});
};
instance.listener.contentChange = debounce(handleContentChange, 200);
handleContentChange();
instance.listener.saved = function (payload) {
console.log('elementList: ', payload);
};
// 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 = text;
instance.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();
}
}
]);
});
onUnmounted(() => {
tableCellList.value.forEach(tr => {
tr.forEach(td => td.classList.remove('active'));
});
while (tablePanelDom.value.firstChild) {
tablePanelDom.value.removeChild(tablePanelDom.value.firstChild);
}
editorRef.value?.destroy();
});
const refState = toRefs(state);
return {
...refState
};
}
};
css部分注意和自己组件库的冲突
<style>
.table-title span {
font-size: 12px;
color: #3d4757;
display: inline;
margin: 0;
}
.table-panel {
cursor: pointer;
}
.table-panel .table-row {
display: flex;
flex-wrap: nowrap;
margin-top: 10px;
pointer-events: none;
}
.table-panel .table-cel {
width: 16px;
height: 16px;
box-sizing: border-box;
border: 1px solid #e2e6ed;
background: #fff;
position: relative;
margin-right: 6px;
pointer-events: none;
}
.table-panel .table-cel.active {
border: 1px solid rgba(73, 145, 242, 0.2);
background: rgba(73, 145, 242, 0.15);
}
.table-panel .table-row .table-cel:last-child {
margin-right: 0;
}
</style>
<style lang="scss" scoped>
#canvasEditor {
display: flex;
justify-content: center;
background: #f2f4f7;
}
.btn-save {
position: absolute;
width: 40px;
height: 28px;
line-height: 26px;
font-size: 12px;
text-align: center;
color: #fff;
border: 1px solid #dcdfe6;
border-radius: 4px;
background-color: #409eff;
cursor: pointer;
}
.btn-back {
position: absolute;
width: 40px;
height: 28px;
line-height: 26px;
font-size: 12px;
text-align: center;
color: #333;
border: 1px solid #dcdfe6;
border-radius: 4px;
background-color: #fff;
margin-left: 50px;
cursor: pointer;
}
// @import url(@/assets/css/canvas-editor.css);
@import url(@/assets/css/dialog.css);
@import url(@/assets/css/signature.css);
</style>
<style lang="scss" scoped>
::-webkit-scrollbar {
height: 16px;
width: 16px;
overflow: visible;
}
::-webkit-scrollbar-button {
width: 0;
height: 0;
}
::-webkit-scrollbar-corner {
background: transparent;
}
::-webkit-scrollbar-thumb {
background-color: #ddd;
background-clip: padding-box;
border: 4px solid #f2f4f7;
border-radius: 8px;
min-height: 24px;
}
::-webkit-scrollbar-thumb:hover {
background-color: #c9c9c9;
}
::-webkit-scrollbar-track {
background: #f2f4f7;
background-clip: padding-box;
}
* {
margin: 0;
padding: 0;
}
/* body {
background-color: #f2f4f7;
} */
ul {
list-style: none;
}
.menu {
width: 100%;
height: 60px;
top: 0;
z-index: 9;
position: fixed;
display: flex;
align-items: center;
justify-content: center;
background: #f2f4f7;
box-shadow: 0 2px 4px 0 transparent;
}
.menu-divider {
width: 1px;
height: 16px;
margin: 0 8px;
display: inline-block;
background-color: #cfd2d8;
}
.menu-item {
height: 24px;
display: flex;
align-items: center;
position: relative;
}
.menu-item > div {
width: 24px;
height: 24px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
margin: 0 2px;
}
.menu-item > div:hover {
background: rgba(25, 55, 88, 0.04);
}
.menu-item > div.active {
background: rgba(25, 55, 88, 0.08);
}
.menu-item i {
width: 16px;
height: 16px;
display: inline-block;
background-repeat: no-repeat;
background-size: 100% 100%;
}
.menu-item > div > span {
width: 16px;
height: 3px;
display: inline-block;
border: 1px solid #e2e6ed;
}
.menu-item .select {
border: none;
font-size: 12px;
line-height: 24px;
user-select: none;
}
.menu-item .select::after {
position: absolute;
content: '';
top: 11px;
width: 0;
height: 0;
right: 2px;
border-color: #767c85 transparent transparent;
border-style: solid solid none;
border-width: 3px 3px 0;
}
.menu-item .options {
min-width: 70px;
position: absolute;
left: 0;
top: 25px;
padding: 10px;
background: #fff;
font-size: 14px;
box-shadow: 0 2px 12px 0 rgb(56 56 56 / 20%);
border: 1px solid #e2e6ed;
border-radius: 2px;
display: none;
}
.menu-item .options.visible {
display: block;
}
.menu-item .options li {
padding: 5px;
margin: 5px 0;
user-select: none;
transition: all 0.3s;
white-space: nowrap;
}
.menu-item .options li:hover {
background-color: #ebecef;
}
.menu-item .options li.active {
background-color: #e2e6ed;
}
.menu-item .menu-item__font {
width: 65px;
position: relative;
}
.menu-item .menu-item__size {
width: 50px;
text-align: center;
position: relative;
}
.menu-item__font .select,
.menu-item__size .select {
width: 100%;
height: 100%;
}
.menu-item__undo.no-allow,
.menu-item__redo.no-allow,
.menu-item > div.disable {
color: #c0c4cc;
cursor: not-allowed;
opacity: 0.4;
pointer-events: none;
}
.menu-item__undo i {
background-image: url('@/assets/images/canvas-editor/undo.svg');
}
.menu-item__redo i {
background-image: url('@/assets/images/canvas-editor/redo.svg');
}
.menu-item__painter i {
background-image: url('@/assets/images/canvas-editor/painter.svg');
}
.menu-item__format i {
background-image: url('@/assets/images/canvas-editor/format.svg');
}
.menu-item__size-add i {
background-image: url('@/assets/images/canvas-editor/size-add.svg');
}
.menu-item__size-minus i {
background-image: url('@/assets/images/canvas-editor/size-minus.svg');
}
.menu-item__bold i {
background-image: url('@/assets/images/canvas-editor/bold.svg');
}
.menu-item__italic i {
background-image: url('@/assets/images/canvas-editor/italic.svg');
}
.menu-item .menu-item__underline {
width: 30px;
position: relative;
}
.menu-item__underline > i {
flex-shrink: 0;
background-image: url('@/assets/images/canvas-editor/underline.svg');
}
.menu-item__underline .select {
width: 100%;
height: 100%;
}
.menu-item .menu-item__underline .options {
width: 128px;
}
.menu-item .menu-item__underline li {
padding: 1px 5px;
}
.menu-item__underline li i {
pointer-events: none;
}
.menu-item__underline li[data-decoration-style='solid'] {
background-image: url('@/assets/images/canvas-editor/line-single.svg');
}
.menu-item__underline li[data-decoration-style='double'] {
background-image: url('@/assets/images/canvas-editor/line-double.svg');
}
.menu-item__underline li[data-decoration-style='dashed'] {
background-image: url('@/assets/images/canvas-editor/line-dash-small-gap.svg');
}
.menu-item__underline li[data-decoration-style='dotted'] {
background-image: url('@/assets/images/canvas-editor/line-dot.svg');
}
.menu-item__underline li[data-decoration-style='wavy'] {
background-image: url('@/assets/images/canvas-editor/line-wavy.svg');
}
.menu-item__strikeout i {
background-image: url('@/assets/images/canvas-editor/strikeout.svg');
}
.menu-item__superscript i {
background-image: url('@/assets/images/canvas-editor/superscript.svg');
}
.menu-item__subscript i {
background-image: url('@/assets/images/canvas-editor/subscript.svg');
}
.menu-item__color,
.menu-item__highlight {
display: flex;
flex-direction: column;
}
.menu-item__color #color,
.menu-item__highlight #highlight {
width: 1px;
height: 1px;
visibility: hidden;
outline: none;
appearance: none;
}
.menu-item__color i {
background-image: url('@/assets/images/canvas-editor/color.svg');
}
.menu-item__color span {
background-color: #000000;
}
.menu-item__highlight i {
background-image: url('@/assets/images/canvas-editor/highlight.svg');
}
.menu-item__highlight span {
background-color: #ffff00;
}
.menu-item .menu-item__title {
width: 60px;
position: relative;
}
.menu-item__title .select {
width: calc(100% - 20px);
height: 100%;
}
.menu-item__title i {
transform: translateX(-5px);
background-image: url('@/assets/images/canvas-editor/title.svg');
}
.menu-item__title .options {
width: 80px;
}
.menu-item__left i {
background-image: url('@/assets/images/canvas-editor/left.svg');
}
.menu-item__center i {
background-image: url('@/assets/images/canvas-editor/center.svg');
}
.menu-item__right i {
background-image: url('@/assets/images/canvas-editor/right.svg');
}
.menu-item__alignment i {
background-image: url('@/assets/images/canvas-editor/alignment.svg');
}
.menu-item__row-margin {
position: relative;
}
.menu-item__row-margin i {
background-image: url('@/assets/images/canvas-editor/row-margin.svg');
}
.menu-item__list {
position: relative;
}
.menu-item__list i {
background-image: url('@/assets/images/canvas-editor/list.svg');
}
.menu-item__list .options {
width: 110px;
}
.menu-item__list .options > ul > li * {
pointer-events: none;
}
.menu-item__list .options > ul > li li {
margin-left: 18px;
}
.menu-item__image i {
background-image: url('@/assets/images/canvas-editor/image.svg');
}
.menu-item__image input {
display: none;
}
.menu-item__table {
position: relative;
}
.menu-item__table i {
background-image: url('@/assets/images/canvas-editor/table.svg');
}
.menu-item .menu-item__table__collapse {
width: 270px;
height: 310px;
background: #fff;
box-shadow: 0 2px 12px 0 rgb(56 56 56 / 20%);
border: 1px solid #e2e6ed;
box-sizing: border-box;
border-radius: 2px;
position: absolute;
display: none;
z-index: 99;
top: 25px;
left: 0;
padding: 14px 27px;
cursor: auto;
}
.menu-item .menu-item__table__collapse .table-close {
position: absolute;
right: 10px;
top: 5px;
cursor: pointer;
}
.menu-item .menu-item__table__collapse .table-close:hover {
color: #7d7e80;
}
.menu-item .menu-item__table__collapse:hover {
background: #fff;
}
.menu-item .menu-item__table__collapse .table-title {
display: flex;
justify-content: flex-start;
padding-bottom: 5px;
border-bottom: 1px solid #e2e6ed;
}
.menu-item__hyperlink i {
background-image: url('@/assets/images/canvas-editor/hyperlink.svg');
}
.menu-item__separator {
position: relative;
}
.menu-item__separator > i {
background-image: url('@/assets/images/canvas-editor/separator.svg');
}
.menu-item .menu-item__separator .options {
width: 128px;
}
.menu-item .menu-item__separator li {
padding: 1px 5px;
}
.menu-item__separator li i {
pointer-events: none;
}
.menu-item__separator li[data-separator='0,0'] {
background-image: url('@/assets/images/canvas-editor/line-single.svg');
}
.menu-item__separator li[data-separator='1,1'] {
background-image: url('@/assets/images/canvas-editor/line-dot.svg');
}
.menu-item__separator li[data-separator='3,1'] {
background-image: url('@/assets/images/canvas-editor/line-dash-small-gap.svg');
}
.menu-item__separator li[data-separator='4,4'] {
background-image: url('@/assets/images/canvas-editor/line-dash-large-gap.svg');
}
.menu-item__separator li[data-separator='7,3,3,3'] {
background-image: url('@/assets/images/canvas-editor/line-dash-dot.svg');
}
.menu-item__separator li[data-separator='6,2,2,2,2,2'] {
background-image: url('@/assets/images/canvas-editor/line-dash-dot-dot.svg');
}
.menu-item__watermark > i {
background-image: url('@/assets/images/canvas-editor/watermark.svg');
}
.menu-item__watermark {
position: relative;
}
.menu-item__codeblock i {
background-image: url('@/assets/images/canvas-editor/codeblock.svg');
}
.menu-item__page-break i {
background-image: url('@/assets/images/canvas-editor/page-break.svg');
}
.menu-item__control {
position: relative;
}
.menu-item__control i {
background-image: url('@/assets/images/canvas-editor/control.svg');
}
.menu-item__checkbox i {
background-image: url('@/assets/images/canvas-editor/checkbox.svg');
}
.menu-item__latex i {
background-image: url('@/assets/images/canvas-editor/latex.svg');
}
.menu-item__date {
position: relative;
}
.menu-item__date i {
background-image: url('@/assets/images/canvas-editor/date.svg');
}
.menu-item__date .options {
width: 160px;
}
.menu-item__block i {
background-image: url('@/assets/images/canvas-editor/block.svg');
}
.menu-item .menu-item__control .options {
width: 55px;
}
.menu-item__search {
position: relative;
}
.menu-item__search i {
background-image: url('@/assets/images/canvas-editor/search.svg');
}
.menu-item .menu-item__search__collapse {
width: 260px;
height: 72px;
box-sizing: border-box;
position: absolute;
display: none;
z-index: 99;
top: 25px;
left: 0;
background: #ffffff;
box-shadow: 0px 5px 5px #e3dfdf;
}
.menu-item .menu-item__search__collapse:hover {
background: #ffffff;
}
.menu-item .menu-item__search__collapse > div {
width: 250px;
height: 36px;
padding: 0 5px;
line-height: 36px;
display: flex;
align-items: center;
justify-content: space-between;
border-radius: 4px;
}
.menu-item .menu-item__search__collapse > div input {
width: 205px;
height: 27px;
appearance: none;
background-color: #fff;
background-image: none;
border-radius: 4px;
border: 1px solid #ebebeb;
box-sizing: border-box;
color: #606266;
display: inline-block;
line-height: 27px;
outline: none;
padding: 0 5px;
}
.menu-item .menu-item__search__collapse > div span {
height: 100%;
color: #dcdfe6;
font-size: 25px;
display: inline-block;
border: 0;
padding: 0 10px;
}
.menu-item .menu-item__search__collapse__replace button {
display: inline-block;
border: 1px solid #e2e6ed;
border-radius: 2px;
background: #fff;
line-height: 22px;
padding: 0 6px;
white-space: nowrap;
margin-left: 4px;
cursor: pointer;
font-size: 12px;
}
.menu-item .menu-item__search__collapse__replace button:hover {
background: rgba(25, 55, 88, 0.04);
}
.menu-item .menu-item__search__collapse__search {
position: relative;
}
.menu-item .menu-item__search__collapse__search label {
right: 110px;
font-size: 12px;
color: #3d4757;
position: absolute;
}
.menu-item .menu-item__search__collapse__search > input {
padding: 5px 90px 5px 5px !important;
}
.menu-item .menu-item__search__collapse__search > div {
width: 28px;
height: 27px;
display: flex;
justify-content: center;
align-items: center;
position: absolute;
border-left: 1px solid #e2e6ed;
transition: all 0.5s;
}
.menu-item .menu-item__search__collapse__search > div:hover {
background-color: rgba(25, 55, 88, 0.04);
}
.menu-item .menu-item__search__collapse__search i {
width: 6px;
height: 8px;
transform: translateY(1px);
}
.menu-item .menu-item__search__collapse__search .arrow-left {
right: 76px;
}
.menu-item .menu-item__search__collapse__search .arrow-left i {
background: url(@/assets/images/canvas-editor/arrow-left.svg) no-repeat;
}
.menu-item .menu-item__search__collapse__search .arrow-right {
right: 48px;
}
.menu-item .menu-item__search__collapse__search .arrow-right i {
background: url(@/assets/images/canvas-editor/arrow-right.svg) no-repeat;
}
.menu-item__print i {
background-image: url('@/assets/images/canvas-editor/print.svg');
}
.catalog {
width: 250px;
position: fixed;
left: 0;
bottom: 0;
top: 100px;
padding: 0 20px 40px 20px;
}
.catalog .catalog__header {
height: 48px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #e2e6ed;
}
.catalog .catalog__header span {
color: #3d4757;
font-size: 14px;
font-weight: bold;
}
.catalog .catalog__header i {
width: 16px;
height: 16px;
cursor: pointer;
display: inline-block;
background: url(@/assets/images/canvas-editor/close.svg) no-repeat;
transition: all 0.2s;
}
.catalog .catalog__header > div:hover {
background: rgba(235, 238, 241);
}
.catalog__main {
height: calc(100% - 60px);
padding: 10px 0;
overflow-y: auto;
overflow-x: hidden;
}
.catalog__main .catalog-item {
width: 100%;
padding-left: 10px;
box-sizing: border-box;
}
.catalog__main > .catalog-item {
padding-left: 0;
}
.catalog__main .catalog-item .catalog-item__content {
width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.catalog__main .catalog-item .catalog-item__content:hover > span {
color: #4991f2;
}
.catalog__main .catalog-item .catalog-item__content span {
color: #3d4757;
line-height: 30px;
font-size: 12px;
white-space: nowrap;
cursor: pointer;
user-select: none;
}
.editor > div {
margin: 80px auto;
}
.ce-page-container canvas {
box-shadow: rgb(158 161 165 / 40%) 0px 2px 12px 0px;
}
.comment {
width: 250px;
height: 650px;
position: fixed;
transform: translateX(420px);
top: 200px;
left: 50%;
overflow-y: auto;
}
.comment-item {
background: #ffffff;
border: 1px solid #e2e6ed;
position: relative;
border-radius: 8px;
padding: 15px;
font-size: 14px;
margin-bottom: 20px;
cursor: pointer;
transition: all 0.5s;
}
.comment-item:hover {
border-color: #c0c6cf;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.comment-item.active {
border-color: #e99d00;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.comment-item__title {
height: 22px;
position: relative;
display: flex;
align-items: center;
color: #c1c6ce;
}
.comment-item__title span:first-child {
background-color: #dbdbdb;
width: 4px;
height: 16px;
margin-right: 5px;
display: inline-block;
border-radius: 999px;
}
.comment-item__title span:nth-child(2) {
width: 200px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.comment-item__title i {
width: 16px;
height: 16px;
cursor: pointer;
position: absolute;
right: -8px;
top: -8px;
background: url(@/assets/images/canvas-editor/close.svg) no-repeat;
}
.comment-item__title i:hover {
opacity: 0.6;
}
.comment-item__info {
height: 28px;
display: flex;
align-items: center;
justify-content: space-between;
}
.comment-item__info > span:first-child {
font-weight: 600;
}
.comment-item__info > span:last-child {
color: #c1c6ce;
}
.comment-item__content {
line-height: 22px;
}
.footer-canvas {
width: 100%;
height: 30px;
display: flex;
align-items: center;
justify-content: space-between;
background: #f2f4f7;
z-index: 9;
position: fixed;
bottom: 0;
left: 0;
font-size: 12px;
padding: 0 4px 0 20px;
box-sizing: border-box;
}
.footer-canvas > div:first-child {
display: flex;
align-items: center;
}
.footer-canvas .catalog-mode {
padding: 1px;
position: relative;
}
.footer-canvas .catalog-mode i {
width: 16px;
height: 16px;
margin-right: 5px;
cursor: pointer;
display: inline-block;
background-image: url('@/assets/images/canvas-editor/catalog.svg');
}
.footer-canvas .page-mode {
padding: 1px;
position: relative;
}
.footer-canvas .page-mode i {
width: 16px;
height: 16px;
margin-right: 5px;
cursor: pointer;
display: inline-block;
background-image: url('@/assets/images/canvas-editor/page-mode.svg');
}
.footer-canvas .options {
width: 70px;
position: absolute;
left: 0;
bottom: 25px;
padding: 10px;
background: #fff;
font-size: 14px;
box-shadow: 0 2px 12px 0 rgb(56 56 56 / 20%);
border: 1px solid #e2e6ed;
border-radius: 2px;
display: none;
}
.footer-canvas .options.visible {
display: block;
}
.footer-canvas .options li {
padding: 5px;
margin: 5px 0;
user-select: none;
transition: all 0.3s;
text-align: center;
cursor: pointer;
}
.footer-canvas .options li:hover {
background-color: #ebecef;
}
.footer-canvas .options li.active {
background-color: #e2e6ed;
}
.footer-canvas > div:first-child > span {
display: inline-block;
margin-right: 5px;
letter-spacing: 1px;
}
.footer-canvas > div:last-child {
display: flex;
align-items: center;
justify-content: space-between;
}
.footer-canvas > div:last-child > div {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
}
.footer-canvas > div:last-child > div:hover {
background: rgba(25, 55, 88, 0.04);
}
.footer-canvas > div:last-child i {
width: 16px;
height: 16px;
display: inline-block;
cursor: pointer;
}
.footer-canvas .page-scale-minus i {
background-image: url('@/assets/images/canvas-editor/page-scale-minus.svg');
}
.footer-canvas .page-scale-add i {
background-image: url('@/assets/images/canvas-editor/page-scale-add.svg');
}
.footer-canvas .page-scale-percentage {
cursor: pointer;
user-select: none;
}
.footer-canvas .fullscreen i {
background-image: url('@/assets/images/canvas-editor/request-fullscreen.svg');
}
.footer-canvas .fullscreen.exist i {
background-image: url('@/assets/images/canvas-editor/exit-fullscreen.svg');
}
.footer-canvas .paper-margin i {
background-image: url('@/assets/images/canvas-editor/paper-margin.svg');
}
.footer-canvas .editor-mode {
cursor: pointer;
user-select: none;
}
.footer-canvas .paper-size {
position: relative;
}
.footer-canvas .paper-size i {
background-image: url('@/assets/images/canvas-editor/paper-size.svg');
}
.footer-canvas .paper-size .options {
right: 0;
left: unset;
}
.footer-canvas .paper-direction {
position: relative;
}
.footer-canvas .paper-direction i {
background-image: url('@/assets/images/canvas-editor/paper-direction.svg');
}
.footer-canvas .paper-direction .options {
right: 0;
left: unset;
}
.ce-contextmenu-signature {
background-image: url('@/assets/images/canvas-editor/signature.svg');
}
.ce-contextmenu-word-tool {
background-image: url('@/assets/images/canvas-editor/word-tool.svg');
}
</style>
引入使用组件
例:
<canvas-editor
v-if="!loadingInit"
:header="canvasEditor.header"
:main="canvasEditor.main"
:footer="canvasEditor.footer"
:options="canvasEditor.options"
@getCanvasEditorData="getCanvasEditorData"
/>
import CanvasEditor from '@/components/editor/index.vue';
components: {
CanvasEditor
},
canvasEditor: {
main: [],
options: [],
header: [],
footer: []
},
// getCanvasEditorData 事件
主要获取富文本 main内容 image的base64 options配置项 用于数据保存
getCanvasEditorData: ({ value, imgage, options }) => {
},
主要配置
// 页眉配置
header: {
type: Array,
default: []
},
// 主要编辑内容
main: {
type: Array,
default: []
},
// 页脚信息
footer: {
type: Array,
default: []
},
//
options: {
type: Object,
default: {}
},
// 批注 TODO
commentList: {
type: Array,
default: []
}
完整配置
interface IEditorOption {
mode?: EditorMode // 编辑器模式:编辑、清洁(不显示视觉辅助元素。如:分页符)、只读、表单(仅控件内可编辑)、打印(不显示辅助元素、未书写控件及前后括号)。默认:编辑
defaultType?: string // 默认元素类型。默认:TEXT
defaultColor?: string // 默认字体颜色。默认:#000000
defaultFont?: string // 默认字体。默认:Microsoft YaHei
defaultSize?: number // 默认字号。默认:16
minSize?: number // 最小字号。默认:5
maxSize?: number // 最大字号。默认:72
defaultBasicRowMarginHeight?: number // 默认行高。默认:8
defaultRowMargin?: number // 默认行间距。默认:1
defaultTabWidth?: number // 默认tab宽度。默认:32
width?: number // 纸张宽度。默认:794
height?: number // 纸张高度。默认:1123
scale?: number // 缩放比例。默认:1
pageGap?: number // 纸张间隔。默认:20
underlineColor?: string // 下划线颜色。默认:#000000
strikeoutColor?: string // 删除线颜色。默认:#FF0000
rangeColor?: string // 选区颜色。默认:#AECBFA
rangeAlpha?: number // 选区透明度。默认:0.6
rangeMinWidth?: number // 选区最小宽度。默认:5
searchMatchColor?: string // 搜索高亮颜色。默认:#FFFF00
searchNavigateMatchColor?: string // 搜索导航高亮颜色。默认:#AAD280
searchMatchAlpha?: number // 搜索高亮透明度。默认:0.6
highlightAlpha?: number // 高亮元素透明度。默认:0.6
resizerColor?: string // 图片尺寸器颜色。默认:#4182D9
resizerSize?: number // 图片尺寸器大小。默认:5
marginIndicatorSize?: number // 页边距指示器长度。默认:35
marginIndicatorColor?: string // 页边距指示器颜色。默认:#BABABA
margins?: IMargin // 页面边距。默认:[100, 120, 100, 120]
pageMode?: PageMode // 纸张模式:连页、分页。默认:分页
tdPadding?: IPadding // 单元格内边距。默认:[0, 5, 5, 5]
defaultTrMinHeight?: number // 默认表格行最小高度。默认:42
defaultColMinWidth?: number // 默认表格列最小宽度(整体宽度足够时应用,否则会按比例缩小)。默认:40
defaultHyperlinkColor?: string // 默认超链接颜色。默认:#0000FF
header?: IHeader // 页眉信息。{top?:number; maxHeightRadio?:MaxHeightRatio;}
footer?: IFooter // 页脚信息。{bottom?:number; maxHeightRadio?:MaxHeightRatio;}
pageNumber?: IPageNumber // 页码信息。{bottom:number; size:number; font:string; color:string; rowFlex:RowFlex; format:string; numberType:NumberType;}
paperDirection?: PaperDirection // 纸张方向:纵向、横向
inactiveAlpha?: number // 正文内容失焦时透明度。默认值:0.6
historyMaxRecordCount?: number // 历史(撤销重做)最大记录次数。默认:100次
printPixelRatio?: number // 打印像素比率(值越大越清晰,但尺寸越大)。默认:3
maskMargin?: IMargin // 编辑器上的遮盖边距(如悬浮到编辑器上的菜单栏、底部工具栏)。默认:[0, 0, 0, 0]
letterClass?: string[] // 排版支持的字母类。默认:a-zA-Z。内置可选择的字母表类:LETTER_CLASS
contextMenuDisableKeys?: string[] // 禁用的右键菜单。默认:[]
scrollContainerSelector?: string // 滚动区域选择器。默认:document
wordBreak?: WordBreak // 单词与标点断行:BREAK_WORD首行不出现标点&单词不拆分、BREAK_ALL按字符宽度撑满后折行。默认:BREAK_WORD
watermark?: IWatermark // 水印信息。{data:string; color?:string; opacity?:number; size?:number; font?:string;}
control?: IControlOption // 控件信息。 {placeholderColor?:string; bracketColor?:string; prefix?:string; postfix?:string; borderWidth?: number; borderColor?: string;}
checkbox?: ICheckboxOption // 复选框信息。{width?:number; height?:number; gap?:number; lineWidth?:number; fillStyle?:string; strokeStyle?: string;}
radio?: IRadioOption // 单选框信息。{width?:number; height?:number; gap?:number; lineWidth?:number; fillStyle?:string; strokeStyle?: string;}
cursor?: ICursorOption // 光标样式。{width?: number; color?: string; dragWidth?: number; dragColor?: string;}
title?: ITitleOption // 标题配置。{ defaultFirstSize?: number; defaultSecondSize?: number; defaultThirdSize?: number defaultFourthSize?: number; defaultFifthSize?: number; defaultSixthSize?: number;}
placeholder?: IPlaceholder // 编辑器空白占位文本
group?: IGroup // 成组配置。{opacity?:number; backgroundColor?:string; activeOpacity?:number; activeBackgroundColor?:string; disabled?:boolean}
pageBreak?: IPageBreak // 分页符配置。{font?:string; fontSize?:number; lineDash?:number[];}
zone?: IZoneOption // 编辑器区域配置。{tipDisabled?:boolean;}
background?: IBackgroundOption // 背景配置。{color?:string; image?:string; size?:BackgroundSize; repeat?:BackgroundRepeat;}。默认:{color: '#FFFFFF'}
lineBreak?: ILineBreakOption // 换行符配置。{disabled?:boolean; color?:string; lineWidth?:number;}
separator?: ISeparatorOption // 分隔符配置。{lineWidth?:number; strokeStyle?:string;}
}
页眉配置
interface IHeader {
top?: number // 距离页面顶部大小。默认:30
maxHeightRadio?: MaxHeightRatio // 占页面最大高度比。默认:HALF
disabled?: boolean // 是否禁用
}
页码配置
interface IPageNumber {
bottom?: number // 距离页面底部大小。默认:60
size?: number // 字体大小。默认:12
font?: string // 字体。默认:Microsoft YaHei
color?: string // 字体颜色。默认:#000000
rowFlex?: RowFlex // 行对齐方式。默认:CENTER
format?: string // 页码格式。默认:{pageNo}。示例:第{pageNo}页/共{pageCount}页
numberType?: NumberType // 数字类型。默认:ARABIC
disabled?: boolean // 是否禁用
startPageNo?: number // 起始页码。默认:1
fromPageNo?: number // 从第几页开始出现页码。默认:0
}
水印配置
interface IWatermark {
data: string // 文本。
color?: string // 颜色。默认:#AEB5C0
opacity?: number // 透明度。默认:0.3
size?: number // 字体大小。默认:200
font?: string // 字体。默认:Microsoft YaHei
}
占位文本配置
interface IPlaceholder {
data: string // 文本。
color?: string // 颜色。默认:#DCDFE6
opacity?: number // 透明度。默认:1
size?: number // 字体大小。默认:16
font?: string // 字体。默认:Microsoft YaHei
}
更多推荐
所有评论(0)