RichTextPreview.vue 5.5 KB

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