dsy преди 1 месец
родител
ревизия
32c76fa4a5

+ 1 - 1
.env

@@ -11,4 +11,4 @@ VUE_APP_BookWebSI = '/GCLSBookWebSI/ServiceInterface'
 VUE_APP_EepServer = '/EEPServer/SI'
 
 #version
-VUE_APP_VERSION = '2026.07.23'
+VUE_APP_VERSION = '2026.07.28'

+ 13 - 3
packages/BookEep.vue

@@ -75,6 +75,15 @@ import {
   GetCoursewareFeedbackList,
 } from '@/api/book';
 
+const defaultBc = {
+  background_image_url: '',
+  background_position: {
+    left: 0,
+    top: 0,
+  },
+  background: {},
+};
+
 export default {
   name: 'BookEep',
   components: {
@@ -130,8 +139,9 @@ export default {
   },
   data() {
     const isMobile = /android|iphone|ipad|ipod/i.test(navigator.userAgent || navigator.vendor || window.opera);
+
     return {
-      background: this.bookBackground.length > 0 ? JSON.parse(this.bookBackground) : {},
+      background: this.bookBackground.length > 0 ? JSON.parse(this.bookBackground) : defaultBc,
       groupRowList: this.contentGroupRowList.length > 0 ? JSON.parse(this.contentGroupRowList) : [],
       data: this.content.length > 0 ? JSON.parse(this.content) : { row_list: [] },
       cList: this.handleComponentList(),
@@ -194,6 +204,8 @@ export default {
       handler(newVal) {
         if (newVal && newVal.length > 0) {
           this.background = JSON.parse(newVal);
+        } else {
+          this.background = defaultBc;
         }
       },
     },
@@ -766,14 +778,12 @@ $total-width: $courseware-width + $courseware-left-margin + $courseware-right-ma
       width: $courseware-left-margin;
       min-width: $courseware-left-margin;
       max-width: $courseware-left-margin;
-      background-color: $courseware-bgColor;
     }
 
     .preview-right {
       width: $courseware-right-margin;
       min-width: $courseware-right-margin;
       max-width: $courseware-right-margin;
-      background-color: $courseware-bgColor;
     }
 
     &.no-audit {

+ 25 - 0
packages/index.js

@@ -18,6 +18,7 @@ import VueSignaturePad from 'vue-signature-pad';
 import BookEep from './BookEep.vue';
 import pkg from './package.json';
 import { setRuntimeServices } from '@/utils/runtime-services';
+import EepSvgIcon from '@/common/SvgIcon';
 
 const components = [BookEep];
 const version = pkg.version;
@@ -32,6 +33,30 @@ const install = (VueInstance, options = {}) => {
   if (install.installed) return;
   install.installed = true;
 
+  // 在 BookEep 组件子树内强制 SvgIcon 解析到库内实现,避免宿主全局同名组件覆盖
+  // 必须无条件覆盖:Vue 2 的 $options.components 通过原型链继承全局组件,
+  // 用 if 守卫会导致永远跳过后注册的覆盖逻辑。
+  if (!VueInstance.__EEP_UI_ICON_MIXIN_INSTALLED__) {
+    VueInstance.__EEP_UI_ICON_MIXIN_INSTALLED__ = true;
+    VueInstance.mixin({
+      beforeCreate() {
+        if (this.$options.name === 'BookEep') {
+          this.__eepUiScope = true;
+        } else if (this.$parent && this.$parent.__eepUiScope) {
+          this.__eepUiScope = true;
+        }
+
+        if (!this.__eepUiScope) return;
+
+        const components = this.$options.components || (this.$options.components = {});
+        // 无条件覆盖,因为 Vue 2 的 $options.components 继承自全局 Vue.options.components,
+        // 宿主已注册的 SvgIcon 会出现在原型链上,导致 if (!components.SvgIcon) 永远为 false
+        components.EepSvgIcon = EepSvgIcon;
+        components.SvgIcon = EepSvgIcon;
+      },
+    });
+  }
+
   setRuntimeServices({
     router: options.router,
     store: options.store,

+ 1 - 1
packages/package.json

@@ -1,6 +1,6 @@
 {
   "name": "eep-ui",
-  "version": "0.0.26",
+  "version": "0.0.32",
   "main": "eep/eep-ui.umd.js",
   "style": "eep/eep-ui.css",
   "exports": {

+ 3 - 1
src/common/SvgIcon/index.vue

@@ -15,7 +15,9 @@
 import { isExternal, isNumber } from '@/utils/validate';
 
 export default {
-  name: 'SvgIcon',
+  name: 'EepSvgIcon',
+  // 禁用 attribute 自动继承,防止 size 等 prop 泄漏为 DOM 属性
+  inheritAttrs: false,
   props: {
     size: {
       type: [String, Number],

+ 1 - 0
src/icons/index.js

@@ -1,6 +1,7 @@
 import Vue from 'vue';
 import SvgIcon from '@/common/SvgIcon'; // svg component
 
+Vue.component('EepSvgIcon', SvgIcon);
 Vue.component('SvgIcon', SvgIcon);
 
 const req = require.context('./svg', true, /\.svg$/);

+ 2 - 1
src/views/book/courseware/create/components/base/3d_model/3DModel.vue

@@ -14,6 +14,7 @@
         :icon-class="iconClass"
         :limit="limit"
         :single-size="200"
+        :upload-tip="'支持fbx、gltf、glb格式文件,单个文件不超过200Mb。'"
         @updateFileList="updateFileList"
       />
     </template>
@@ -35,7 +36,7 @@ export default {
     return {
       data: get3DModelData(),
       labelText: '3D模型',
-      acceptFileType: '.fbx,.gltf,.glb,.zip',
+      acceptFileType: '.fbx,.gltf,.glb',
       iconClass: '3d',
       limit: 1,
     };

+ 9 - 9
src/views/book/courseware/create/components/base/common/UploadFile.vue

@@ -373,11 +373,11 @@ export default {
 
       const suffix = fileName.slice(fileName.lastIndexOf('.') + 1, fileName.length).toLowerCase();
       let fileType = [];
-      let typeTip = '';
+      let typeTip = '当前文件格式暂不支持上传,请更换。';
 
       if (this.type === 'audio') {
         fileType = ['mp3', 'acc', 'wma', 'wav'];
-        typeTip = '音频文件只能是 mp3、acc、wma、wav格式!';
+        typeTip += '音频文件只能是 mp3、acc、wma、wav格式!';
       } else if (
         this.type === 'picture' ||
         this.type === 'image_text' ||
@@ -387,10 +387,10 @@ export default {
         this.type === 'character'
       ) {
         fileType = ['jpg', 'png', 'jpeg'];
-        typeTip = '图片文件只能是 jpg、png、jpeg 格式!';
+        typeTip += '图片文件只能是 jpg、png、jpeg 格式!';
       } else if (this.type === 'video' || this.type === 'video_interaction') {
         fileType = ['mp4'];
-        typeTip = '视频文件只能是 mp4 格式!';
+        typeTip += '视频文件只能是 mp4 格式!';
       } else if (this.type === 'upload_preview' || this.type === 'video_interaction_file') {
         fileType = [
           'png',
@@ -411,17 +411,17 @@ export default {
           'zip',
           'rar',
         ];
-        typeTip = '文件不支持';
+        typeTip += '文件不支持';
       } else if (this.type === 'h5_games') {
         fileType = ['zip', 'html'];
-        typeTip = 'H5游戏文件只能是 zip、html 格式!';
+        typeTip += 'H5游戏文件只能是 zip、html 格式!';
       } else if (this.type === '3DModel') {
-        fileType = ['fbx', 'zip', 'gltf', 'glb'];
-        typeTip = '3D模型文件只能是 fbx、gltf、glb、zip 格式!';
+        fileType = ['fbx', 'gltf', 'glb'];
+        typeTip += '3D模型文件只能是 fbx、gltf、glb 格式!';
       }
       const isNeedType = fileType.includes(suffix);
       if (!isNeedType) {
-        typeTip += `,[${fileName}]被移除!`;
+        typeTip += ` [${fileName}]被移除!`;
         this.$message.error(typeTip);
         this.$refs.upload.handleRemove(file);
         return false;

+ 2 - 1
src/views/book/courseware/create/components/question/fill/Fill.vue

@@ -795,7 +795,8 @@ export default {
     }
 
     :deep .pinyin-area .rich-text-container,
-    :deep .pinyin-area .pinyin-paragraph {
+    :deep .pinyin-area .pinyin-paragraph,
+    :deep .pinyin-area .pinyin-sentence  {
       display: inline;
     }
   }

+ 88 - 80
src/views/book/courseware/create/components/question/matching/Matching.vue

@@ -1,87 +1,95 @@
 <template>
   <ModuleBase ref="base" :type="data.type">
     <template #content>
-      <ul class="option-list">
-        <li v-for="(li, i) in data.option_list" :key="i" class="option-item">
-          <div v-for="(item, j) in li" :key="item.mark" class="option">
-            <span class="serial-number">{{ computeOptionMethods[data.property.serial_number_type_list[j]](i) }}</span>
-            <span class="option-content">
-              <RichText
-                v-if="property.isGetContent"
-                ref="richText"
-                v-model="item.content"
-                placeholder="请输入"
-                :inline="true"
-                :height="32"
-                :font-size="data?.unified_attrib?.font_size"
-                :font-family="data?.unified_attrib?.font"
-                :font-color="data?.unified_attrib?.text_color"
-                :is-view-pinyin="isEnable(data.property.view_pinyin)"
-                @createParsedTextInfoPinyin="handleParsedTextPinyin($event, i, j)"
-              />
-            </span>
-            <span class="multilingual" @click="openMultilingual(i, j)">
-              <SvgIcon icon-class="multilingual" class-name="multilingual" width="12" height="12" />
-            </span>
-          </div>
-        </li>
-      </ul>
-      <div class="answer-tips">答案:</div>
-      <ul class="answer-list">
-        <li v-for="(li, i) in data.answer.answer_list" :key="i" class="answer-item">
-          <template v-for="(item, j) in li">
-            <span v-if="j === 0" :key="`zero-${j}-${item.mark}`">{{ answerList[j][i].label }}</span>
-            <el-select v-if="j === 1 || j === 2" :key="`select-${j}`" v-model="item.mark" multiple placeholder="请选择">
-              <el-option
-                v-for="answer in answerList[j]"
-                :key="answer.value"
-                :label="answer.label"
-                :value="answer.value"
-              />
-            </el-select>
-            <span v-if="j === 1 && data.property.column_num === 3" :key="`one-${j}-${item.mark}`">
-              {{ answerList[j][i].label }}
-            </span>
-          </template>
-        </li>
-      </ul>
-
-      <el-divider v-if="isEnable(data.property.view_pinyin)" content-position="left">
-        <span>拼音效果</span>
-        <el-button
-          v-show="isEnable(data.property.view_pinyin)"
-          type="text"
-          icon="el-icon-refresh"
-          title="刷新"
-          class="refresh-pinyin-btn"
-          @click.native="handleViewPinyin(true)"
-        />
-      </el-divider>
-      <div v-show="isEnable(data.property.view_pinyin)" class="proofread-table-wrapper">
-        <table class="proofread-table">
-          <tbody>
-            <template v-for="(row, i) in data.option_list">
-              <tr v-for="(item, j) in row" :key="item.mark">
-                <td class="pinyin-cell">
-                  <PinyinText
-                    ref="PinyinText"
-                    :paragraph-list="item.paragraph_list"
-                    :rich-text-list="item.rich_text_list"
-                    :pinyin-position="data.property.pinyin_position"
-                    @fillCorrectPinyin="fillCorrectPinyin($event, i, j)"
-                  />
-                </td>
-                <td class="proofread-cell">
-                  <WordProofread
-                    button-text="分词校对"
-                    :word-data="getOptionWordData(item)"
-                    @save="saveOptionWord($event, i, j)"
-                  />
-                </td>
-              </tr>
+      <div v-loading="loading">
+        <ul class="option-list">
+          <li v-for="(li, i) in data.option_list" :key="i" class="option-item">
+            <div v-for="(item, j) in li" :key="item.mark" class="option">
+              <span class="serial-number">{{ computeOptionMethods[data.property.serial_number_type_list[j]](i) }}</span>
+              <span class="option-content">
+                <RichText
+                  v-if="property.isGetContent"
+                  ref="richText"
+                  v-model="item.content"
+                  placeholder="请输入"
+                  :inline="true"
+                  :height="32"
+                  :font-size="data?.unified_attrib?.font_size"
+                  :font-family="data?.unified_attrib?.font"
+                  :font-color="data?.unified_attrib?.text_color"
+                  :is-view-pinyin="isEnable(data.property.view_pinyin)"
+                  @createParsedTextInfoPinyin="handleParsedTextPinyin($event, i, j)"
+                />
+              </span>
+              <span class="multilingual" @click="openMultilingual(i, j)">
+                <SvgIcon icon-class="multilingual" class-name="multilingual" width="12" height="12" />
+              </span>
+            </div>
+          </li>
+        </ul>
+        <div class="answer-tips">答案:</div>
+        <ul class="answer-list">
+          <li v-for="(li, i) in data.answer.answer_list" :key="i" class="answer-item">
+            <template v-for="(item, j) in li">
+              <span v-if="j === 0" :key="`zero-${j}-${item.mark}`">{{ answerList[j][i].label }}</span>
+              <el-select
+                v-if="j === 1 || j === 2"
+                :key="`select-${j}`"
+                v-model="item.mark"
+                multiple
+                placeholder="请选择"
+              >
+                <el-option
+                  v-for="answer in answerList[j]"
+                  :key="answer.value"
+                  :label="answer.label"
+                  :value="answer.value"
+                />
+              </el-select>
+              <span v-if="j === 1 && data.property.column_num === 3" :key="`one-${j}-${item.mark}`">
+                {{ answerList[j][i].label }}
+              </span>
             </template>
-          </tbody>
-        </table>
+          </li>
+        </ul>
+
+        <el-divider v-if="isEnable(data.property.view_pinyin)" content-position="left">
+          <span>拼音效果</span>
+          <el-button
+            v-show="isEnable(data.property.view_pinyin)"
+            type="text"
+            icon="el-icon-refresh"
+            title="刷新"
+            class="refresh-pinyin-btn"
+            @click.native="handleViewPinyin(true)"
+          />
+        </el-divider>
+        <div v-show="isEnable(data.property.view_pinyin)" class="proofread-table-wrapper">
+          <table class="proofread-table">
+            <tbody>
+              <template v-for="(row, i) in data.option_list">
+                <tr v-for="(item, j) in row" :key="item.mark">
+                  <td class="pinyin-cell">
+                    <PinyinText
+                      ref="PinyinText"
+                      :paragraph-list="item.paragraph_list"
+                      :rich-text-list="item.rich_text_list"
+                      :pinyin-position="data.property.pinyin_position"
+                      @fillCorrectPinyin="fillCorrectPinyin($event, i, j)"
+                    />
+                  </td>
+                  <td class="proofread-cell">
+                    <WordProofread
+                      button-text="分词校对"
+                      :word-data="getOptionWordData(item)"
+                      @save="saveOptionWord($event, i, j)"
+                    />
+                  </td>
+                </tr>
+              </template>
+            </tbody>
+          </table>
+        </div>
       </div>
 
       <MultilingualFill

+ 16 - 7
src/views/book/courseware/create/components/question/voice_matrix/VoiceMatrix.vue

@@ -73,7 +73,7 @@
         <div class="lrc-box" style="margin: 5px 0">
           <div v-if="data.lrc_arr && data.lrc_arr.length > 0" class="lrc-box">
             <span>已有字幕时间节点</span>
-            <el-button type="text" @click="rebuildWordTime">重新生成</el-button>
+            <el-button type="text" @click="clearWordTime">重新生成</el-button>
           </div>
           <template v-else>
             <el-button v-if="!isWordTime" size="medium" @click="autoWordTime"> 自动生成字幕节点 </el-button>
@@ -199,11 +199,17 @@ export default {
       GetStaticResources('tool-ParseLRCFile', {
         content_type: 'FILE',
         file_id: this.data.lrc_data.file_id,
-      }).then(({ lrc_list }) => {
-        this.data.lrc_arr = lrc_list;
-        this.distribution();
-        loading.close();
-      });
+      })
+        .then(({ lrc_list }) => {
+          this.data.lrc_arr = lrc_list;
+          this.distribution();
+        })
+        .catch(() => {
+          this.$message.error('LRC 文件解析失败,请检查文件格式');
+        })
+        .finally(() => {
+          loading.close();
+        });
     },
     /**
      * 分配标记
@@ -317,7 +323,7 @@ export default {
     /**
      * 重新生成字幕节点
      */
-    rebuildWordTime() {
+    clearWordTime() {
       this.isWordTime = false;
       this.$set(this.data, 'lrc_arr', []);
     },
@@ -353,6 +359,9 @@ export default {
           this.$set(this.data, 'lrc_arr', lrcArr);
           this.distribution();
         })
+        .catch(() => {
+          this.$message.error('字幕节点生成失败,请重试');
+        })
         .finally(() => {
           this.isWordTime = false;
         });

+ 10 - 0
src/views/book/courseware/preview/components/3d_model/3DModelPreview.vue

@@ -448,6 +448,16 @@ export default {
         this.$message.warning('请等待模型加载完成后,再切换全屏');
         return;
       }
+
+      // 在切换全屏前,记录当前容器的宽高,以便在退出全屏时恢复
+      const container = this.$refs.three;
+      const width = container.clientWidth;
+      const height = container.clientHeight;
+      if (document.fullscreenElement === null) {
+        this.$refs.three.style.width = `${width}px`;
+        this.$refs.three.style.height = `${height}px`;
+      }
+
       toggleFullScreen(this.$refs.three);
       this.$nextTick(() => {
         setTimeout(() => {

+ 7 - 6
src/views/book/courseware/preview/components/fill/FillPreview.vue

@@ -10,7 +10,7 @@
         :file-id="data.audio_file_id"
       />
       <div class="fill-wrapper">
-        <p>
+        <div class="fill-content">
           <template v-for="(li, i) in modelEssay">
             <template v-if="li.type === 'text'">
               <PinyinText
@@ -94,7 +94,7 @@
               </template>
             </template>
           </template>
-        </p>
+        </div>
       </div>
       <SoundRecord
         v-if="isEnable(data.property.is_enable_voice_answer)"
@@ -132,7 +132,7 @@
       @closeAnswerAnalysis="closeAnswerAnalysis"
     >
       <div slot="right-answer" class="fill-wrapper">
-        <p>
+        <div class="fill-content">
           <template v-for="(li, i) in modelEssay">
             <template v-if="li.type === 'text'">
               <PinyinText
@@ -152,7 +152,7 @@
               </span>
             </template>
           </template>
-        </p>
+        </div>
       </div>
     </AnswerAnalysis>
   </div>
@@ -519,11 +519,12 @@ export default {
     font-size: 16pt;
 
     :deep .pinyin-area .rich-text-container,
-    :deep .pinyin-area .pinyin-paragraph {
+    :deep .pinyin-area .pinyin-paragraph,
+    :deep .pinyin-area .pinyin-sentence {
       display: inline;
     }
 
-    p {
+    .fill-content {
       margin: 0;
     }
 

+ 8 - 6
src/views/book/courseware/preview/components/voice_matrix/VoiceMatrixPreview.vue

@@ -317,11 +317,7 @@ export default {
     },
   },
   created() {
-    Bus.$on('audioPause', (id) => {
-      if (this.cid === id) return;
-      if (this.$refs.luyin?.microphoneStatus) this.$refs.luyin.microphone();
-      this.handleParentPlay();
-    });
+    Bus.$on('audioPause', this.onAudioPause);
     if (!('record_list' in this.answer)) {
       this.$set(this.answer, 'record_list', []);
     }
@@ -330,9 +326,15 @@ export default {
     document.querySelector('body').addEventListener('click', this.restoreAudioStatus);
   },
   beforeDestroy() {
+    Bus.$off('audioPause', this.onAudioPause);
     document.querySelector('body').removeEventListener('click', this.restoreAudioStatus);
   },
   methods: {
+    onAudioPause(id) {
+      if (this.cid === id) return;
+      if (this.$refs.luyin?.microphoneStatus) this.$refs.luyin.microphone();
+      this.handleParentPlay();
+    },
     handleWav(data) {
       this.data.record_list = data;
     },
@@ -552,7 +554,7 @@ export default {
         }
         return lrc_data;
       }
-      return false;
+      return null;
     },
 
     sentPause(isRecord) {