QuillEditor.vue 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523
  1. <template>
  2. <div>
  3. <!-- 图片上传组件辅助-->
  4. <el-upload
  5. class="avatar-uploader"
  6. action="notnull"
  7. :http-request="uploadFileFun"
  8. name="img"
  9. :headers="header"
  10. :show-file-list="false"
  11. :on-success="uploadSuccess"
  12. :on-error="uploadError"
  13. :before-upload="beforeUpload"
  14. v-show="false">
  15. <el-button class="stBtnUpload" size="small" type="primary">点击上传</el-button>
  16. </el-upload>
  17. <el-button style="display: none" @click="richtext()" size="small" type="primary">返回</el-button>
  18. <quill-editor
  19. class="editor"
  20. v-model="content"
  21. :ref="quillEditorRef"
  22. :options="editorOption"
  23. @blur="onEditorBlur($event)" @focus="onEditorFocus($event)"
  24. @change="onEditorChange($event)">
  25. </quill-editor>
  26. </div>
  27. </template>
  28. <script>
  29. let cusSpecial = ['α', 'β', 'γ'];
  30. // 工具栏配置
  31. const toolbarOptions = [
  32. ['bold', 'italic', 'underline', 'strike'], // 加粗 斜体 下划线 删除线
  33. ['blockquote', 'code-block', 'formula'], // 引用 代码块
  34. [{ header: 1 }, { header: 2 }], // 1、2 级标题
  35. [{ list: 'ordered' }, { list: 'bullet' }], // 有序、无序列表
  36. [{ script: 'sub' }, { script: 'super' }], // 上标/下标
  37. [{ indent: '-1' }, { indent: '+1' }], // 缩进
  38. // [{'direction': 'rtl'}], // 文本方向
  39. [{ size: ['small', false, 'large', 'huge'] }], // 字体大小
  40. [{ header: [1, 2, 3, 4, 5, 6, false] }], // 标题
  41. [{ color: [] }, { background: [] }], // 字体颜色、字体背景颜色
  42. [{ font: [] }], // 字体种类
  43. [{ align: [] }], // 对齐方式
  44. ['clean'], // 清除文本格式
  45. ['link', 'image', 'video'], // 链接、图片、视频
  46. cusSpecial, // 特殊符号
  47. ];
  48. import { quillEditor } from 'vue-quill-editor';
  49. import * as Quill from 'quill';
  50. import * as Delta from 'quill-delta';
  51. import ImageResize from 'quill-image-resize-module';
  52. import StImage from './QuillEditorStBlockEmbed';
  53. Quill.register('modules/imageResize', ImageResize);
  54. Quill.register('formats/stImage', StImage);
  55. import 'quill/dist/quill.core.css';
  56. import 'quill/dist/quill.snow.css';
  57. import 'quill/dist/quill.bubble.css';
  58. import { getUploadImg } from '@/api/AlCloud';
  59. import { getUploadConfig } from '@/api/base64';
  60. import axios from 'axios';
  61. import store from '@/store';
  62. export default {
  63. props: {
  64. /*编辑器的内容*/
  65. value: {
  66. type: String,
  67. },
  68. flg: {
  69. type: String,
  70. },
  71. quillEditorRef: {
  72. type: String,
  73. },
  74. /*图片大小*/
  75. maxSize: {
  76. type: Number,
  77. default: 4000, //kb
  78. },
  79. },
  80. components: {
  81. quillEditor,
  82. },
  83. data() {
  84. return {
  85. curLength: 0,
  86. uploadfileParam: {},
  87. content: this.value,
  88. quillUpdateImg: false, // 根据图片上传状态来确定是否显示loading动画,刚开始是false,不显示
  89. editorOption: {
  90. theme: 'snow', // or 'bubble'
  91. placeholder: '请输入内容...',
  92. modules: {
  93. toolbar: {
  94. container: toolbarOptions,
  95. handlers: {
  96. image: function (value) {
  97. if (value) {
  98. // 触发input框选择图片文件
  99. // this.$refs[this.btnRef].click();
  100. this.container.parentElement.parentElement.children[0].children[0].children[0].click();
  101. this.container.parentElement.parentElement.children[1].click();
  102. } else {
  103. this.quill.format('image', false);
  104. }
  105. },
  106. 'formula': (value) => {
  107. let quill = this.$refs[this.quillEditorRef].quill;
  108. this.curLength = quill.getSelection() ? quill.getSelection().index : 0;
  109. let chuck = this.getSelectionLatex();
  110. if (chuck) {
  111. this.formula.latex = chuck.latex;
  112. this.formula.type = chuck.type;
  113. } else {
  114. this.formula.latex = '';
  115. this.formula.type = '';
  116. }
  117. this.formula.dialogVisible = true;
  118. this.formula.quillEditorRef = this.quillEditorRef;
  119. store.commit({
  120. type: 'setFormula'
  121. , formula: this.formula,
  122. });
  123. },
  124. 'α': (value) => {
  125. let quill = this.$refs[this.quillEditorRef].quill;
  126. // 获取光标所在位置
  127. let length = quill.getSelection().index;
  128. // 插入图片 res.url为服务器返回的图片地址
  129. /*quill.insertEmbed(length, 'image', `${multipartParams.url}/${multipartParams.key}`);
  130. // 调整光标到最后
  131. quill.setSelection(length + 1);*/
  132. quill.insertText(length, 'α');
  133. quill.setSelection(length + 1);
  134. },
  135. 'β': (value) => {
  136. let quill = this.$refs[this.quillEditorRef].quill;
  137. // 获取光标所在位置
  138. let length = quill.getSelection().index;
  139. // 插入图片 res.url为服务器返回的图片地址
  140. /*quill.insertEmbed(length, 'image', `${multipartParams.url}/${multipartParams.key}`);
  141. // 调整光标到最后
  142. quill.setSelection(length + 1);*/
  143. quill.insertText(length, 'β');
  144. quill.setSelection(length + 1);
  145. },
  146. 'γ': (value) => {
  147. let quill = this.$refs[this.quillEditorRef].quill;
  148. // 获取光标所在位置
  149. let length = quill.getSelection().index;
  150. // 插入图片 res.url为服务器返回的图片地址
  151. /*quill.insertEmbed(length, 'image', `${multipartParams.url}/${multipartParams.key}`);
  152. // 调整光标到最后
  153. quill.setSelection(length + 1);*/
  154. quill.insertText(length, 'γ');
  155. quill.setSelection(length + 1);
  156. },
  157. },
  158. },
  159. imageResize: {},
  160. },
  161. },
  162. // serverUrl: "/v1/blog/imgUpload", // 这里写你要上传的图片服务器地址
  163. header: {
  164. // token: sessionStorage.token
  165. }, // 有的图片服务器要求请求头需要有token
  166. currentImgSrc: '',
  167. sttipinputw: 0,
  168. sttipinputh: 0,
  169. formula: {
  170. dialogVisible: false,
  171. imgsrc: '',
  172. latex: '',
  173. type: '',
  174. quillEditorRef: '',
  175. },
  176. };
  177. },
  178. methods: {
  179. // describe: 此功能用户放切屏点击图片失去焦点 author: Wgy date:2020-07-07
  180. richtext(){
  181. window.onblur = null;
  182. this.$emit('richText','richtext');
  183. },
  184. onEditorBlur() {
  185. //失去焦点事件
  186. },
  187. onEditorFocus() {
  188. //获得焦点事件
  189. },
  190. onEditorChange() {
  191. //内容改变事件
  192. this.$emit('syncValue', this.flg, this.content);
  193. },
  194. // 富文本图片上传前
  195. beforeUpload() {
  196. // 显示loading动画
  197. this.quillUpdateImg = true;
  198. },
  199. uploadSuccess(res, file) {
  200. },
  201. // 富文本图片上传失败
  202. uploadError() {
  203. // loading动画消失
  204. this.quillUpdateImg = false;
  205. },
  206. uploadFileFun(truck) {
  207. var file = truck.file;
  208. const suffix = file.name.split('.').pop();
  209. var dd = new Date();
  210. var y = dd.getFullYear();
  211. var m = dd.getMonth() + 1;//获取当前月份的日期
  212. const options = {
  213. prefix: 'resource/QuillEditor/' + y + m + '/',
  214. suffix: suffix,
  215. };
  216. getUploadImg(options).then((res) => {
  217. if (res.code === 0) {
  218. // 二进制文件通过forData对象进行传递
  219. const FormDataForAl = new FormData();
  220. const multipartParams = Object.assign({}, res.data, {
  221. Filename: `images/${file.name}`,
  222. success_action_status: '200',
  223. });
  224. // 参数数据
  225. FormDataForAl.append('key', multipartParams.key);
  226. FormDataForAl.append('policy', multipartParams.policy);
  227. FormDataForAl.append('signature', multipartParams.signature);
  228. FormDataForAl.append('OSSAccessKeyId', multipartParams.accessid);
  229. FormDataForAl.append('success_action_status', multipartParams.success_action_status);
  230. // OSS要求, file放到最后
  231. FormDataForAl.append('file', file);
  232. axios.post(multipartParams.uploadUrl, FormDataForAl).then(alRes => {
  233. if (alRes.status === 200) {
  234. let quill = this.$refs[this.quillEditorRef].quill;
  235. // 获取光标所在位置
  236. let length = quill.getSelection().index;
  237. // 插入图片 res.url为服务器返回的图片地址
  238. quill.insertEmbed(length, 'image', `${multipartParams.downloadUrl}/${multipartParams.key}`);
  239. // 调整光标到最后
  240. quill.setSelection(length + 1);
  241. }
  242. }).catch(alerr => {
  243. this.$message.error('图片插入失败');
  244. console.error('阿里云错误', alerr);
  245. });
  246. }
  247. });
  248. },
  249. clearContent() {
  250. this.content = '';
  251. },
  252. setContent(val) {
  253. this.content = val;
  254. },
  255. quillEditorAddEventListener() {
  256. let dom = this.$refs[this.quillEditorRef].$el.children[1].children[0];
  257. let quill = this.$refs[this.quillEditorRef].quill;
  258. let that = this;
  259. var pasteFun = function (e) {
  260. var clipboardData = e.clipboardData;
  261. if (!(clipboardData && clipboardData.items)) {//是否有粘贴内容
  262. return;
  263. }
  264. for (var i = 0; i < clipboardData.items.length; i++) {
  265. var item = clipboardData.items[i];
  266. if (item.kind === 'string' && item.type === 'text/plain') {
  267. item.getAsString((str) => {
  268. // str 是获取到的字符串,创建文本框
  269. //处理粘贴的文字内容
  270. setTimeout(() => {
  271. let length = quill.getSelection().index;
  272. // 插入
  273. quill.insertText(length, str);
  274. // 调整光标到最后
  275. quill.setSelection(length + str.length);
  276. }, 50);
  277. });
  278. } else if (item.kind === 'file') {//file 一般是各种截图base64数据
  279. var pasteFile = item.getAsFile();
  280. // pasteFile就是获取到的文件
  281. var reader = new FileReader();
  282. reader.onload = function (event) {
  283. var base64Img = event.target.result;
  284. // image/png;base64,
  285. let base64ImgPrefix = base64Img.substring(0, base64Img.indexOf(',') + 1);
  286. let base64ImgData = base64Img.substring(base64Img.indexOf(',') + 1);
  287. let opt = {
  288. 'data': base64ImgData,
  289. 'prefix': 'resource/',
  290. 'suffix': base64ImgPrefix.substring(base64ImgPrefix.indexOf('/') + 1, base64ImgPrefix.indexOf(';')),
  291. };
  292. getUploadConfig(opt).then(res => {
  293. if (res.code === 0) {
  294. let uri = res.data;
  295. // 获取光标所在位置
  296. let length = quill.getSelection().index;
  297. // 插入图片 res.url为服务器返回的图片地址
  298. quill.insertEmbed(length, 'image', uri);
  299. // 调整光标到最后
  300. quill.setSelection(length + 1);
  301. } else {
  302. that.$message.error('图片插入失败');
  303. }
  304. });
  305. }; // data url
  306. reader.readAsDataURL(pasteFile);
  307. }
  308. }
  309. };
  310. dom.removeEventListener('paste', pasteFun);
  311. dom.addEventListener('paste', pasteFun);
  312. },
  313. getSelectionLatex() {
  314. let type = '';
  315. let leaf = null;
  316. // 如果光标前一个是公式 取得公式的latex
  317. let quill = this.$refs[this.quillEditorRef].quill;
  318. let selection = quill.getSelection();
  319. if (!selection) {
  320. return null;
  321. }
  322. if (selection.length === 0) {
  323. type = 'rightSingle';
  324. leaf = quill.getLeaf(selection.index);
  325. } else {
  326. type = 'selected';
  327. leaf = quill.getLeaf(selection.index + 1);
  328. }
  329. // let contents = quill.getContents();
  330. // let line = quill.getLine(selection.index);
  331. // let bounds = quill.getBounds(selection.index);
  332. // console.log(selection, leaf);
  333. if (leaf[0] instanceof StImage) {
  334. // console.log(leaf[0].domNode.dataset.latex);
  335. return {
  336. type: type,
  337. latex: leaf[0].domNode.dataset.latex,
  338. };
  339. }
  340. return null;
  341. },
  342. // Delta operation
  343. delQuillContent(index) {
  344. let quill = this.$refs[this.quillEditorRef].quill;
  345. /*console.dir(quill);
  346. console.dir(quill.getSelection());
  347. console.dir(quill.getContents());*/
  348. // .insert('White', { color: '#fff' })
  349. var stDelta = new Delta().retain(index).delete(1);
  350. let quillContents = quill.getContents().compose(stDelta);
  351. quill.setContents(quillContents);
  352. },
  353. passValue() {
  354. // console.log('do passValue!!');
  355. let latex = this.formula.latex;
  356. if (latex.indexOf('placeholder') > -1) {
  357. this.formula.dialogVisible = false;
  358. let quill = this.$refs[this.quillEditorRef].quill;
  359. quill.setSelection(this.curLength);
  360. return;
  361. }
  362. if (latex && latex !== '') {
  363. if (this.formula.type === 'rightSingle') {
  364. // del formula
  365. this.delQuillContent(this.curLength - 1);
  366. let quill = this.$refs[this.quillEditorRef].quill;
  367. // 获取光标所在位置
  368. // let length = quill.getSelection() ? quill.getSelection().index : 0;
  369. // 插入图片 res.url为服务器返回的图片地址
  370. quill.insertEmbed(this.curLength - 1, 'stimage', {
  371. src: this.formula.imgsrc,
  372. 'data-latex': latex,
  373. });
  374. // quill.formatText(length, 1, { 'data-latex': latex });
  375. /*var delta = quill.getContents();
  376. console.log(delta);*/
  377. // 调整光标到最后
  378. // quill.insertText(length, img);
  379. quill.setSelection(this.curLength);
  380. } else if (this.formula.type === 'selected') {
  381. // del formula
  382. this.delQuillContent(this.curLength);
  383. let quill = this.$refs[this.quillEditorRef].quill;
  384. // 获取光标所在位置
  385. // let length = quill.getSelection() ? quill.getSelection().index : 0;
  386. // 插入图片 res.url为服务器返回的图片地址
  387. quill.insertEmbed(this.curLength, 'stimage', {
  388. src: this.formula.imgsrc,
  389. 'data-latex': latex,
  390. });
  391. // quill.formatText(length, 1, { 'data-latex': latex });
  392. /*var delta = quill.getContents();
  393. console.log(delta);*/
  394. // 调整光标到最后
  395. // quill.insertText(length, img);
  396. quill.setSelection(this.curLength + 1);
  397. } else {
  398. let quill = this.$refs[this.quillEditorRef].quill;
  399. // 获取光标所在位置
  400. // let length = quill.getSelection() ? quill.getSelection().index : 0;
  401. // 插入图片 res.url为服务器返回的图片地址
  402. quill.insertEmbed(this.curLength, 'stimage', {
  403. src: this.formula.imgsrc,
  404. 'data-latex': latex,
  405. });
  406. // quill.formatText(length, 1, { 'data-latex': latex });
  407. /*var delta = quill.getContents();
  408. console.log(delta);*/
  409. // 调整光标到最后
  410. // quill.insertText(length, img);
  411. quill.setSelection(this.curLength + 1);
  412. this.formula.dialogVisible = false;
  413. }
  414. this.formula.dialogVisible = false;
  415. }
  416. },
  417. },
  418. mounted() {
  419. let checkCount = 0;
  420. let checkCountMax = 100;
  421. let stInterval = setInterval(() => {
  422. if (this.quillEditorRef) {
  423. clearInterval(stInterval);
  424. this.$nextTick(function () {
  425. this.quillEditorAddEventListener();
  426. });
  427. /*let dom = this.$refs[this.quillEditorRef].$el.children[1].children[0];
  428. const specialSignals = document.querySelectorAll('.ql-special-signal');
  429. for (const specialSignal of specialSignals) {
  430. // specialSignal.style.cssText = "width:80px; border:1px solid #ccc; border-radius:5px;";
  431. // specialSignal.style.cssText = "width:50px;";
  432. // specialSignal.innerText="特殊符号";
  433. specialSignal.classList.add('el-icon-edit-outline');
  434. specialSignal.title = "特殊符号";
  435. }*/
  436. for (const special of cusSpecial) {
  437. const specialSignals = document.querySelectorAll('.ql-' + special);
  438. for (const specialSignal of specialSignals) {
  439. // specialSignal.style.cssText = "width:80px; border:1px solid #ccc; border-radius:5px;";
  440. // specialSignal.style.cssText = "width:50px;";
  441. specialSignal.innerText = special;
  442. // specialSignal.classList.add('el-icon-edit-outline');
  443. specialSignal.title = special;
  444. }
  445. }
  446. } else {
  447. if (checkCount > checkCountMax) {
  448. clearInterval(stInterval);
  449. }
  450. }
  451. checkCount++;
  452. }, 100);
  453. /*监听富文本复制粘贴*/
  454. document.onpaste = function (e) {
  455. let arrPath = e.path;
  456. let has = false;
  457. if (arrPath) {
  458. for (const path of arrPath) {
  459. if (path.className && path.className.indexOf('ql-editor') > -1) {
  460. has = true;
  461. break;
  462. }
  463. }
  464. }
  465. if (has) {
  466. e.preventDefault();
  467. }
  468. };
  469. },
  470. watch: {
  471. '$store.state.formula': {
  472. handler(newVal, oldVal) {
  473. // console.log('in store.state.formula');
  474. // console.log(newVal);
  475. this.formula = newVal;
  476. /*console.dir('======start=======');
  477. console.dir(this.formula.quillEditorRef);
  478. console.dir(this.quillEditorRef);
  479. console.dir('======end=======');*/
  480. if (this.formula.dialogVisible === false && this.formula.quillEditorRef === this.quillEditorRef) {
  481. this.passValue();
  482. }
  483. },
  484. deep: true,
  485. },
  486. },
  487. };
  488. </script>