Kaynağa Gözat

预览优化

dsy 2 gün önce
ebeveyn
işleme
efe3726702

+ 1 - 1
.env

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

+ 23 - 3
src/components/PinyinText.vue

@@ -33,7 +33,7 @@
                   'align-items': getWordAlignItems(word, block),
                 }"
               >
-                <span class="pinyin" :style="getPinyinStyle(word)"> {{ getPinyinText(word) }}</span>
+                <span class="pinyin" :style="getPinyinStyle(word, block)"> {{ getPinyinText(word) }}</span>
                 <span class="py-char" :style="getCharStyle(word, block)">{{ convertText(word.text) }}</span>
               </span>
               <template v-else>
@@ -44,7 +44,7 @@
                       'align-items': getWordAlignItems(word, block),
                     }"
                   >
-                    <span class="pinyin" :style="getPinyinStyle(word)"> {{ getCharPinyin(word, cIndex) }}</span>
+                    <span class="pinyin" :style="getPinyinStyle(word, block)"> {{ getCharPinyin(word, cIndex) }}</span>
                     <span class="py-char" :style="getCharStyle(word, block, cIndex)">{{ convertText(char) }}</span>
                   </span>
                 </span>
@@ -606,6 +606,7 @@ export default {
         oldIndex,
         paragraphIndex,
         styleObj,
+        rawStyleText: combinedStyle,
         hasEmphasisDot,
       };
     },
@@ -619,12 +620,26 @@ export default {
       return styleObj;
     },
 
