RichTextPreview.vue 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. <!-- eslint-disable vue/no-v-html -->
  2. <template>
  3. <div :class="['describe-preview']" :style="[getAreaStyle(), getComponentStyle()]">
  4. <SerialNumberPosition v-if="isEnable(data.property.sn_display_mode)" :property="data.property" />
  5. <div class="main">
  6. <div ref="leftDiv" :style="{ width: data.note_list?.length > 0 ? '' : '100%' }">
  7. <PinyinText
  8. v-if="isEnable(data.property.view_pinyin)"
  9. :paragraph-list="data.paragraph_list"
  10. :pinyin-position="data.property.pinyin_position"
  11. :pinyin-overall-position="data.property.pinyin_overall_position"
  12. :pinyin-size="data?.unified_attrib?.pinyin_size"
  13. :font-size="data?.unified_attrib?.font_size"
  14. :font-family="data?.unified_attrib?.font"
  15. :pinyin-padding="data.property.pinyin_padding"
  16. :is-preview="isPreview"
  17. />
  18. <span
  19. v-else
  20. class="rich-text"
  21. @click="handleRichFillClick"
  22. v-html="convertText(sanitizeHTML(data.content))"
  23. ></span>
  24. </div>
  25. <div v-show="showLang" class="lang">
  26. {{ data.multilingual?.find((item) => item.type === getLang())?.translation }}
  27. </div>
  28. </div>
  29. <el-dialog
  30. ref="optimizedDialog"
  31. title=""
  32. :visible.sync="noteDialogVisible"
  33. width="680px"
  34. :style="dialogStyle"
  35. :close-on-click-modal="false"
  36. destroy-on-close
  37. @close="noteDialogVisible = false"
  38. >
  39. <span v-html="sanitizeHTML(selectedNote)"></span>
  40. </el-dialog>
  41. </div>
  42. </template>
  43. <script>
  44. import { getRichTextData } from '@/views/book/courseware/data/richText';
  45. import PreviewMixin from '../common/PreviewMixin';
  46. import { isEnable } from '@/views/book/courseware/data/common';
  47. import PinyinText from '@/components/PinyinText.vue';
  48. export default {
  49. name: 'RichTextPreview',
  50. components: { PinyinText },
  51. mixins: [PreviewMixin],
  52. data() {
  53. return {
  54. isEnable,
  55. data: getRichTextData(),
  56. isPreview: true,
  57. divHeight: 'auto',
  58. observer: null,
  59. noteDialogVisible: false,
  60. selectedNote: '',
  61. dialogStyle: {
  62. position: 'fixed',
  63. top: '0',
  64. left: '0',
  65. margin: '0',
  66. },
  67. };
  68. },
  69. mounted() {
  70. this.observer = new ResizeObserver(() => {
  71. this.updateHeight();
  72. });
  73. this.observer.observe(this.$refs.leftDiv.closest('.describe-preview'));
  74. },
  75. beforeDestroy() {
  76. this.observer.disconnect();
  77. },
  78. methods: {
  79. handleRichFillClick(event) {
  80. // 检查点击的元素是否是 rich-fill 或者其子元素
  81. const richFillElement = event.target.closest('.rich-fill');
  82. if (richFillElement) {
  83. // 处理点击事件
  84. let selectedNoteId = richFillElement.dataset.annotationId;
  85. if (this.data.note_list.some((p) => p.id === selectedNoteId)) {
  86. this.noteDialogVisible = true;
  87. this.selectedNote = this.data.note_list.find((p) => p.id === selectedNoteId).note;
  88. }
  89. } else {
  90. this.selectedNote = '';
  91. this.noteDialogVisible = false;
  92. }
  93. this.$nextTick(() => {
  94. const dialogElement = this.$refs.optimizedDialog;
  95. // 确保对话框DOM已渲染
  96. if (!dialogElement) {
  97. return;
  98. }
  99. // 获取对话框内容区域的DOM元素
  100. const dialogContent = dialogElement.$el.querySelector('.el-dialog');
  101. if (!dialogContent) {
  102. return;
  103. }
  104. const dialogRect = dialogContent.getBoundingClientRect();
  105. const dialogWidth = dialogRect.width;
  106. const dialogHeight = dialogRect.height;
  107. const padding = 10; // 安全边距
  108. const clickX = event.clientX;
  109. const clickY = event.clientY;
  110. const windowWidth = window.innerWidth;
  111. const windowHeight = window.innerHeight;
  112. // 水平定位 - 中心对齐
  113. let left = clickX - dialogWidth / 2;
  114. // 边界检查
  115. left = Math.max(padding, Math.min(left, windowWidth - dialogWidth - padding));
  116. // 垂直定位 - 点击位置作为下边界中心
  117. let top = clickY - dialogHeight;
  118. // 上方空间不足时,改为向下展开
  119. if (top < padding) {
  120. top = clickY + padding;
  121. // 如果向下展开会超出屏幕,则贴底部显示
  122. if (top + dialogHeight > windowHeight - padding) {
  123. top = windowHeight - dialogHeight - padding;
  124. }
  125. }
  126. this.dialogStyle = {
  127. position: 'fixed',
  128. top: `${top - 20}px`,
  129. left: `${left}px`,
  130. margin: '0',
  131. transform: 'none',
  132. };
  133. });
  134. },
  135. updateHeight() {
  136. this.$nextTick(() => {
  137. const target = this.$refs.leftDiv;
  138. const parent = target.closest('.describe-preview');
  139. if (!parent) return;
  140. setTimeout(() => {
  141. this.divHeight = parent.offsetHeight - 16;
  142. }, 800);
  143. });
  144. },
  145. },
  146. };
  147. </script>
  148. <style lang="scss" scoped>
  149. @use '@/styles/mixin.scss' as *;
  150. .describe-preview {
  151. @include preview-base;
  152. &.middle {
  153. margin-top: calc($title-content-spacing - $component-spacing);
  154. margin-bottom: calc($title-content-spacing - $component-spacing);
  155. }
  156. &.top {
  157. margin-bottom: calc($title-content-spacing - $component-spacing);
  158. }
  159. &.bottom {
  160. margin-top: calc($title-content-spacing - $component-spacing);
  161. }
  162. :deep .el-dialog {
  163. position: fixed;
  164. margin: 0 !important;
  165. transition: all 0.2s; /* 添加平滑过渡效果 */
  166. .el-dialog__header {
  167. padding: 0 !important;
  168. .el-dialog__headerbtn {
  169. top: 6px !important;
  170. right: 6px !important;
  171. }
  172. }
  173. .el-dialog__body {
  174. padding: 10px !important;
  175. }
  176. }
  177. }
  178. </style>