2 Komitmen 1d112eb3e8 ... 21b3d78e78

Pembuat SHA1 Pesan Tanggal
  dsy 21b3d78e78 Merge branch 'master' of http://60.205.254.193:3000/GCLS/eep_page 2 hari lalu
  dsy 65e534c74f 连线题多选连线 2 hari lalu

+ 1 - 1
.env

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

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "eep_page",
-  "version": "2025.12.31",
+  "version": "2026.01.07",
   "private": true,
   "main": "main.js",
   "description": "智慧梧桐数字教材编辑器",

+ 6 - 6
src/App.vue

@@ -2,12 +2,7 @@
   <div id="app">
     <RouterView />
     <GlobalProgress />
-    <CommonProgress
-      :visible.sync="visible"
-      title="下载更新包"
-      :percentage="percentage"
-      text="正在下载更新包,请稍候..."
-    />
+    <CommonProgress :visible.sync="visible" :title="title" :percentage="percentage" text="正在下载升级包,请等待..." />
   </div>
 </template>
 
@@ -32,6 +27,11 @@ export default {
       percentage: 0,
     };
   },
+  computed: {
+    title() {
+      return `下载升级包(当前版本 ${Number(process.env.VUE_APP_VERSION.replace(/\./g, ''))},升级包版本 ${this.version})`;
+    },
+  },
   mounted() {
     const isDev = process.env.NODE_ENV === 'development';
     if (isDev) return;

+ 3 - 3
src/components/RichText.vue

@@ -309,12 +309,12 @@ export default {
                 onSubmit: (api) => {
                   const color = api.getData().color;
                   // 加下划线
-                  //editor.execCommand('Underline');
-                  //注册自定义格式
+                  // editor.execCommand('Underline');
+                  // 注册自定义格式
                   editor.formatter.register('coloredunderline', {
                     inline: 'span',
                     styles: {
-                      'border-bottom': `0.1em solid ${color}`, //要固定线粗细的话,就使用2px
+                      'border-bottom': `0.1em solid ${color}`, // 要固定线粗细的话,就使用2px
                       'padding-bottom': '1px',
                     },
                     merge_siblings: false,

+ 11 - 0
src/utils/validate.js

@@ -55,3 +55,14 @@ export function isNumber(value) {
 export function isNodeType(node, type) {
   return node.nodeName.toLowerCase() === type.toLowerCase();
 }
+
+/**
+ * @description 数组是否有交集
+ * @param {Array} a
+ * @param {Array} b
+ * @returns {Boolean}
+ */
+export function arraysIntersect(a, b) {
+  const s = new Set(a);
+  return b.some((x) => s.has(x));
+}

+ 1 - 1
src/views/book/courseware/create/components/CreateCanvas.vue

@@ -181,7 +181,7 @@ export default {
         },
         // 组件列表
         row_list: [],
-        // 全篇设置
+        // 样式调整
         unified_attrib,
       },
       rowCheckList: {}, // 行复选框列表

+ 1 - 1
src/views/book/courseware/create/components/FullTextSettings.vue

@@ -5,7 +5,7 @@
     :close-on-click-modal="false"
     :visible="visible"
     :before-close="handleClose"
-    title="全篇设置"
+    title="样式调整"
   >
     <el-form ref="form" :model="unified_attrib" label-width="80px" size="small">
       <el-form-item label="主题色">

+ 10 - 5
src/views/book/courseware/create/components/question/matching/Matching.vue

@@ -28,7 +28,8 @@
       <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">
-            <el-select :key="`${j}-${item.mark}`" v-model="item.mark" placeholder="请选择">
+            <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"
@@ -36,6 +37,9 @@
                 :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>
@@ -119,7 +123,7 @@ export default {
             }
             // 修改答案列表
             this.data.answer.answer_list.forEach((li) => {
-              li.push({ mark: '' });
+              li.push({ mark: [] });
             });
             return;
           }
@@ -147,7 +151,7 @@ export default {
             }
             // 增加答案列表
             this.data.answer.answer_list.push(
-              Array.from({ length: this.data.property.column_num }, () => ({ mark: '' })),
+              Array.from({ length: this.data.property.column_num }, () => ({ mark: [] })),
             );
             // 将答案列表最后一项的第一个元素设置进答案列表第一项
             this.data.answer.answer_list[this.data.answer.answer_list.length - 1][0] = {
@@ -271,9 +275,10 @@ export default {
   .answer-item {
     display: flex;
     column-gap: 16px;
+    align-items: center;
 
-    span:first-child {
-      width: 70px;
+    span {
+      display: inline-block;
       height: 32px;
       padding: 4px 8px;
       color: #c9cdd4;

+ 1 - 1
src/views/book/courseware/data/fill.js

@@ -57,7 +57,7 @@ export function getFillProperty() {
     audio_position: audioPositionList[0].value,
     audio_generation_method: audioGenerationMethodList[0].value,
     fill_font: fillFontList[0].value,
-    is_enable_voice_answer: switchOption[0].value,
+    is_enable_voice_answer: switchOption[1].value,
     view_pinyin: 'false', // 显示拼音
     pinyin_position: pinyinPositionList[0].value,
     is_first_sentence_first_hz_pinyin_first_char_upper_case: displayList[0].value, // 句首大写

+ 63 - 6
src/views/book/courseware/preview/components/3d_model/3DModelPreview.vue

@@ -9,6 +9,7 @@
         :size="isFullScreen ? '32px' : '16px'"
         @click="handleToggleFullScreen"
       />
+      <span v-if="!loaded" class="progress">{{ progress }}</span>
     </div>
   </div>
 </template>
@@ -36,10 +37,12 @@ export default {
       camera: null, // 相机
       mixer: null, // 动画混合器
       renderer: null, // 渲染器
+      pmremGenerator: null, // PMREM 生成器,用于环境光照(PBR)
       controls: null, // 轨道控制器
       animationId: null, // 动画帧ID
       isFullScreen: false, // 是否全屏
       loaded: false, // 是否加载完成
+      progress: '0%', // 加载进度
     };
   },
   watch: {
@@ -81,20 +84,30 @@ export default {
       // 创建相机
       const width = container.clientWidth;
       const height = container.clientHeight;
-      this.camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000); // 视角75度
+      this.camera = new THREE.PerspectiveCamera(60, width / height, 0.1, 1000); // 视角75度
       this.camera.position.set(5, 5, 5); // 设置相机位置
 
       // 创建渲染器
       this.renderer = new THREE.WebGLRenderer({ antialias: true }); // 开启抗锯齿
+      // 在高 DPI 屏幕上限制像素比以平衡清晰度与性能
+      this.renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
       this.renderer.setSize(width, height);
       this.renderer.shadowMap.enabled = true; // 启用阴影映射
       this.renderer.shadowMap.type = THREE.PCFSoftShadowMap; // 使用柔和阴影
 
       // 启用颜色管理和正确的色彩空间
       this.renderer.outputColorSpace = THREE.SRGBColorSpace; // 设置输出色彩空间
+      // 启用物理光照计算,使光照强度与光源单位保持一致
+      this.renderer.physicallyCorrectLights = true;
       this.renderer.toneMapping = THREE.ACESFilmicToneMapping; // 使用ACES电影色调映射
       this.renderer.toneMappingExposure = 1.0; // 曝光度
 
+      // 创建 PMREMGenerator,用于从 equirectangular/HDR 贴图生成环境贴图(用于 PBR 材质)
+      if (THREE.PMREMGenerator) {
+        this.pmremGenerator = new THREE.PMREMGenerator(this.renderer); // 创建 PMREM 生成器
+        this.pmremGenerator.compileEquirectangularShader(); // 预编译着色器
+      }
+
       container.appendChild(this.renderer.domElement); // 将渲染器添加到DOM
 
       // 添加光源
@@ -116,6 +129,8 @@ export default {
       this.controls.enableDamping = true; // 启用阻尼效果
       this.controls.dampingFactor = 0.25; // 阻尼系数
 
+      // 绑定 animate 的 this,避免在 requestAnimationFrame 中丢失上下文
+      this.animate = this.animate.bind(this);
       // 开始渲染循环
       this.animate();
 
@@ -128,7 +143,7 @@ export default {
         return;
       }
 
-      const modelUrl = this.data.file_list[0].file_url;
+      const modelUrl = this.data.model_list[0].file_url;
       if (!modelUrl) {
         console.error('模型文件URL不存在');
         return;
@@ -190,7 +205,7 @@ export default {
           }
         },
         (progress) => {
-          console.log('加载进度:', `${(progress.loaded / progress.total) * 100}%`);
+          this.progress = `${((progress.loaded / progress.total) * 100).toFixed(2)}%`;
           if (progress.loaded >= progress.total) {
             this.loaded = true;
           }
@@ -226,7 +241,7 @@ export default {
           this.addModelToScene(fbx);
         },
         (progress) => {
-          console.log('加载进度:', `${(progress.loaded / progress.total) * 100}%`);
+          this.progress = `${((progress.loaded / progress.total) * 100).toFixed(2)}%`;
           if (progress.loaded >= progress.total) {
             this.loaded = true;
           }
@@ -245,9 +260,10 @@ export default {
         this.loadOBJModel(loader, url);
         return;
       }
+
       const mtlLoader = new MTLLoader();
 
-      // 设置材质加载路径
+      // 设置材质和纹理加载路径
       const basePath = mtlUrl.substring(0, mtlUrl.lastIndexOf('/') + 1);
       mtlLoader.setPath(basePath);
       mtlLoader.setResourcePath(basePath);
@@ -264,7 +280,6 @@ export default {
         },
         undefined,
         (error) => {
-          // 材质文件加载失败,使用默认材质
           console.warn('MTL材质文件加载失败,使用默认材质:', error);
           this.loadOBJModel(loader, url);
         },
@@ -283,6 +298,7 @@ export default {
           this.addModelToScene(obj);
         },
         (progress) => {
+          this.progress = `${((progress.loaded / progress.total) * 100).toFixed(2)}%`;
           if (progress.loaded >= progress.total) {
             this.loaded = true;
           }
@@ -316,6 +332,27 @@ export default {
       this.camera.position.set(cameraZ, cameraZ, cameraZ);
       this.camera.lookAt(0, 0, 0);
       this.controls.target.set(0, 0, 0);
+
+      // 计算相机到原点的大致距离并根据模型尺寸调整 far
+      const cameraDistance = Math.sqrt(
+        this.camera.position.x * this.camera.position.x +
+          this.camera.position.y * this.camera.position.y +
+          this.camera.position.z * this.camera.position.z,
+      );
+      const desiredFar = cameraDistance + Math.max(size.x, size.y, size.z) * 2;
+      this.camera.far = Math.max(this.camera.far || 1000, desiredFar);
+      this.camera.updateProjectionMatrix();
+
+      // 为 OrbitControls 设置限制,避免相机飞离或穿透模型
+      if (this.controls) {
+        const minDist = Math.max(0.1, Math.min(maxDim * 0.5, cameraDistance * 0.1));
+        const maxDist = Math.max(cameraDistance * 10, maxDim * 10, cameraZ * 10);
+        this.controls.minDistance = minDist;
+        this.controls.maxDistance = maxDist;
+        // 限制极角,防止视角翻转
+        this.controls.maxPolarAngle = Math.PI - 0.1;
+        this.controls.update();
+      }
     },
 
     animate() {
@@ -364,6 +401,10 @@ export default {
       this.camera.updateProjectionMatrix();
 
       // 更新渲染器尺寸
+      // 同步像素比以适配当前屏幕密度,避免过高开销
+      if (this.renderer.setPixelRatio) {
+        this.renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
+      }
       this.renderer.setSize(width, height);
     },
 
@@ -388,6 +429,14 @@ export default {
       if (this.scene) {
         this.scene.clear();
       }
+
+      // 释放 PMREMGenerator
+      if (this.pmremGenerator) {
+        try {
+          this.pmremGenerator.dispose();
+        } catch (e) {}
+        this.pmremGenerator = null;
+      }
     },
 
     /**
@@ -430,6 +479,14 @@ export default {
       @include spin;
     }
 
+    .progress {
+      position: absolute;
+      top: 10px;
+      right: 28px;
+      font-size: 12px;
+      color: #666;
+    }
+
     .fullscreen-btn {
       position: absolute;
       top: 10px;

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

@@ -174,7 +174,7 @@ export default {
         background-color: $content-color;
         border-style: solid;
         border-width: 1px;
-        border-radius: 40px;
+        border-radius: 4px;
 
         &.right {
           background-color: $right-bc-color;

+ 84 - 65
src/views/book/courseware/preview/components/matching/MatchingPreview.vue

@@ -9,7 +9,7 @@
           <div
             v-for="({ content, mark, multilingual, paragraph_list }, j) in item"
             :key="mark"
-            :class="['item-wrapper', `item-${mark}`, computedAnswerClass(mark), { isMobile: isMobile }]"
+            :class="['item-wrapper', `item-${mark}`, computedAnswerClass(mark, i, j), { isMobile: isMobile }]"
             :style="{
               cursor: disabled ? 'default' : 'pointer',
               flex: isMobile ? '0 1 auto' : '',
@@ -51,6 +51,7 @@
 
 <script>
 import { getMatchingData, svgNS } from '@/views/book/courseware/data/matching';
+import { arraysIntersect } from '@/utils/validate';
 
 import PreviewMixin from '../common/PreviewMixin';
 
@@ -89,34 +90,26 @@ export default {
         let arr = [];
 
         list.forEach((item) => {
-          item.forEach(({ mark, preMark, nextMark }, j) => {
-            if (preMark.length === 0 && nextMark.length === 0) return;
-            if (nextMark.length > 0 && j === 0) {
-              nextMark.forEach((m) => {
-                arr.push([{ mark }, { mark: m }]);
-              });
+          let markArr = [];
+          item.forEach(({ mark, nextMark }, j) => {
+            if (j === 0) {
+              markArr.push(mark);
+              markArr.push([...nextMark]);
+            } else if (item.length === 3 && j === 1) {
+              markArr.push([...nextMark]);
             }
           });
+          arr.push(markArr);
         });
-
+        // 答案数组为 第一个值为固定字符串,后面为连接的值数组,如:['A', ['B', 'C'], ['1', '2']],根据此结构来判断对错、绘制连线
         this.answer.answer_list = arr;
       },
       deep: true,
     },
-    isJudgingRightWrong(cur) {
-      if (cur) {
-        this.$nextTick(() => {
-          this.clearLine();
-          this.circulateAnswerList();
-        });
-      } else {
-        // this.clearLine();
-      }
-    },
     isShowRightAnswer(cur) {
       if (cur) {
         this.$nextTick(() => {
-          this.circulateAnswerList(true);
+          this.circulateAnswerList();
         });
       }
     },
@@ -266,26 +259,38 @@ export default {
       this.createLine(mark);
     },
 
-    // 循环答案列表
-    circulateAnswerList(isShowRightAnswer = false) {
-      let answer_list = isShowRightAnswer ? this.data.answer.answer_list : this.answer.answer_list;
-      answer_list.forEach((item) => {
-        item.forEach((_m, j) => {
-          let mark = isShowRightAnswer ? _m.mark : _m;
-          if (mark.length <= 0 || j >= item.length - 1) return;
-
-          let cur = { i: -1, j: -1 }; // 当前连线点
-          this.answerList.findIndex((item, i) => {
-            return item.some((li, j) => {
-              if (li.mark === mark) {
-                cur = { i, j };
+    /**
+     * @description 循环答案列表
+     */
+    circulateAnswerList() {
+      this.data.answer.answer_list.forEach((item, index) => {
+        let preMark = ''; // 上一个连线点标识
+        item.forEach(({ mark }, j) => {
+          if (j === 0) {
+            preMark = mark;
+            return;
+          }
+          if (mark.length <= 0) return;
+          if (j === 2) {
+            preMark = this.data.option_list[index][j - 1].mark;
+          }
+          let cur = { i: -1, j: -1, mark: '' }; // 当前连线点
+
+          // 根据 preMark 查找当前连线点的位置
+          this.data.option_list.find((list, i) => {
+            return list.find((li, idx) => {
+              if (li.mark === preMark) {
+                cur = { i, j: idx };
                 return true;
               }
             });
           });
-          this.curConnectionPoint = { i: cur.i, j: cur.j, mark };
-          const nextMark = isShowRightAnswer ? item[j + 1].mark : item[j + 1].mark;
-          this.createLine(nextMark, false, isShowRightAnswer);
+
+          // 循环 mark 数组,创建连接线
+          mark.forEach((m) => {
+            this.curConnectionPoint = { i: cur.i, j: cur.j, mark: preMark };
+            this.createLine(m, false, true);
+          });
         });
       });
     },
@@ -432,42 +437,55 @@ export default {
     /**
      * 计算答题对错选项class
      * @param {string} mark 选项标识
+     * @param {number} i 选项列表索引
+     * @param {number} j 选项索引
+     * @returns {string} 返回 class 名称
      */
-    computedAnswerClass(mark) {
+    computedAnswerClass(mark, i, j) {
       if (!this.isJudgingRightWrong) return '';
-      let answer = this.data.answer.answer_list.filter((item) => {
-        return item.some((li) => li.mark === mark);
-      });
-      let userAnswer = this.answer.answer_list.filter((item) => {
-        return item.some((li) => li.mark === mark);
-      });
+      if (j === 0) {
+        return this.judgeFirstAnswerRightWrong(i);
+      }
+      let answer = this.data.answer.answer_list.find((item) => {
+        return item.some((li) => Array.isArray(li.mark) && li.mark.includes(mark));
+      }); // 正确答案列
+
+      let userAnswer = this.answer.answer_list.find((item) => {
+        return item.some((li) => Array.isArray(li) && li.includes(mark));
+      }); // 选项对应的用户答案列
+
+      if (answer === undefined || userAnswer === undefined) return 'wrong';
       if (answer.length === 0 && userAnswer.length === 0) return '';
       if (answer.length === 0 || userAnswer.length === 0) return 'wrong';
 
       let isRight = true;
-      userAnswer.forEach((item) => {
-        let len = item.length;
-        let index = item.findIndex((li) => li.mark === mark);
-        // 根据 mark 查找答案列表中对应的选项
-        let answerList = answer.filter((item) => {
-          return item.findIndex((li) => li.mark === mark) === index;
-        });
 
-        // 循环答案列表和用户答案,判断是否完全相等
-        for (let i = 0; i < answerList.length; i++) {
-          let isAllRight = true;
-          for (let j = 0; j < len; j++) {
-            if (j === index) continue;
-            let isEqual = item[j].mark === answerList[i][j].mark;
-            if (!isEqual) {
-              isAllRight = false;
-              break;
-            }
-          }
-          isRight = isAllRight;
-          if (isRight) break;
-        }
-      });
+      if (j === 1) {
+        // 判断第二行答案对错,answer[0].mark 和 userAnswer[0] 是否相等
+        isRight = answer[0].mark === userAnswer[0];
+      } else if (j === 2) {
+        // 判断第三行答案对错,answer[1].mark 和 userAnswer[1] 是否相交
+        isRight = arraysIntersect(answer[1].mark, userAnswer[1]);
+      }
+
+      return isRight ? 'right' : 'wrong';
+    },
+    /**
+     * 判断第一列答案对错,是否有个一个选项连接正确
+     * @param {number} i 选项列表索引
+     */
+    judgeFirstAnswerRightWrong(i) {
+      let answer = this.data.answer.answer_list[i]; // 正确答案列
+      let userAnswer = this.answer.answer_list[i]; // 选项对应的用户答案列
+      if (answer === undefined || userAnswer === undefined) return 'wrong';
+      if (answer.length === 0 && userAnswer.length === 0) return '';
+      if (answer.length === 0 || userAnswer.length === 0) return 'wrong';
+      let isRight = true;
+
+      // 判断 answer[1].mark 和 userAnswer[1] 是否相交
+      const answerSet = new Set(answer[1].mark);
+      isRight = userAnswer[1].some((x) => answerSet.has(x));
+
       return isRight ? 'right' : 'wrong';
     },
   },
@@ -492,6 +510,7 @@ export default {
       display: flex;
       column-gap: 8px;
       align-items: center;
+      padding: 1px;
 
       .item-wrapper {
         position: relative;
@@ -503,7 +522,7 @@ export default {
         min-height: 48px;
         padding: 12px 24px;
         background-color: $content-color;
-        border-radius: 40px;
+        border-radius: 4px;
 
         &:not(:last-child, .isMobile) {
           margin-right: 52px;

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

@@ -142,7 +142,7 @@ export default {
       cursor: pointer;
       background-color: $content-color;
       border: 1px solid $content-color;
-      border-radius: 40px;
+      border-radius: 4px;
 
       .lang {
         // width: 100%;

+ 1 - 2
src/views/book/courseware/preview/components/voice_matrix/VoiceMatrixPreview.vue

@@ -24,7 +24,6 @@
         <AudioLine
           v-show="!hasSelectedCell"
           ref="audioLine"
-          P
           audio-id="voiceMatrixAudio"
           :mp3="mp3Url"
           :get-cur-time="getCurTime"
@@ -719,7 +718,7 @@ $border-color: #e6e6e6;
             user-select: none;
             background-color: #fff;
             border: 1px solid $border-color;
-            border-radius: 8px;
+            border-radius: 4px;
             transition: 0.2s;
 
             &:hover {

+ 1 - 1
src/views/org_manage/index.vue

@@ -33,7 +33,7 @@
 
         <el-table-column prop="operation" label="操作" fixed="right" width="200" align="center" header-align="center">
           <template slot-scope="{ row }">
-            <span class="link" @click="setOrgManager(row.id)">机构管理信息</span>
+            <span class="link" @click="setOrgManager(row.id)">机构信息管理</span>
             <span class="link danger" @click="deleteOrg(row.id)">删除</span>
           </template>
         </el-table-column>

+ 1 - 1
src/views/personal_workbench/edit_task/edit/index.vue

@@ -48,7 +48,7 @@
           <span class="link" @click="showSetBackground">背景图</span>
           <span class="link" @click="saveCoursewareContent('quit')">退出编辑</span>
           <span class="link" @click="saveCoursewareContent">保存</span>
-          <span v-if="isEdit && !type" class="link" @click="showFullTextSettings">全篇设置</span>
+          <span v-if="isEdit && !type" class="link" @click="showFullTextSettings">样式调整</span>
           <span v-if="type" class="link" @click="goBackTemplateList">返回模板列表</span>
           <span v-else class="link" @click="goBackBookList">返回教材列表</span>
         </div>

+ 2 - 1
src/views/project_manage/common/PreviewURL.vue

@@ -1,6 +1,7 @@
 <template>
-  <el-dialog title="预览链接" :visible="visible" width="680px" :close-on-click-modal="false" @close="dialogClose">
+  <el-dialog title="预览链接" :visible="visible" width="720px" :close-on-click-modal="false" @close="dialogClose">
     <p class="preview-url">{{ url }}</p>
+    <h3 style="text-align: center">有效期:7天</h3>
 
     <div slot="footer">
       <el-button @click="dialogClose">关闭</el-button>

+ 1 - 1
src/views/project_manage/org/offlinepackauth/index.vue

@@ -118,7 +118,7 @@
       :percentage="percentage"
       :visible.sync="visible"
       title="导出离线包"
-      :text="`正在导出离线包,请稍候...`"
+      :text="`正在导出离线包,请等待...`"
     />
   </div>
 </template>