+    getBlockColor(block) {
+      const colorFromObject = block?.styleObj?.color;
+      if (colorFromObject) return colorFromObject;
+
+      const styleText = block?.rawStyleText || '';
+      const colorMatch = styleText.match(/(?:^|;)\s*color\s*:\s*([^;]+)/i);
+      return colorMatch ? colorMatch[1].trim() : '';
+    },
+
     // 获取单个字符的样式(包括着重点)
     getCharStyle(word, block) {
       const baseStyle = { ...word.activeTextStyle };
       baseStyle['font-size'] = baseStyle.fontSize;
       baseStyle['font-family'] = baseStyle.fontFamily;
 
+      const blockColor = this.getBlockColor(block);
+      if (blockColor) {
+        baseStyle.color = `${blockColor} !important`;
+      }
+
       if (this.isAllSetting) {
         baseStyle['font-size'] = this.fontSize;
         baseStyle['font-family'] = this.fontFamily;
@@ -673,7 +688,7 @@ export default {
       return 'center';
     },
     // 拼音固定为拼音字体,跟随汉字的字号、颜色、粗细的样式
-    getPinyinStyle(item) {
+    getPinyinStyle(item, block) {
       const styles = {};
 
       // 固定使用 League 字体
@@ -689,6 +704,11 @@ export default {
         }
       }
 
+      const blockColor = this.getBlockColor(block);
+      if (blockColor) {
+        styles['color'] = `${blockColor} !important`;
+      }
+
       // 如果设置了全局字号,优先使用全局字号
       if (this.isAllSetting && this.fontSize) {
         styles['font-size'] = this.fontSize;

+ 86 - 25
src/views/book/courseware/create/components/question/fill/Fill.vue

@@ -88,6 +88,7 @@
             ref="PinyinText"
             :rich-text-list="item.rich_text_list"
             :pinyin-position="data.property.pinyin_position"
+            :body-styles="getBodyStyles()"
             @fillCorrectPinyin="fillCorrectPinyin($event, i, -1, 'model_essay')"
           />
         </div>
@@ -179,10 +180,11 @@ export default {
       const arr = [];
       let totalText = '';
       let totalRichText = [];
+      const openStyleTagStack = [];
       for (let i = 0; i < text_list.length; i++) {
         const textItem = text_list[i];
         const text = textItem.text || '';
-        const isStyle = textItem.is_style === 'true';
+        const isStyle = textItem.is_style === 'true' || textItem.is_style === true;
         if (isStyle) {
           const isRichFill = /class=\s*(\\?["'])rich-fill\1/.test(text);
           if (isRichFill) {
@@ -216,37 +218,39 @@ export default {
           } else {
             totalText += text;
             totalRichText = totalRichText.concat(textItem);
+            this.syncOpenStyleTagStack(openStyleTagStack, textItem);
           }
         } else {
           const splitBlocks = this.splitTextItemByUnderline(textItem, preservedAnyOneAnswers, preservedAnyOneState);
 
+          const currentOpenStyleTags = openStyleTagStack.map((tagItem) => ({ ...tagItem }));
+          const firstBlockPrefix =
+            totalText.length > 0
+              ? this.buildFirstBlockStylePrefix(totalRichText, currentOpenStyleTags)
+              : currentOpenStyleTags;
+
           // 样式标签单独成块会导致 PinyinText 的样式栈断开,需并入紧随其后的文本块。
-          if (totalText.length > 0) {
-            if (splitBlocks.length > 0) {
+          if (splitBlocks.length > 0) {
+            if (totalText.length > 0) {
               splitBlocks[0].content = `${totalText}${splitBlocks[0].content || ''}`;
-              const inheritedOpenStyleTags = totalRichText.filter((tagItem) => {
-                const tagText = tagItem?.text || '';
-                const isStyleTag = tagItem?.is_style === 'true' || tagItem?.is_style === true;
-                if (!isStyleTag) return false;
-                if (/^<\//.test(tagText)) return false;
-                if (/^<br\s*\/?\s*>$/i.test(tagText)) return false;
-                return /^<\w+[^>]*>$/.test(tagText);
-              });
-
-              splitBlocks.forEach((block, blockIndex) => {
-                const prevRichTextList = block.rich_text_list || [];
-                block.rich_text_list = [
-                  ...(blockIndex === 0 ? totalRichText : inheritedOpenStyleTags),
-                  ...prevRichTextList,
-                ];
-              });
-            } else {
-              arr.push({
-                content: totalText,
-                type: 'text',
-                rich_text_list: totalRichText,
-              });
             }
+
+            splitBlocks.forEach((block, blockIndex) => {
+              const prevRichTextList = block.rich_text_list || [];
+              block.rich_text_list = [
+                ...(blockIndex === 0 ? firstBlockPrefix : currentOpenStyleTags),
+                ...prevRichTextList,
+              ];
+            });
+
+            totalText = '';
+            totalRichText = [];
+          } else if (totalText.length > 0) {
+            arr.push({
+              content: totalText,
+              type: 'text',
+              rich_text_list: firstBlockPrefix,
+            });
             totalText = '';
             totalRichText = [];
           }
@@ -265,6 +269,59 @@ export default {
 
       return arr;
     },
+    getStyleTagName(tagText = '') {
+      const trimmedText = String(tagText).trim();
+      const closeMatch = trimmedText.match(/^<\/(\w+)>$/);
+      if (closeMatch) return closeMatch[1].toLowerCase();
+
+      const openMatch = trimmedText.match(/^<(\w+)([^>]*)>$/);
+      if (openMatch) return openMatch[1].toLowerCase();
+
+      return '';
+    },
+    isOpenStyleTag(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;
+      if (/^<br\s*\/?\s*>$/i.test(tagText)) return false;
+      return /^<\w+[^>]*>$/.test(tagText);
+    },
+    syncOpenStyleTagStack(openStyleTagStack, styleTagItem) {
+      const tagText = String(styleTagItem?.text || '').trim();
+      const isStyleTag = styleTagItem?.is_style === 'true' || styleTagItem?.is_style === true;
+      if (!isStyleTag) return;
+      if (/^<br\s*\/?\s*>$/i.test(tagText)) return;
+
+      const closeMatch = tagText.match(/^<\/(\w+)>$/);
+      if (closeMatch) {
+        const closeTagName = closeMatch[1].toLowerCase();
+        for (let i = openStyleTagStack.length - 1; i >= 0; i--) {
+          const tagName = this.getStyleTagName(openStyleTagStack[i]?.text);
+          if (tagName === closeTagName) {
+            openStyleTagStack.splice(i, 1);
+            break;
+          }
+        }
+        return;
+      }
+
+      if (this.isOpenStyleTag(styleTagItem)) {
+        openStyleTagStack.push(styleTagItem);
+      }
+    },
+    buildFirstBlockStylePrefix(transitionStyleTags = [], currentOpenStyleTags = []) {
+      const transitionOpenTagNames = transitionStyleTags
+        .filter((tagItem) => this.isOpenStyleTag(tagItem))
+        .map((tagItem) => this.getStyleTagName(tagItem?.text));
+
+      const missingOpenTags = currentOpenStyleTags.filter((tagItem) => {
+        const tagName = this.getStyleTagName(tagItem?.text);
+        return tagName && !transitionOpenTagNames.includes(tagName);
+      });
+
+      return [...missingOpenTags, ...transitionStyleTags];
+    },
     /**
      * 根据文本中的连续下划线分割文本块,并将下划线部分转换为输入块
      * @param {Object} textItem 富文本中的一个文本项
@@ -620,6 +677,10 @@ export default {
     removeWord(index) {
       this.data.word_list.splice(index, 1);
     },
+    getBodyStyles() {
+      if (!this.$refs.richText) return {};
+      return this.$refs.richText.getBodyInitialStyles();
+    },
   },
 };
 </script>

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

@@ -3,7 +3,11 @@
     <!-- 重做 -->
     <div v-show="permissionControl.can_answer" class="button retry" @click="retry()"></div>
     <!-- 判断对错 -->
-    <div v-show="permissionControl.can_judge_correct" class="button correct" @click="judgeCorrect"></div>
+    <div
+      v-show="permissionControl.can_judge_correct && isShowAnswer"
+      class="button correct"
+      @click="judgeCorrect"
+    ></div>
     <!-- 查看答案 -->
     <div
       v-show="permissionControl.can_show_answer && isShowAnswer"

+ 19 - 1
src/views/book/courseware/preview/components/fill/FillPreview.vue

@@ -20,6 +20,7 @@
                 :paragraph-list="li.paragraph_list"
                 :rich-text-list="li.rich_text_list"
                 :pinyin-position="data.property.pinyin_position"
+                :body-styles="getBodyStyles()"
                 :is-preview="true"
               />
               <span v-else :key="`text-${i}`" class="html-content" v-html="renderTextBlockContent(li)"></span>
@@ -109,10 +110,10 @@
       <div v-if="showLang" class="lang">
         {{ data.multilingual.find((item) => item.type === getLang())?.translation }}
       </div>
+      <PreviewOperation @showAnswerAnalysis="showAnswerAnalysis" @judgeCorrect="judgeCorrect" @retry="retry" />
     </div>
 
     <WriteDialog :visible.sync="writeVisible" @confirm="handleWriteConfirm" />
-    <PreviewOperation @showAnswerAnalysis="showAnswerAnalysis" @judgeCorrect="judgeCorrect" @retry="retry" />
     <AnswerCorrect
       :visible.sync="visibleAnswerCorrect"
       :is-check-correct="isCheckCorrect"
@@ -477,6 +478,23 @@ export default {
 
       this.$set(this.inputWidthMap, mark, nextWidth);
     },
+    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;
+    },
   },
 };
 </script>

+ 7 - 1
src/views/book/courseware/preview/components/input/InputPreview.vue

@@ -28,7 +28,13 @@
       </div>
     </div>
 
-    <PreviewOperation @showAnswerAnalysis="showAnswerAnalysis" @judgeCorrect="judgeCorrect" @retry="retry" />
+    <div></div>
+    <PreviewOperation
+      :is-show-answer="false"
+      @showAnswerAnalysis="showAnswerAnalysis"
+      @judgeCorrect="judgeCorrect"
+      @retry="retry"
+    />
     <AnswerCorrect
       :answer-correct="data?.answer_correct"
       :visible.sync="visibleAnswerCorrect"

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

@@ -60,9 +60,9 @@
           </div>
         </li>
       </ul>
+      <PreviewOperation @showAnswerAnalysis="showAnswerAnalysis" @judgeCorrect="judgeCorrect" @retry="retry" />
     </div>
 
-    <PreviewOperation @showAnswerAnalysis="showAnswerAnalysis" @judgeCorrect="judgeCorrect" @retry="retry" />
     <AnswerCorrect
       :answer-correct="data?.answer_correct"
       :visible.sync="visibleAnswerCorrect"

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

@@ -38,9 +38,9 @@
           </template>
         </li>
       </ul>
+      <PreviewOperation @showAnswerAnalysis="showAnswerAnalysis" @judgeCorrect="judgeCorrect" @retry="retry" />
     </div>
 
-    <PreviewOperation @showAnswerAnalysis="showAnswerAnalysis" @judgeCorrect="judgeCorrect" @retry="retry" />
     <AnswerCorrect
       :answer-correct="data?.answer_correct"
       :visible.sync="visibleAnswerCorrect"

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

@@ -206,9 +206,9 @@
           @playing="playChange"
         />
       </div>
+      <PreviewOperation @showAnswerAnalysis="showAnswerAnalysis" @judgeCorrect="judgeCorrect" @retry="retry" />
     </div>
 
-    <PreviewOperation @showAnswerAnalysis="showAnswerAnalysis" @judgeCorrect="judgeCorrect" @retry="retry" />
     <AnswerCorrect
       :answer-correct="data?.answer_correct"
       :visible.sync="visibleAnswerCorrect"

+ 4 - 3
src/views/personal_workbench/project/ProductionEditorialManage.vue

@@ -13,8 +13,9 @@
           </span>
         </div>
         <span class="link" @click="visibleAuditSteps = true">设置审核步骤</span>
-        <div class="member-list">{{ member_list.map((member) => member.name).join(';') }}</div>
-        <span class="link" @click="selectMembers">修改</span>
+        <span class="line"></span>
+        <div class="member-list">项目成员:{{ member_list.map((member) => member.name).join(';') }}</div>
+        <span class="link" @click="selectMembers">设置项目成员</span>
         <div class="operator flex">
           <span class="link" @click="openBookUnifiedTitle()">教材标题设置</span>
           <span class="link" @click="openBookUnifiedAttrib()">教材样式设置</span>
@@ -175,7 +176,7 @@
     <BookUnifiedTitle :visible.sync="visibleTitle" :book-id="book_id" />
     <SelectMembers
       :visible.sync="visibleMembers"
-      title="选择项目成员"
+      title="设置项目成员"
       :selected-list="memberInfoList"
       type="member"
       @confirm="handleSelectedMembers"