dsy пре 5 дана
родитељ
комит
4f133b9fec
31 измењених фајлова са 458 додато и 73 уклоњено
  1. 1 0
      .gitignore
  2. 1 2
      src/components/CommonPreview.vue
  3. 177 4
      src/components/PinyinText.vue
  4. 1 2
      src/courseware_preview/index.vue
  5. 1 1
      src/main.js
  6. 1 2
      src/mobile_preview/index.vue
  7. 1 0
      src/views/book/courseware/preview/CoursewarePreview.vue
  8. 1 1
      src/views/book/courseware/preview/common/PreviewOperation.vue
  9. 3 3
      src/views/book/courseware/preview/components/article/PhraseModelChs.vue
  10. 16 15
      src/views/book/courseware/preview/components/article/Voicefullscreen.vue
  11. 5 4
      src/views/book/courseware/preview/components/article/components/AudioRed.vue
  12. 7 3
      src/views/book/courseware/preview/components/article/components/WordPhraseDetail.vue
  13. 1 1
      src/views/book/courseware/preview/components/article/components/Wordcard.vue
  14. 73 1
      src/views/book/courseware/preview/components/article/index.vue
  15. 21 0
      src/views/book/courseware/preview/components/common/PreviewMixin.js
  16. 3 3
      src/views/book/courseware/preview/components/dialogue_article/PhraseModelChs.vue
  17. 74 1
      src/views/book/courseware/preview/components/dialogue_article/index.vue
  18. 2 0
      src/views/book/courseware/preview/components/fill/FillPreview.vue
  19. 4 2
      src/views/book/courseware/preview/components/judge/JudgePreview.vue
  20. 7 1
      src/views/book/courseware/preview/components/matching/MatchingPreview.vue
  21. 3 2
      src/views/book/courseware/preview/components/math/MathPreview.vue
  22. 8 5
      src/views/book/courseware/preview/components/new_word/NewWordPreview.vue
  23. 12 6
      src/views/book/courseware/preview/components/new_word/components/WordPhraseDetail.vue
  24. 4 1
      src/views/book/courseware/preview/components/rich_text/RichTextPreview.vue
  25. 4 2
      src/views/book/courseware/preview/components/select/SelectPreview.vue
  26. 7 1
      src/views/book/courseware/preview/components/sort/SortPreview.vue
  27. 3 0
      src/views/book/courseware/preview/components/table/TablePreview.vue
  28. 10 3
      src/views/book/courseware/preview/components/video_interaction/VideoInteractionPreview.vue
  29. 4 3
      src/views/book/courseware/preview/components/voice_matrix/components/AudioLine.vue
  30. 2 3
      src/web_preview/index.vue
  31. 1 1
      vue.config.js

+ 1 - 0
.gitignore

@@ -2,6 +2,7 @@
 node_modules
 /dist
 /out
+智慧梧桐数字教材预览
 
 # local env files
 .env.local

+ 1 - 2
src/components/CommonPreview.vue

@@ -36,7 +36,7 @@
             :data="data"
             :component-list="component_list"
             :background="background"
-            :can-remark="isTrue(courseware_info.is_my_audit_task) && isTrue(courseware_info.is_can_add_audit_remark)"
+            :can-remark="false"
             :show-remark="isShowAudit"
             :component-remark-obj="remark_list_obj"
             @computeScroll="computeScroll"
