RichTextPreview.vue 5.3 KB

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