RichTextPreview.vue 4.9 KB

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