Editor.vue 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. <template>
  2. <div class="main-container">
  3. <QuillEditor ref="myQuillEditorRef"
  4. theme="snow"
  5. v-model:content="data.content"
  6. :options="data.editorOption"
  7. contentType="delta"
  8. @update:content="setValue()"
  9. />
  10. </div>
  11. </template>
  12. <script setup lang="ts">
  13. import {QuillEditor, Quill, Delta} from '@vueup/vue-quill'
  14. import { reactive, onMounted, ref, toRaw, watch, defineProps } from 'vue'
  15. import '@vueup/vue-quill/dist/vue-quill.snow.css'
  16. import emitter from '@/utils/emitter'
  17. import dayjs from 'dayjs'
  18. import useApis from "@/apis";
  19. import {useRoute} from "vue-router";
  20. import {useStore} from "@/store";
  21. import type {engagementVO} from "@/types/vo.js";
  22. import {ElMessage} from "element-plus";
  23. const apis = useApis()
  24. const store = useStore()
  25. const route = useRoute();
  26. const assignmentId = Number(route.params.assignmentId);
  27. const myQuillEditorRef = ref()
  28. const data = reactive({
  29. content: {},
  30. editorOption: {
  31. modules: {
  32. toolbar: [
  33. ['bold', 'italic', 'underline', 'strike'],
  34. [{ 'size': ['small', false, 'large', 'huge'] }],
  35. [{ 'font': [] }],
  36. [{ 'align': [] }],
  37. [{ 'list': 'ordered' }, { 'list': 'bullet' }],
  38. [{ 'indent': '-1' }, { 'indent': '+1' }],
  39. [{ 'header': 1 }, { 'header': 2 }],
  40. ['image'],
  41. [{ 'direction': 'rtl' }],
  42. [{ 'color': [] }, { 'background': [] }]
  43. ]
  44. },
  45. placeholder: '请输入内容...'
  46. }
  47. })
  48. // 抛出更改内容
  49. const setValue = () => {
  50. const html = toRaw(myQuillEditorRef.value).getHTML()
  51. emitter.emit('get-html', html)
  52. const text = toRaw(myQuillEditorRef.value).getText()
  53. emitter.emit('get-text', text)
  54. const content1 = toRaw(myQuillEditorRef.value).getContents()
  55. emitter.emit('get-quill-content', content1)
  56. }
  57. /**
  58. * 合并所有insert内容,因为存在attribute的缘故,原内容被分割成很多个insert
  59. * @param ops
  60. */
  61. const processOps = (ops) => {
  62. let combinedInsert = '';
  63. ops.forEach(item => {
  64. if (item.insert) {
  65. combinedInsert += item.insert;
  66. }
  67. });
  68. return combinedInsert
  69. }
  70. //初始化Quill及行为监控
  71. const initQuill = () => {
  72. // 获取quill
  73. const quill = toRaw(myQuillEditorRef.value).getQuill()
  74. // 加入工具栏
  75. if (myQuillEditorRef.value) {
  76. quill.getModule('toolbar')
  77. }
  78. // 文本变化监控
  79. quill.on(Quill.events.TEXT_CHANGE, (...args) => {
  80. const [delta, oldDelta, source] = args;
  81. const deleteOps = delta.ops.filter(op => 'delete' in op);
  82. // 直接将 ops 数组中的属性合并到一个对象
  83. const opsObj = {};
  84. delta.ops.forEach(op => {
  85. Object.assign(opsObj, op);
  86. });
  87. delete opsObj.attributes;
  88. //有bug,当replace时,既有delete又有insert,此时会被误判为删除操作
  89. if(deleteOps.length > 0){ //删除操作
  90. // 获取删除操作的位置和长度
  91. const deleteStart = delta.ops.find(op => 'retain' in op).retain || 0; // 删除起始位置
  92. const deleteLength = delta.ops.find(op => 'delete' in op).delete; // 删除长度
  93. const oldContent = processOps(args["1"].ops)// 原内容
  94. // 提取被删除的具体内容
  95. const deletedContent = oldContent.slice(deleteStart, deleteStart + deleteLength);
  96. emitter.emit('change-record', {
  97. type: "delete",
  98. retain: 0,
  99. ...opsObj,
  100. oldContent,
  101. deletedContent,
  102. docLength: quill.getLength() - 1,
  103. letterCount: (quill.getText().match(/[a-zA-Z\d\u4e00-\u9fa5]/g) || []).length
  104. })
  105. } else {
  106. emitter.emit('change-record', {
  107. type: "insert",
  108. retain: 0,
  109. ...opsObj,
  110. docLength: quill.getLength() - 1,
  111. letterCount: (quill.getText().match(/[a-zA-Z\d\u4e00-\u9fa5]/g) || []).length,
  112. key: args["0"].ops.find(op => 'insert' in op).insert || null
  113. })
  114. }
  115. });
  116. // 光标变化监控
  117. quill.on(Quill.events.SELECTION_CHANGE, (...args) => {
  118. emitter.emit('change-record', {
  119. type: "cursor_move",
  120. retain: args["0"].index, // 将 index 属性重命名为 retain
  121. length: args["0"].length, // 保留 length 属性
  122. time: dayjs(new Date()).format("YYYY-MM-DD HH:mm:ss.SSS")
  123. });
  124. });
  125. }
  126. const fetchData = () => {
  127. apis.getEngagement(store.user.id, assignmentId)
  128. .then(res => {
  129. const engagementData = res.data.data;
  130. const engagement: engagementVO = Object.assign({}, engagementData);
  131. // data.content = engagement.textContent
  132. // const rawEditor = toRaw(myQuillEditorRef.value);
  133. // rawEditor.setContents(data.content); // 强制设置内容
  134. if(engagement.quillContent != null){
  135. data.content = new Delta(JSON.parse(engagement.quillContent));
  136. const rawEditor = toRaw(myQuillEditorRef.value);
  137. rawEditor.setContents(data.content); // 强制设置内容
  138. }
  139. })
  140. .catch(err => {
  141. console.log(err)
  142. })
  143. }
  144. // 初始化编辑器
  145. onMounted(() => {
  146. initQuill()
  147. fetchData()
  148. });
  149. </script>
  150. <script lang="ts">
  151. export default {
  152. name: "Editor"
  153. }
  154. </script>
  155. <style scoped lang="scss">
  156. // 调整样式
  157. .main-container {
  158. height: 80vh;
  159. border: solid 2px rgba(199, 199, 199, 0.3);
  160. }
  161. .main-container :deep(.ql-container) {
  162. height: 95%;
  163. max-height: 95%;
  164. background: #fff;
  165. }
  166. .main-container :deep(.ql-toolbar) {
  167. height: 5%;
  168. background: #fff;
  169. min-height: 40px;
  170. }
  171. .main-container :deep(.ql-formats) {
  172. height: 21px;
  173. line-height: 21px;
  174. }
  175. </style>