Ver Fonte

1. 填空题字体问题 2. 填空题的横线位置向下,作答的文字底部与文章内容对齐,字体上答案字体和学生输入字体保持一致 3. 填空题编辑和预览效果不一致问题,以及统一显示效果

dsy há 2 dias atrás
pai
commit
58616f07d6

+ 1 - 1
.env

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

+ 1 - 1
packages/package.json

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

+ 27 - 11
src/components/PinyinText.vue

@@ -516,7 +516,7 @@ export default {
       const altMatch = attrs.match(/alt=["']([^"']*)["']/i);
       const widthMatch = attrs.match(/width=["']?(\d+)["']?/i);
       const heightMatch = attrs.match(/height=["']?(\d+)["']?/i);
-      const styleMatch = attrs.match(/style=["']([^"']*)["']/i);
+      const styleValue = this.extractStyleAttr(attrs);
 
       const containerStyleObj = {};
       tagStack.forEach((tagItem) => {
@@ -526,8 +526,8 @@ export default {
       });
 
       const imageStyleObj = {};
-      if (styleMatch) {
-        this.mergeStyleString(imageStyleObj, styleMatch[1]);
+      if (styleValue) {
+        this.mergeStyleString(imageStyleObj, styleValue);
       }
 
       return {
@@ -550,7 +550,7 @@ export default {
       const posterMatch = attrs.match(/poster=["']([^"']*)["']/i);
       const widthMatch = attrs.match(/width=["']?(\d+)["']?/i);
       const heightMatch = attrs.match(/height=["']?(\d+)["']?/i);
-      const styleMatch = attrs.match(/style=["']([^"']*)["']/i);
+      const styleValue = this.extractStyleAttr(attrs);
 
       // 直接从完整的文本中匹配 source
       const sourceMatch = item.text.match(/<source\s+([^>]*)\/?\s*>/i);
@@ -574,8 +574,8 @@ export default {
       });
 
       const mediaStyleObj = {};
-      if (styleMatch) {
-        this.mergeStyleString(mediaStyleObj, styleMatch[1]);
+      if (styleValue) {
+        this.mergeStyleString(mediaStyleObj, styleValue);
       }
 
       return {
@@ -597,7 +597,7 @@ export default {
 
       const attrs = audioMatch[1];
       const srcMatch = attrs.match(/src=["']([^"']*)["']/i);
-      const styleMatch = attrs.match(/style=["']([^"']*)["']/i);
+      const styleValue = this.extractStyleAttr(attrs);
 
       const containerStyleObj = {};
       tagStack.forEach((tagItem) => {
@@ -607,8 +607,8 @@ export default {
       });
 
       const mediaStyleObj = {};
-      if (styleMatch) {
-        this.mergeStyleString(mediaStyleObj, styleMatch[1]);
+      if (styleValue) {
+        this.mergeStyleString(mediaStyleObj, styleValue);
       }
 
       return {
@@ -651,10 +651,26 @@ export default {
       }
     },
 
+    /**
+     * 提取标签属性中的 style 值。
+     * style 的属性值内部可能包含与包裹引号不同的引号,例如
+     * style="font-family: 'times new roman', times, serif;",
+     * 因此必须按包裹引号成对匹配;若使用 ["']([^"']*)["'] 这类写法,会在内层单引号处
+     * 提前截断(只得到 "font-family: "),导致字体等样式被丢弃、显示效果与编辑器设置不一致。
+     * @param {String} attrs 标签的属性字符串(不含标签名)
+     * @returns {String} style 属性值,未匹配到时返回空字符串
+     */
+    extractStyleAttr(attrs = '') {
+      const styleMatch = String(attrs).match(/\bstyle\s*=\s*(?:"([^"]*)"|'([^']*)')/i);
+      if (!styleMatch) return '';
+      const [, doubleQuotedStyle, singleQuotedStyle] = styleMatch;
+      if (doubleQuotedStyle === undefined) return singleQuotedStyle || '';
+      return doubleQuotedStyle;
+    },
+
     // 提取标签信息(样式和类名)
     extractTagInfo(attrs, tagName) {
-      const styleMatch = attrs.match(/style=["']([^"']*)["']/);
-      let style = styleMatch ? styleMatch[1] : null;
+      let style = this.extractStyleAttr(attrs) || null;
 
       const classMatch = attrs.match(/class=["']([^"']*)["']/);
       const className = classMatch ? classMatch[1] : '';

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

@@ -327,6 +327,19 @@ export default {
           return;
         }
 
+        // 无可见内容的块中出现块级开标签(如 <p style="text-align: center;">)时,
+        // 说明这部分内容已属于下一个段落(例如下一段的首行缩进),必须单独成块:
+        // 若并入上一块,下一段落的对齐方式会被当成上一段落的对齐方式、缩进也会丢失,
+        // 导致预览与富文本编辑器中的效果不一致。
+        if ((richTextList || []).some((item) => this.isBlockOpenTag(item))) {
+          blocks.push({
+            content,
+            type: 'text',
+            rich_text_list: richTextList,
+          });
+          return;
+        }
+
         for (let i = blocks.length - 1; i >= 0; i--) {
           if (blocks[i] && blocks[i].type === 'text') {
             blocks[i].content = `${blocks[i].content || ''}${content}`;
@@ -430,7 +443,9 @@ export default {
         } else {
           // 段间空白(如 </p> 之后的 \n):前一段落为块级(带对齐)时冗余,丢弃;
           // 否则保留作为段落换行分隔,避免破坏正常换行结构。
-          if (!String(text).trim() && this.isLastTextBlockAligned(arr)) {
+          // 段首缩进等其他空白属于下一个段落,需保留(由 appendTextBlock 单独成块),
+          // 否则缩进会丢失,或被渲染到上一段落的行尾。
+          if (!String(text).trim() && this.isLastTextBlockAligned(arr) && /^[\r\n]+$/.test(String(text))) {
             // 先把累积的闭合标签并入前一个文本块,保持标签配对
             if (totalText.length > 0) {
               this.appendTextBlock(arr, totalText, totalRichText);
@@ -515,6 +530,18 @@ export default {
       return /^<\w+[^>]*>$/.test(tagText);
     },
     /**
+     * 判断标签项是否为块级开标签(如 <p>、<div>、<h1>),这类标签标志着一个新段落的开始。
+     * @param {Object} tagItem 标签项对象,包含 text 和 is_style 属性
+     * @returns {Boolean} 是否为块级开标签
+     */
+    isBlockOpenTag(tagItem = {}) {
+      const tagText = String(tagItem?.text || '').trim();
+      const isStyleTag = tagItem?.is_style === 'true' || tagItem?.is_style === true;
+      if (!isStyleTag) return false;
+      if (/^<\//.test(tagText)) return false;
+      return /^<(p|div|h[1-6]|li|blockquote)\b[^>]*>$/i.test(tagText);
+    },
+    /**
      * 同步更新样式标签栈,遇到开标签入栈,遇到闭标签出栈
      * @param {Array} openStyleTagStack 当前的开标签栈
      * @param {Object} styleTagItem 当前处理的标签项对象
@@ -935,6 +962,10 @@ export default {
 }
 
 .pinyin-wrapper {
+  // 与编辑器(.rich-wrapper)保持一致:占满容器宽度,
+  // 否则容器宽度会被内容(最长的一行)撑满,居中/右对齐段落将对不齐
+  width: 100%;
+
   .model-essay-group {
     display: inline;
   }

+ 14 - 4
src/views/book/courseware/preview/components/fill/FillPreview.vue

@@ -144,7 +144,7 @@
       :analysis-list="data.analysis_list"
       @closeAnswerAnalysis="closeAnswerAnalysis"
     >
-      <div slot="right-answer" class="fill-wrapper">
+      <div slot="right-answer" class="fill-wrapper" :style="{ '--fill-font-family': data.property.fill_font }">
         <div class="fill-content">
           <template v-for="(group, gi) in groupedModelEssay">
             <span :key="`answer-group-${gi}`" :style="groupStyle(group)">
@@ -653,7 +653,9 @@ export default {
     }
 
     .select-content {
-      display: inline-block;
+      // 选中的词贴到控件底部:文字底部与正文文字底部对齐(下边框即填空横线)
+      display: inline-flex;
+      align-items: flex-end;
       height: 32px;
       margin: 0 10px;
       vertical-align: bottom;
@@ -670,6 +672,7 @@ export default {
       align-items: center;
       width: 120px;
       margin: 0 10px;
+      margin-bottom: -2px;
       vertical-align: bottom;
 
       &.pinyin :deep input.el-input__inner {
@@ -697,9 +700,12 @@ export default {
       }
 
       :deep .el-input__inner {
+        // 1em 行盒:输入文字基线与正文文字对齐,下边框即填空横线
+        height: 1em;
         padding: 0;
         font-family: var(--fill-font-family, 'arial'), sans-serif;
         font-size: 16pt;
+        line-height: 1em;
         color: $font-color;
         text-align: center;
         background-color: #fff;
@@ -710,11 +716,15 @@ export default {
     }
 
     .right-answer {
+      // 盒模型/行高与填空输入框一致:文字底部贴正文文字底部,下边框即填空横线
+      // 字体与学生输入保持一致(--fill-font-family 由模板传入)
       position: relative;
+      box-sizing: border-box;
       display: inline-block;
-      height: 32px;
+      height: 1em;
       margin: 0 4px;
-      line-height: 32px;
+      font-family: var(--fill-font-family, 'arial'), sans-serif;
+      line-height: 1em;
       vertical-align: bottom;
       border-bottom: 1px solid $font-color;
     }

+ 26 - 13
src/views/book/courseware/preview/components/table/TablePreview.vue

@@ -836,7 +836,7 @@ export default {
       const altMatch = attrs.match(/alt=["']([^"']*)["']/i);
       const widthMatch = attrs.match(/width=["']?(\d+)["']?/i);
       const heightMatch = attrs.match(/height=["']?(\d+)["']?/i);
-      const styleMatch = attrs.match(/style=["']([^"']*)["']/i);
+      const styleValue = this.extractStyleAttr(attrs);
 
       const containerStyleObj = {};
       tagStack.forEach((tagItem) => {
@@ -846,8 +846,8 @@ export default {
       });
 
       const imageStyleObj = {};
-      if (styleMatch) {
-        this.mergeStyleString(imageStyleObj, styleMatch[1]);
+      if (styleValue) {
+        this.mergeStyleString(imageStyleObj, styleValue);
       }
 
       return {
@@ -974,8 +974,8 @@ export default {
       const baseStyle = {};
       baseStyle['font-size'] = styles ? styles.fontSize : '16px';
       baseStyle['--input-font-family'] = styles ? styles.fontFamily : '';
-      baseStyle['width'] = Math.max(40, words.value.length * 21.3) + 'px';
-      baseStyle['height'] = styles && styles.fontSize ? styles.fontSize.replace('pt', '') * 1.5 + 'pt' : '24px';
+      baseStyle['width'] = `${Math.max(40, words.value.length * 21.3)}px`;
+      baseStyle['height'] = styles && styles.fontSize ? `${styles.fontSize.replace('pt', '') * 1.5}pt` : '24px';
 
       return baseStyle;
     },
@@ -987,10 +987,23 @@ export default {
       }
       return 'center';
     },
+    /**
+     * 提取标签属性中的 style 值。
+     * style 的属性值内部可能包含与包裹引号不同的引号(如 style="font-family: 'times new roman'"),
+     * 必须按包裹引号成对匹配,否则会在内层引号处提前截断,导致字体等样式丢失。
+     * @param {String} attrs 标签的属性字符串(不含标签名)
+     * @returns {String} style 属性值,未匹配到时返回空字符串
+     */
+    extractStyleAttr(attrs = '') {
+      const styleMatch = String(attrs).match(/\bstyle\s*=\s*(?:"([^"]*)"|'([^']*)')/i);
+      if (!styleMatch) return '';
+      const [, doubleQuotedStyle, singleQuotedStyle] = styleMatch;
+      if (doubleQuotedStyle === undefined) return singleQuotedStyle || '';
+      return doubleQuotedStyle;
+    },
     // 提取标签信息(样式和类名)
     extractTagInfo(attrs, tagName) {
-      const styleMatch = attrs.match(/style=["']([^"']*)["']/);
-      let style = styleMatch ? styleMatch[1] : null;
+      let style = this.extractStyleAttr(attrs) || null;
 
       const classMatch = attrs.match(/class=["']([^"']*)["']/);
       const className = classMatch ? classMatch[1] : '';
@@ -1033,7 +1046,7 @@ export default {
       const posterMatch = attrs.match(/poster=["']([^"']*)["']/i);
       const widthMatch = attrs.match(/width=["']?(\d+)["']?/i);
       const heightMatch = attrs.match(/height=["']?(\d+)["']?/i);
-      const styleMatch = attrs.match(/style=["']([^"']*)["']/i);
+      const styleValue = this.extractStyleAttr(attrs);
 
       // 直接从完整的文本中匹配 source
       const sourceMatch = item.text.match(/<source\s+([^>]*)\/?\s*>/i);
@@ -1057,8 +1070,8 @@ export default {
       });
 
       const mediaStyleObj = {};
-      if (styleMatch) {
-        this.mergeStyleString(mediaStyleObj, styleMatch[1]);
+      if (styleValue) {
+        this.mergeStyleString(mediaStyleObj, styleValue);
       }
 
       return {
@@ -1080,7 +1093,7 @@ export default {
 
       const attrs = audioMatch[1];
       const srcMatch = attrs.match(/src=["']([^"']*)["']/i);
-      const styleMatch = attrs.match(/style=["']([^"']*)["']/i);
+      const styleValue = this.extractStyleAttr(attrs);
 
       const containerStyleObj = {};
       tagStack.forEach((tagItem) => {
@@ -1090,8 +1103,8 @@ export default {
       });
 
       const mediaStyleObj = {};
-      if (styleMatch) {
-        this.mergeStyleString(mediaStyleObj, styleMatch[1]);
+      if (styleValue) {
+        this.mergeStyleString(mediaStyleObj, styleValue);
       }
 
       return {

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

@@ -339,7 +339,7 @@ export default {
     },
     updateServerAddress() {
       if (process.env.NODE_ENV === 'development' && this.form.server_address !== '/production') {
-        return updateBaseURL('/eep');
+        return updateBaseURL('/production');
       }
       updateBaseURL(this.form.server_address);
     },

+ 1 - 1
src/views/personal_workbench/check_task/audit/index.vue

@@ -125,7 +125,7 @@ export default {
 
   .main-container {
     flex: 1;
-    min-width: 1110px;
+    min-width: 1230px;
     overflow: auto;
   }