@@ -376,7 +376,6 @@ export default {
       GetLanguageTypeList({
         book_id: this.courseware_info.book_id,
         is_contain_zh: 'true',
-        is_only_can_translation: 'true',
       }).then(({ language_type_list }) => {
         this.langList = language_type_list;
       });

+ 177 - 4
src/components/PinyinText.vue

@@ -1,3 +1,4 @@
+<!-- eslint-disable vue/no-v-html -->
 <template>
   <div class="pinyin-area" :style="{ 'text-align': pinyinOverallPosition, padding: pinyinPadding }">
     <AudioPlay
@@ -14,7 +15,7 @@
           v-if="block.type === 'text'"
           :key="'text-' + index"
           class="pinyin-sentence"
-          :style="[{ 'align-items': pinyinPosition === 'top' ? 'flex-end' : 'flex-start' }]"
+          :style="[{ 'align-items': pinyinPosition === 'top' ? 'flex-end' : 'flex-start' }, block.sentenceStyle]"
         >
           <span v-for="(word, wIndex) in block.word_list" :key="wIndex" class="pinyin-text" :style="block.styleObj">
             <span
@@ -104,6 +105,15 @@
             您的浏览器不支持音频播放
           </audio>
         </span>
+
+        <!-- 数学公式块(保留 MathJax 原始结构) -->
+        <MathHtml
+          v-else-if="block.type === 'math'"
+          :key="'math-' + index"
+          :html="block.html"
+          :style-obj="block.containerStyle"
+          :is-display="block.isDisplay"
+        />
         <!-- 换行符 -->
         <br v-else-if="block.type === 'newline'" :key="'newline-' + index" />
       </template>
@@ -176,11 +186,44 @@ import { sanitizeHTML } from '@/utils/common';
 import { isEnable } from '@/views/book/courseware/data/common';
 import AudioPlay from '@/views/book/courseware/preview/components/character_base/components/AudioPlay.vue';
 
+// MathHtml 组件用于渲染数学公式的 HTML 内容,保留 MathJax 的原始结构
+const MathHtml = {
+  name: 'MathHtml',
+  functional: true,
+  props: {
+    html: {
+      type: String,
+      default: '',
+    },
+    styleObj: {
+      type: Object,
+      default: () => ({}),
+    },
+    isDisplay: {
+      type: Boolean,
+      default: false,
+    },
+  },
+  render(h, context) {
+    return h('span', {
+      class: {
+        'math-container': true,
+        'math-display': context.props.isDisplay,
+      },
+      style: context.props.styleObj,
+      domProps: {
+        innerHTML: sanitizeHTML(context.props.html || ''),
+      },
+    });
+  },
+};
+
 export default {
   name: 'PinyinText',
   components: {
     CorrectPinyin,
     AudioPlay,
+    MathHtml,
   },
   inject: ['convertText'],
   props: {
@@ -301,10 +344,10 @@ export default {
 
             // 如果找到了结束标签,合并完成,跳出内部循环
             if (nextItem.text && nextItem.text.includes('</video>')) {
-              j++; // 跳过结束标签
+              j += 1; // 跳过结束标签
               break;
             }
-            j++;
+            j += 1;
           }
 
           // 将合并后的内容作为一个新项加入结果
@@ -319,7 +362,7 @@ export default {
         } else {
           // 其他项直接加入
           result.push(item);
-          i++;
+          i += 1;
         }
       }
 
@@ -335,10 +378,33 @@ export default {
       let oldIndex = -1;
       let paragraphIndex = 0;
       const tagStack = [];
+      let mathContext = null;
 
       for (const item of listToParse) {
         oldIndex += 1;
 
+        if (mathContext) {
+          mathContext.html += this.getRawItemText(item);
+
+          if (item.is_style === 'true' || item.is_style === true) {
+            this.updateHtmlTagStack(mathContext.stack, item.text || '');
+            if (mathContext.stack.length === 0) {
+              blocks.push(this.parseMathBlock(mathContext.html, tagStack));
+              mathContext = null;
+            }
+          }
+          continue;
+        }
+
+        if (this.isMathRootTag(item)) {
+          const rootTag = this.extractTagName(item.text || '');
+          mathContext = {
+            html: this.getRawItemText(item),
+            stack: rootTag ? [rootTag] : ['span'],
+          };
+          continue;
+        }
+
         if (item.text && typeof item.text === 'string' && item.text.includes('<img')) {
           blocks.push(this.parseImageBlock(item, tagStack));
         } else if (item.text && typeof item.text === 'string' && item.text.includes('<video')) {
@@ -363,6 +429,10 @@ export default {
         }
       }
 
+      if (mathContext) {
+        blocks.push(this.parseMathBlock(mathContext.html, tagStack));
+      }
+
       return blocks;
     },
   },
@@ -607,6 +677,13 @@ export default {
 
       const styleObj = combinedStyle ? this.parseStyleToObject(combinedStyle) : null;
 
+      // 提取 paddingLeft 用于段落缩进,从 styleObj 中移除
+      const sentenceStyle = {};
+      if (styleObj && styleObj.paddingLeft) {
+        sentenceStyle.paddingLeft = styleObj.paddingLeft;
+        delete styleObj.paddingLeft;
+      }
+
       return {
         type: 'text',
         word_list: item.word_list,
@@ -614,6 +691,7 @@ export default {
         oldIndex,
         paragraphIndex,
         styleObj,
+        sentenceStyle,
         rawStyleText: combinedStyle,
         hasEmphasisDot,
       };
@@ -628,6 +706,59 @@ export default {
       return styleObj;
     },
 
+    parseMathBlock(html, tagStack) {
+      const containerStyleObj = {};
+      tagStack.forEach((tagItem) => {
+        if (tagItem.style) {
+          this.mergeStyleString(containerStyleObj, tagItem.style);
+        }
+      });
+
+      const isDisplay =
+        /<mjx-container[^>]*\bdisplay=["']true["']/i.test(html) ||
+        /<mjx-assistive-mml[^>]*\bdisplay=["']block["']/i.test(html);
+
+      return {
+        type: 'math',
+        html,
+        containerStyle: containerStyleObj,
+        isDisplay,
+      };
+    },
+
+    isMathRootTag(item) {
+      if (!item || !(item.is_style === 'true' || item.is_style === true) || !item.text) return false;
+      return /class=["'][^"']*\bmathjax-container\b[^"']*\beditor-math\b[^"']*["']/i.test(item.text);
+    },
+
+    getRawItemText(item) {
+      if (!item) return '';
+      return item.text || '';
+    },
+
+    extractTagName(tagText) {
+      const match = (tagText || '').match(/^<\/?\s*([a-zA-Z][\w-]*)/);
+      return match ? match[1].toLowerCase() : '';
+    },
+
+    updateHtmlTagStack(tagStack, tagText) {
+      if (!tagText || !tagStack) return;
+
+      const tagName = this.extractTagName(tagText);
+      if (!tagName) return;
+
+      if (/^<\//.test(tagText)) {
+        for (let i = tagStack.length - 1; i >= 0; i--) {
+          if (tagStack[i] === tagName) {
+            tagStack.splice(i, 1);
+            break;
+          }
+        }
+      } else if (!/\/>\s*$/.test(tagText)) {
+        tagStack.push(tagName);
+      }
+    },
+
     getBlockColor(block) {
       const colorFromObject = block?.styleObj?.color;
       if (colorFromObject) return colorFromObject;
@@ -726,6 +857,32 @@ export default {
         this.isAllSetting = false;
       }
 
+      // 优先取文字单独的字号
+      let _fontSize = block.styleObj && block.styleObj.fontSize ? block.styleObj.fontSize : null;
+      // 如果没有,取继承的字号
+      if (!_fontSize && item.activeTextStyle && item.activeTextStyle.fontSize) {
+        _fontSize = item.activeTextStyle.fontSize;
+      }
+      // 如果没有,取组件的字号
+      if (!_fontSize && this.bodyStyles.fontSize) _fontSize = this.bodyStyles.fontSize;
+      // 如果没有,取全局字号
+      if (!_fontSize && this.fontSize) _fontSize = this.fontSize;
+
+      if (_fontSize) {
+        // 取出字号里面的数字,做计算处理之后,自动加上原来的单位
+        const fontSizeMatch = _fontSize.match(/^([\d.]+)(\D*)$/);
+        if (fontSizeMatch) {
+          const fontSizeValue = parseFloat(fontSizeMatch[1]);
+          const fontSizeUnit = fontSizeMatch[2] || 'px';
+
+          // 根据字号大小确定比例:16号及以下75%,16号以上60%
+          const ratio = fontSizeValue <= 16 ? 0.75 : 0.6;
+          const calculatedSize = Math.round(fontSizeValue * ratio);
+
+          // 保留原始单位
+          styles['font-size'] = `${calculatedSize}${fontSizeUnit}`;
+        }
+      }
       // 明确重置不应该继承的样式
       styles['text-decoration'] = 'none';
       styles['font-style'] = 'normal';
@@ -793,6 +950,22 @@ export default {
       margin: 0.5em 0;
     }
 
+    :deep(.mathjax-container.editor-math > svg[aria-hidden='true']) {
+      display: none !important;
+    }
+
+    .math-container.math-display {
+      display: block !important;
+      width: 100%;
+      text-align: center;
+    }
+
+    .math-container.math-display :deep(mjx-container[display='true']) {
+      display: block !important;
+      margin: 0 auto;
+      text-align: center;
+    }
+
     span {
       display: inline-flex;
     }

+ 1 - 2
src/courseware_preview/index.vue

@@ -46,7 +46,7 @@
             :courseware-id="curSelectId"
             :component-list="component_list"
             :background="background"
-            :can-remark="isTrue(courseware_info.is_my_audit_task) && isTrue(courseware_info.is_can_add_audit_remark)"
+            :can-remark="false"
             :show-remark="false"
             :project="project"
             :component-remark-obj="remark_list_obj"
@@ -459,7 +459,6 @@ export default {
       GetLanguageTypeList({
         book_id: this.courseware_info.book_id,
         is_contain_zh: 'true',
-        is_only_can_translation: 'true',
       }).then(({ language_type_list }) => {
         this.langList = language_type_list;
       });

+ 1 - 1
src/main.js

@@ -14,7 +14,7 @@ import ElementUI from 'element-ui';
 import '@/styles/element-variables.scss';
 
 import '@/utils/mathjax'; // 必须在引入mathjax前引入mathjax的配置文件
-import 'mathjax/es5/tex-mml-chtml';
+import 'mathjax/es5/tex-mml-svg';
 
 import VueSignaturePad from 'vue-signature-pad';
 

+ 1 - 2
src/mobile_preview/index.vue

@@ -22,7 +22,7 @@
             :data="data"
             :component-list="component_list"
             :background="background"
-            :can-remark="isTrue(courseware_info.is_my_audit_task) && isTrue(courseware_info.is_can_add_audit_remark)"
+            :can-remark="false"
             :show-remark="false"
             :component-remark-obj="remark_list_obj"
             @computeScroll="computeScroll"
@@ -266,7 +266,6 @@ export default {
       GetLanguageTypeList({
         book_id: this.courseware_info.book_id,
         is_contain_zh: 'true',
-        is_only_can_translation: 'true',
       }).then(({ language_type_list }) => {
         this.langList = language_type_list;
       });

+ 1 - 0
src/views/book/courseware/preview/CoursewarePreview.vue

@@ -93,6 +93,7 @@ export default {
     return {
       getDragStatus: () => false,
       bookInfo: this.bookInfo,
+      getCoursewareId: () => this.courseware_id,
     };
   },
   props: {

+ 1 - 1
src/views/book/courseware/preview/common/PreviewOperation.vue

@@ -1,7 +1,7 @@
 <template>
   <div ref="operation" class="operation" :class="{ 'is-wrapped': isWrapped }">
     <!-- 重做 -->
-    <div v-show="permissionControl.can_answer" class="button retry" @click="retry()"></div>
+    <div v-show="permissionControl.can_answer && isShowRetry" class="button retry" @click="retry()"></div>
     <!-- 判断对错 -->
     <div
       v-show="permissionControl.can_judge_correct && isShowAnswer"

+ 3 - 3
src/views/book/courseware/preview/components/article/PhraseModelChs.vue

@@ -1336,9 +1336,9 @@
           :current-tree-i-d="currentTreeID"
           :TaskModel="TaskModel"
           :write-list="curQue.Bookanswer.writeModel"
-          :mp3-url="activeWord ? activeWord.newWordMp3 : ''"
-          :bg="activeWord ? activeWord.bg : null"
-          :ed="activeWord ? activeWord.ed : null"
+          :mp3-url="activeWord ? activeWord.newWordMp3 || activeWord.audio_file_url : ''"
+          :bg="activeWord ? activeWord.audio_begin_time || activeWord.bg : null"
+          :ed="activeWord ? activeWord.audio_end_time || activeWord.ed : null"
           :attrib="attrib"
           :is-mobile="isMobile"
           @changeCurQue="changeCurQue"

+ 16 - 15
src/views/book/courseware/preview/components/article/Voicefullscreen.vue

@@ -655,6 +655,7 @@ export default {
     Wordcard,
     Notecard,
   },
+  inject: ['convertText'],
   props: [
     'sentList',
     'sentIndex',
@@ -672,7 +673,6 @@ export default {
     'attrib',
     'isMobile',
   ],
-  inject: ['convertText'],
   data() {
     return {
       pySize: 32,
@@ -1220,9 +1220,9 @@ export default {
         this.NNPENewWordList.forEach((itemn) => {
           // items.forEach((itemn) => {
           if (itemn.new_word === new_word) {
-            this.wordbg = itemn.bg;
-            this.worded = itemn.ed;
-            this.wordPlayMp3 = itemn.newWordMp3;
+            this.wordbg = itemn.audio_begin_time || itemn.bg;
+            this.worded = itemn.audio_end_time || itemn.ed;
+            this.wordPlayMp3 = itemn.newWordMp3 || itemn.audio_file_url;
           }
           // });
         });
@@ -1280,9 +1280,9 @@ export default {
         this.NNPENewWordList.forEach((itemn) => {
           // items.forEach((itemn) => {
           if (itemn.new_word === new_word) {
-            this.wordbgs = itemn.bg;
-            this.wordeds = itemn.ed;
-            this.wordPlayMp3 = itemn.newWordMp3;
+            this.wordbgs = itemn.audio_begin_time || itemn.bg;
+            this.wordeds = itemn.audio_end_time || itemn.ed;
+            this.wordPlayMp3 = itemn.newWordMp3 || itemn.audio_file_url;
           }
           // });
         });
@@ -1317,9 +1317,9 @@ export default {
         this.NNPENewWordList.forEach((itemn) => {
           // items.forEach((itemn) => {
           if (itemn.new_word === new_word) {
-            this.wordbgs = itemn.bg;
-            this.wordeds = itemn.ed;
-            this.wordPlayMp3 = itemn.newWordMp3;
+            this.wordbgs = itemn.audio_begin_time || itemn.bg;
+            this.wordeds = itemn.audio_end_time || itemn.ed;
+            this.wordPlayMp3 = itemn.newWordMp3 || itemn.audio_file_url;
           }
           // });
         });
@@ -1464,7 +1464,8 @@ export default {
   &.bg2 {
     background: linear-gradient(180deg, #274533 0%, #385f45 100%);
 
-    .newActive {
+    .NNPE-words > span.NNPE-chs .newActive,
+    .NNPE-words-box > span.NNPE-chs .newActive {
       color: #ffc600 !important;
     }
   }
@@ -1905,7 +1906,7 @@ export default {
             }
 
             &.font-white {
-              color: #fff;
+              color: #fff !important;
             }
 
             &.wordBlank {
@@ -1926,7 +1927,7 @@ export default {
             }
 
             .font-white {
-              color: #fff;
+              color: #fff !important;
             }
 
             .active {
@@ -1984,7 +1985,7 @@ export default {
           }
 
           &.font-white {
-            color: #fff;
+            color: #fff !important;
           }
 
           &.noFont {
@@ -2013,7 +2014,7 @@ export default {
           }
 
           .font-white {
-            color: #fff;
+            color: #fff !important;
           }
 
           .active {

+ 5 - 4
src/views/book/courseware/preview/components/article/components/AudioRed.vue

@@ -5,7 +5,7 @@
       <div
         class="icon-mask icon-compare-play"
         :style="{
-          backgroundColor: attrib?.topic_color,
+          backgroundColor: attrib ? attrib.topic_color : '#e35454',
         }"
         :class="type == 'full' ? 'icon-big' : ''"
         v-if="isPlaying"
@@ -13,7 +13,7 @@
       <div
         class="icon-mask icon-compare-pause"
         :style="{
-          backgroundColor: attrib?.topic_color,
+          backgroundColor: attrib ? attrib.topic_color : '#e35454',
         }"
         :class="type == 'full' ? 'icon-big' : ''"
         v-else
@@ -23,7 +23,7 @@
       <div
         class="icon-mask icon-normal-play"
         :style="{
-          backgroundColor: attrib?.topic_color,
+          backgroundColor: attrib ? attrib.topic_color : '#e35454',
         }"
         :class="type == 'full' ? 'icon-big' : ''"
         v-if="isPlaying"
@@ -31,7 +31,7 @@
       <div
         class="icon-mask icon-normal-pause"
         :style="{
-          backgroundColor: attrib?.topic_color,
+          backgroundColor: attrib ? attrib.topic_color : '#e35454',
         }"
         :class="type == 'full' ? 'icon-big' : ''"
         v-else
@@ -169,6 +169,7 @@ export default {
 
   img,
   .icon-mask {
+    flex-shrink: 0;
     float: left;
     width: 20px;
     height: 20px;

+ 7 - 3
src/views/book/courseware/preview/components/article/components/WordPhraseDetail.vue

@@ -68,13 +68,16 @@
             <div class="yinpin">
               <span class="pinyintext"> {{ data.pinyin.toLowerCase() }}</span>
               <template v-if="data.newWordMp3">
-                <Audio :mp3="data.newWordMp3" :theme-color="themeColor" :bg="bg" :ed="ed" />
+                <Audio :mp3="data.newWordMp3" :attrib="attrib" :theme-color="themeColor" :bg="bg" :ed="ed" />
               </template>
               <template v-else-if="data.mp3_list && data.mp3_list[0] && data.mp3_list[0].id">
-                <Audio :mp3="data.mp3_list[0].id" :theme-color="themeColor" />
+                <Audio :mp3="data.mp3_list[0].id" :attrib="attrib" :theme-color="themeColor" />
               </template>
               <template v-else-if="data.mp3Url">
-                <Audio :mp3="data.mp3Url" :theme-color="themeColor" :bg="bg" :ed="ed" />
+                <Audio :mp3="data.mp3Url" :attrib="attrib" :theme-color="themeColor" :bg="bg" :ed="ed" />
+              </template>
+              <template v-else-if="data.audio_file_url">
+                <AudioRed :mp3="data.audio_file_url" :attrib="attrib" :theme-color="themeColor" :bg="bg" :ed="ed" />
               </template>
             </div>
             <p
@@ -1066,6 +1069,7 @@ export default {
             line-height: 30px;
             color: #2c2c2c;
             word-break: normal;
+            white-space: nowrap;
           }
 
           .content-voices {

+ 1 - 1
src/views/book/courseware/preview/components/article/components/Wordcard.vue

@@ -8,7 +8,7 @@
       <div v-if="word.detail" class="bwc-top">
         <span v-if="word.detail.pinyin">{{ word.detail.pinyin.toLowerCase() }}</span>
         <div style="margin-left: 4px">
-          <Audio :mp3="mp3Url ? mp3Url : ''" :theme-color="attrib ? attrib.topic_color : '#e35454'" :bg="bg" :ed="ed" />
+          <Audio :mp3="mp3Url ? mp3Url : ''" :attrib="attrib" :bg="bg" :ed="ed" />
         </div>
 
         <!-- <img

+ 73 - 1
src/views/book/courseware/preview/components/article/index.vue

@@ -542,6 +542,77 @@ export default {
         //   this.data.mp3_list[0].url = url_map[this.data.mp3_list[0].file_id];
         // });
       }
+      // 处理课文里生词表的音频和时间
+      if (this.data.new_word_list && this.data.new_word_list.new_word_list) {
+        if (this.data.new_word_list.audio_data && this.data.new_word_list.audio_data.temporary_url) {
+          // 如果大音频存在
+          this.data.new_word_list.new_word_list.forEach((item, index) => {
+            this.$set(item, 'audio_file_url', this.data.new_word_list.audio_data.temporary_url);
+            this.$set(
+              item,
+              'audio_begin_time',
+              this.data.new_word_list.lrc_arr[index] ? this.data.new_word_list.lrc_arr[index].bg : null,
+            );
+            this.$set(
+              item,
+              'audio_end_time',
+              this.data.new_word_list.lrc_arr[index] ? this.data.new_word_list.lrc_arr[index].ed : null,
+            );
+          });
+        } else {
+          let mp3_list = [];
+          this.data.new_word_list.new_word_list.forEach((item, index) => {
+            mp3_list.push(item.mp3_list);
+          });
+          GetFileURLMap({ file_id_list: mp3_list }).then(({ url_map }) => {
+            this.data.new_word_list.new_word_list.forEach((item, index) => {
+              this.$set(item, 'audio_file_url', url_map[item.mp3_list]);
+              this.$set(item, 'audio_begin_time', null);
+              this.$set(item, 'audio_end_time', null);
+            });
+          });
+        }
+      }
+      // 处理课文里其他词汇表的音频和时间
+      if (this.data.other_word_list && this.data.other_word_list.new_word_list) {
+        if (this.data.other_word_list.audio_data && this.data.other_word_list.audio_data.temporary_url) {
+          // 如果大音频存在
+          this.data.other_word_list.new_word_list.forEach((item, index) => {
+            this.$set(item, 'audio_file_url', this.data.other_word_list.audio_data.temporary_url);
+            this.$set(
+              item,
+              'audio_begin_time',
+              this.data.other_word_list.lrc_arr[index] ? this.data.other_word_list.lrc_arr[index].bg : null,
+            );
+            this.$set(
+              item,
+              'audio_end_time',
+              this.data.other_word_list.lrc_arr[index] ? this.data.other_word_list.lrc_arr[index].ed : null,
+            );
+          });
+        } else {
+          let mp3_list = [];
+          this.data.other_word_list.new_word_list.forEach((item, index) => {
+            mp3_list.push(item.mp3_list);
+          });
+          GetFileURLMap({ file_id_list: mp3_list }).then(({ url_map }) => {
+            this.data.other_word_list.new_word_list.forEach((item, index) => {
+              this.$set(item, 'audio_file_url', url_map[item.mp3_list]);
+              this.$set(item, 'audio_begin_time', null);
+              this.$set(item, 'audio_end_time', null);
+            });
+          });
+        }
+      }
+      if (this.data.new_word_list_other_component_input) {
+        this.data.new_word_list_other_component_input.forEach((item) => {
+          if (item.audio_begin_time === -1) {
+            item.audio_begin_time = null;
+            item.audio_end_time = null;
+          }
+        });
+      }
+
       this.NNPENewWordList = (
         this.data.new_word_list_other_component_input ? this.data.new_word_list_other_component_input : []
       )
@@ -830,7 +901,8 @@ export default {
   .main {
     display: grid;
     row-gap: 24px;
-    align-items: center;
+
+    // align-items: center;
   }
 
   .NPC-ArticleView {

+ 21 - 0
src/views/book/courseware/preview/components/common/PreviewMixin.js

@@ -325,6 +325,27 @@ const mixin = {
         content: JSON.stringify(content),
       });
     },
+
+    /**
+     * @description 从统一属性中获取样式
+     * @returns {Object} 样式对象
+     */
+    getBodyStyles() {
+      const styles = {};
+      const unifiedAttrib = this.data?.unified_attrib || {};
+
+      if (unifiedAttrib.font) {
+        styles['font-family'] = unifiedAttrib.font;
+      }
+      if (unifiedAttrib.font_size) {
+        styles['font-size'] = unifiedAttrib.font_size;
+      }
+      if (unifiedAttrib.text_color) {
+        styles['color'] = unifiedAttrib.text_color;
+      }
+
+      return styles;
+    },
   },
 };
 

+ 3 - 3
src/views/book/courseware/preview/components/dialogue_article/PhraseModelChs.vue

@@ -1436,9 +1436,9 @@ export default {
       _this.NNPENewWordList.forEach((itemn) => {
         // items.forEach((itemn) => {
         if (itemn.new_word === noteNum) {
-          _this.wordbgs = itemn.bg;
-          _this.wordeds = itemn.ed;
-          _this.wordPlayMp3 = itemn.newWordMp3;
+          _this.wordbgs = itemn.audio_begin_time || itemn.bg;
+          _this.wordeds = itemn.audio_end_time || itemn.ed;
+          _this.wordPlayMp3 = itemn.newWordMp3 || itemn.audio_file_url;
         }
         // });
       });

+ 74 - 1
src/views/book/courseware/preview/components/dialogue_article/index.vue

@@ -252,6 +252,7 @@ import Practice from './Practicechs.vue'; // 语音练习模式
 import WordModel from './WordModelChs.vue'; // 语音练习模式
 import Voicefullscreen from '../article/Voicefullscreen.vue';
 import { getToken } from '@/utils/auth';
+import { GetFileURLMap } from '@/api/app';
 import { analysSubmit, getSysConfig } from '@/api/article';
 import NewWordPreview from '../new_word/NewWordPreview.vue';
 import NotesPreview from '../notes/NotesPreview.vue';
@@ -540,6 +541,77 @@ export default {
         //   this.data.mp3_list[0].url = url_map[this.data.mp3_list[0].file_id];
         // });
       }
+      // 处理课文里生词表的音频和时间
+      if (this.data.new_word_list && this.data.new_word_list.new_word_list) {
+        if (this.data.new_word_list.audio_data && this.data.new_word_list.audio_data.temporary_url) {
+          // 如果大音频存在
+          this.data.new_word_list.new_word_list.forEach((item, index) => {
+            this.$set(item, 'audio_file_url', this.data.new_word_list.audio_data.temporary_url);
+            this.$set(
+              item,
+              'audio_begin_time',
+              this.data.new_word_list.lrc_arr[index] ? this.data.new_word_list.lrc_arr[index].bg : null,
+            );
+            this.$set(
+              item,
+              'audio_end_time',
+              this.data.new_word_list.lrc_arr[index] ? this.data.new_word_list.lrc_arr[index].ed : null,
+            );
+          });
+        } else {
+          let mp3_list = [];
+          this.data.new_word_list.new_word_list.forEach((item, index) => {
+            mp3_list.push(item.mp3_list);
+          });
+          GetFileURLMap({ file_id_list: mp3_list }).then(({ url_map }) => {
+            this.data.new_word_list.new_word_list.forEach((item, index) => {
+              this.$set(item, 'audio_file_url', url_map[item.mp3_list]);
+              this.$set(item, 'audio_begin_time', null);
+              this.$set(item, 'audio_end_time', null);
+            });
+          });
+        }
+      }
+      // 处理课文里其他词汇表的音频和时间
+      if (this.data.other_word_list && this.data.other_word_list.new_word_list) {
+        if (this.data.other_word_list.audio_data && this.data.other_word_list.audio_data.temporary_url) {
+          // 如果大音频存在
+          this.data.other_word_list.new_word_list.forEach((item, index) => {
+            this.$set(item, 'audio_file_url', this.data.other_word_list.audio_data.temporary_url);
+            this.$set(
+              item,
+              'audio_begin_time',
+              this.data.other_word_list.lrc_arr[index] ? this.data.other_word_list.lrc_arr[index].bg : -1,
+            );
+            this.$set(
+              item,
+              'audio_end_time',
+              this.data.other_word_list.lrc_arr[index] ? this.data.other_word_list.lrc_arr[index].ed : -1,
+            );
+          });
+        } else {
+          let mp3_list = [];
+          this.data.other_word_list.new_word_list.forEach((item, index) => {
+            mp3_list.push(item.mp3_list);
+          });
+          GetFileURLMap({ file_id_list: mp3_list }).then(({ url_map }) => {
+            this.data.other_word_list.new_word_list.forEach((item, index) => {
+              this.$set(item, 'audio_file_url', url_map[item.mp3_list]);
+              this.$set(item, 'audio_begin_time', null);
+              this.$set(item, 'audio_end_time', null);
+            });
+          });
+        }
+      }
+      if (this.data.new_word_list_other_component_input) {
+        this.data.new_word_list_other_component_input.forEach((item) => {
+          if (item.audio_begin_time === -1) {
+            item.audio_begin_time = null;
+            item.audio_end_time = null;
+          }
+        });
+      }
+
       this.NNPENewWordList = (
         this.data.new_word_list_other_component_input ? this.data.new_word_list_other_component_input : []
       )
@@ -826,7 +898,8 @@ export default {
   .main {
     display: grid;
     row-gap: 24px;
-    align-items: center;
+
+    // align-items: center;
   }
 
   .NPC-ArticleView {

+ 2 - 0
src/views/book/courseware/preview/components/fill/FillPreview.vue

@@ -111,6 +111,7 @@
       </div>
       <PreviewOperation
         :is-show-retry="isHasFill"
+        :is-show-answer="isHasFill"
         @showAnswerAnalysis="showAnswerAnalysis"
         @judgeCorrect="judgeCorrect"
         @retry="retry"
@@ -498,6 +499,7 @@ export default {
 
   .fill-wrapper {
     grid-area: fill;
+    align-self: start;
     font-size: 16pt;
 
     :deep .pinyin-area .rich-text-container,

+ 4 - 2
src/views/book/courseware/preview/components/judge/JudgePreview.vue

@@ -26,12 +26,13 @@
               :paragraph-list="paragraph_list"
               :rich-text-list="rich_text_list"
               :pinyin-position="data.property.pinyin_position"
+              :body-styles="getBodyStyles()"
               :is-preview="true"
             />
             <div
               v-else
               class="rich-text"
-              :style="{ fontSize: type === typeList[0] ? '12pt' : '' }"
+              :style="{ fontSize: type === typeList[0] ? '12pt' : '', ...getBodyStyles() }"
               v-html="convertText(sanitizeHTML(content))"
             ></div>
           </div>
@@ -98,12 +99,13 @@
               :paragraph-list="paragraph_list"
               :rich-text-list="rich_text_list"
               :pinyin-position="data.property.pinyin_position"
+              :body-styles="getBodyStyles()"
               :is-preview="true"
             />
             <div
               v-else
               class="rich-text"
-              :style="{ fontSize: type === typeList[0] ? '12pt' : '' }"
+              :style="{ fontSize: type === typeList[0] ? '12pt' : '', ...getBodyStyles() }"
               v-html="convertText(sanitizeHTML(content))"
             ></div>
           </div>

+ 7 - 1
src/views/book/courseware/preview/components/matching/MatchingPreview.vue

@@ -27,9 +27,15 @@
                 :paragraph-list="paragraph_list"
                 :rich-text-list="rich_text_list"
                 :pinyin-position="data.property.pinyin_position"
+                :body-styles="getBodyStyles()"
                 :is-preview="true"
               />
-              <span v-else class="content rich-text" v-html="convertText(sanitizeHTML(content))"></span>
+              <span
+                v-else
+                class="content rich-text"
+                :style="getBodyStyles()"
+                v-html="convertText(sanitizeHTML(content))"
+              ></span>
               <div v-if="showLang" class="lang">
                 {{ multilingual.find((item) => item.type === getLang())?.translation }}
               </div>

+ 3 - 2
src/views/book/courseware/preview/components/math/MathPreview.vue

@@ -3,12 +3,13 @@
   <div class="math-preview" :style="getAreaStyle()">
     <SerialNumberPosition v-if="isEnable(data.property.sn_display_mode)" :property="data.property" />
 
-    <div ref="mainMath" class="main" v-html="data.math"></div>
+    <div ref="mainMath" class="main">{{ data.math }}</div>
   </div>
 </template>
 
 <script>
 import PreviewMixin from '../common/PreviewMixin';
+import { typesetMathJax } from '@/utils/mathjax';
 
 import { getMathData } from '@/views/book/courseware/data/math';
 
@@ -36,7 +37,7 @@ export default {
     async renderMath() {
       await this.$nextTick();
       try {
-        await window.MathJax.typesetPromise([this.$refs.mainMath]);
+        await typesetMathJax(this.$refs.mainMath);
       } catch (err) {
         console.error('公式渲染失败:', err);
       }

+ 8 - 5
src/views/book/courseware/preview/components/new_word/NewWordPreview.vue

@@ -1466,8 +1466,8 @@
             :option-res="optionRes"
             :theme-color="'red'"
             :current-tree-i-d="courseware_id"
-            :bg="dataWord && dataWord.bg ? dataWord.bg : null"
-            :ed="dataWord && dataWord.ed ? dataWord.ed : null"
+            :bg="dataWord ? dataWord.audio_begin_time || dataWord.bg : null"
+            :ed="dataWord ? dataWord.audio_end_time || dataWord.ed : null"
             type="newWordDetail"
             :attrib="data.unified_attrib"
             :is-mobile="isMobile"
@@ -1690,11 +1690,13 @@ export default {
   },
   mounted() {
     this.$nextTick(() => {
-      let totalWidth = document.querySelector('.newWord-preview').offsetWidth;
-      this.width = `${totalWidth - (this.data.property.sn_display_mode === 'true' ? 15 : 0)}`;
+      setTimeout(() => {
+        let totalWidth = document.querySelector('.newWord-preview').offsetWidth;
+        this.width = `${totalWidth - (this.data.property.sn_display_mode === 'true' ? 15 : 0)}`;
+      }, 200);
     });
 
-    this.dataHeight = this.$refs.previewContainer.style.height;
+    this.dataHeight = this.$refs.previewContainer ? this.$refs.previewContainer.style.height : 0;
   },
   methods: {
     handleShow(obj) {
@@ -2347,6 +2349,7 @@ export default {
     display: flex;
     flex-flow: wrap;
     gap: 20px;
+    justify-content: center;
   }
 
   .writeTop {

+ 12 - 6
src/views/book/courseware/preview/components/new_word/components/WordPhraseDetail.vue

@@ -77,13 +77,19 @@
             <div class="yinpin">
               <span class="pinyintext" v-html="data.pinyin.toLowerCase()"></span>
               <template v-if="data.newWordMp3">
-                <AudioRed :mp3="data.newWordMp3" :theme-color="themeColor" :bg="bg" :ed="ed" />
+                <AudioRed :mp3="data.newWordMp3" :attrib="attrib" :theme-color="themeColor" :bg="bg" :ed="ed" />
               </template>
               <template v-else-if="data.mp3_list && data.mp3_list[0] && data.mp3_list[0].id">
-                <AudioRed :mp3="data.mp3_list[0].id" :theme-color="themeColor" />
+                <AudioRed :mp3="data.mp3_list[0].id" :attrib="attrib" :theme-color="themeColor" />
               </template>
               <template v-else-if="data.mp3Url">
-                <AudioRed :mp3="data.mp3Url" :theme-color="themeColor" :bg="bg" :ed="ed" />
+                <AudioRed :mp3="data.mp3Url" :attrib="attrib" :theme-color="themeColor" :bg="bg" :ed="ed" />
+              </template>
+              <template v-else-if="data.mp3_list_url">
+                <AudioRed :mp3="data.mp3_list_url" :attrib="attrib" :theme-color="themeColor" :bg="bg" :ed="ed" />
+              </template>
+              <template v-else-if="data.audio_file_url">
+                <AudioRed :mp3="data.audio_file_url" :attrib="attrib" :theme-color="themeColor" :bg="bg" :ed="ed" />
               </template>
             </div>
             <p class="jieshu" v-html="convertText(sanitizeHTML(data.definition_list))"></p>
@@ -220,7 +226,6 @@
         <div class="from">来自百度汉语</div>
         <div class="wordDetail">
           <p class="word">
-            {{ data.new_word }}
             <span v-for="(fy, i) in data.endata.mean_list" :key="i"> [{{ fy.pinyin }}] </span>
             <img v-if="data.endata.src_tts" @click="playAudio('audiosrc')" src="@/assets/play-red.png" alt="" />
             <audio ref="audiosrc" :src="data.endata.src_tts"></audio>
@@ -257,7 +262,7 @@
 
 <script>
 import Strockplayredline from './Strockplayredline.vue';
-import AudioRed from '../../voice_matrix/components/AudioRed.vue';
+import AudioRed from '../../article/components/AudioRed.vue';
 import { getCoursewareWordExampleSentenceList, toolDictionary } from '@/api/article';
 import { sanitizeHTML } from '@/utils/common';
 
@@ -974,10 +979,11 @@ export default {
             line-height: 30px;
             color: #2c2c2c;
             word-break: normal;
+            white-space: nowrap;
           }
 
           .content-voices {
-            width: 16px;
+            // width: 16px;
           }
         }
       }

+ 4 - 1
src/views/book/courseware/preview/components/rich_text/RichTextPreview.vue

@@ -60,7 +60,6 @@ import PreviewMixin from '../common/PreviewMixin';
 import { isEnable } from '@/views/book/courseware/data/common';
 import PinyinText from '@/components/PinyinText.vue';
 import AudioPlay from '../character_base/components/AudioPlay.vue';
-import { getRandomNumber } from '@/utils';
 
 export default {
   name: 'RichTextPreview',
@@ -175,6 +174,10 @@ export default {
 .describe-preview {
   @include preview-base;
 
+  :deep(.mathjax-container.editor-math > svg[aria-hidden='true']) {
+    display: none !important;
+  }
+
   &.middle {
     margin-top: calc($title-content-spacing - $component-spacing);
     margin-bottom: calc($title-content-spacing - $component-spacing);

+ 4 - 2
src/views/book/courseware/preview/components/select/SelectPreview.vue

@@ -30,12 +30,13 @@
             :paragraph-list="paragraph_list"
             :rich-text-list="rich_text_list"
             :pinyin-position="data.property.pinyin_position"
+            :body-styles="getBodyStyles()"
             :is-preview="true"
           />
           <span
             v-else
             class="content rich-text"
-            :style="{ fontSize: type === typeList[0] ? '12pt' : '' }"
+            :style="{ fontSize: type === typeList[0] ? '12pt' : '', ...getBodyStyles() }"
             v-html="convertText(sanitizeHTML(content))"
           ></span>
           <div v-if="showLang" class="lang">
@@ -86,12 +87,13 @@
             :paragraph-list="paragraph_list"
             :rich-text-list="rich_text_list"
             :pinyin-position="data.property.pinyin_position"
+            :body-styles="getBodyStyles()"
             :is-preview="true"
           />
           <span
             v-else
             class="content rich-text"
-            :style="{ fontSize: type === typeList[0] ? '12pt' : '' }"
+            :style="{ fontSize: type === typeList[0] ? '12pt' : '', ...getBodyStyles() }"
             v-html="convertText(sanitizeHTML(content))"
           ></span>
           <div v-if="showLang" class="lang">

+ 7 - 1
src/views/book/courseware/preview/components/sort/SortPreview.vue

@@ -30,9 +30,15 @@
                 :paragraph-list="item.paragraph_list"
                 :rich-text-list="item.rich_text_list"
                 :pinyin-position="data.property.pinyin_position"
+                :body-styles="getBodyStyles()"
                 :is-preview="true"
               />
-              <span v-else class="rich-text" v-html="convertText(sanitizeHTML(item.content))"></span>
+              <span
+                v-else
+                class="rich-text"
+                :style="getBodyStyles()"
+                v-html="convertText(sanitizeHTML(item.content))"
+              ></span>
               <div v-if="showLang" class="lang">
                 {{ item.multilingual.find((item) => item.type === getLang())?.translation }}
               </div>

+ 3 - 0
src/views/book/courseware/preview/components/table/TablePreview.vue

@@ -1234,6 +1234,9 @@ export default {
             styleObject[key.trim()] = value.trim();
           }
         });
+        if (styleObject.hasOwnProperty('text-align') && styleObject['text-align'] === 'center') {
+          this.$set(styleObject, 'justify-content', 'center');
+        }
         return styleObject;
       }
       return {};

+ 10 - 3
src/views/book/courseware/preview/components/video_interaction/VideoInteractionPreview.vue

@@ -136,10 +136,17 @@ export default {
       if (!val) return;
       this.userAnswer = this.answer.answer_list[0];
     },
+    'data.video_list.length': {
+      handler(val) {
+        if (val) {
+          this.initData();
+        }
+      },
+      deep: true,
+      immediate: true,
+    },
   },
-  created() {
-    this.initData();
-  },
+  created() {},
   mounted() {},
   beforeDestroy() {
     if (this.player) {

+ 4 - 3
src/views/book/courseware/preview/components/voice_matrix/components/AudioLine.vue

@@ -6,11 +6,11 @@
         <div class="play" :class="[audio.loading ? 'loadBtn' : audio.playing ? 'playBtn' : 'pauseBtn']">
           <SvgIcon
             v-if="audio.playing"
-            size="24px"
+            size="24"
             icon-class="pause-large-fill"
             :style="{ color: attrib?.topic_color }"
           />
-          <SvgIcon v-else size="24px" icon-class="play-large-fill" :style="{ color: attrib?.topic_color }" />
+          <SvgIcon v-else size="24" icon-class="play-large-fill" :style="{ color: attrib?.topic_color }" />
         </div>
       </div>
 
@@ -385,7 +385,8 @@ export default {
     .play {
       display: block;
       width: 16px;
-      font-size: 0;
+
+      // font-size: 0;
       cursor: pointer;
     }
 

+ 2 - 3
src/web_preview/index.vue

@@ -6,7 +6,7 @@
       </div>
       <div class="courseware-top">
         <span class="name-path">{{ courseware_info.name_path }}</span>
-        <span class="flow-nodename">{{ courseware_info.cur_audit_flow_node_name }}</span>
+        <span class="flow-nodename"></span>
         <slot name="middle" :courseware="courseware_info"></slot>
         <div class="group"></div>
         <div v-if="isShowGroup">
@@ -100,7 +100,7 @@
             :courseware-id="curSelectId"
             :component-list="component_list"
             :background="background"
-            :can-remark="isTrue(courseware_info.is_my_audit_task) && isTrue(courseware_info.is_can_add_audit_remark)"
+            :can-remark="false"
             :show-remark="false"
             :project="project"
             :component-remark-obj="remark_list_obj"
@@ -790,7 +790,6 @@ export default {
       GetLanguageTypeList({
         book_id: this.courseware_info.book_id,
         is_contain_zh: 'true',
-        is_only_can_translation: 'true',
       }).then(({ language_type_list }) => {
         this.langList = language_type_list;
       });

+ 1 - 1
vue.config.js

@@ -33,7 +33,7 @@ if (NODE_ENV === 'development') {
 
 module.exports = defineConfig({
   publicPath: NODE_ENV === 'development' ? '/' : './',
-  outputDir: 'dist',
+  outputDir: '智慧梧桐数字教材预览',
   assetsDir: 'static',
   lintOnSave: false,
   runtimeCompiler: true,