2
0

3 Incheckningar d331b34bb5 ... a03518a956

Upphovsman SHA1 Meddelande Datum
  zq a03518a956 富文本拼音效果优化 2 veckor sedan
  dsy 4f9f5ddb80 项目管理新增成员列表与修改 2 veckor sedan
  dsy 54404f0dfc 1、填空题问题修改 2、作答模式修改 2 veckor sedan
24 ändrade filer med 497 tillägg och 220 borttagningar
  1. 1 1
      .env
  2. BIN
      src/assets/component/component-answer.png
  3. BIN
      src/assets/component/component-correct.png
  4. BIN
      src/assets/component/component-remark.png
  5. BIN
      src/assets/component/component-retry.png
  6. BIN
      src/assets/component/component-view-remark.png
  7. 2 29
      src/components/CommonPreview.vue
  8. 6 2
      src/components/PinyinText.vue
  9. 39 0
      src/components/RichText.vue
  10. 0 69
      src/components/SimAnswerPermissionControl.vue
  11. 6 0
      src/views/book/courseware/create/components/base/rich_text/RichText.vue
  12. 253 14
      src/views/book/courseware/create/components/question/fill/Fill.vue
  13. 12 8
      src/views/book/courseware/preview/common/AnswerCorrect.vue
  14. 19 5
      src/views/book/courseware/preview/common/PreviewOperation.vue
  15. 12 3
      src/views/book/courseware/preview/components/common/PreviewMixin.js
  16. 74 82
      src/views/book/courseware/preview/components/fill/FillPreview.vue
  17. 2 1
      src/views/book/courseware/preview/components/input/InputPreview.vue
  18. 2 1
      src/views/book/courseware/preview/components/judge/JudgePreview.vue
  19. 2 1
      src/views/book/courseware/preview/components/matching/MatchingPreview.vue
  20. 19 0
      src/views/book/courseware/preview/components/rich_text/RichTextPreview.vue
  21. 2 1
      src/views/book/courseware/preview/components/select/SelectPreview.vue
  22. 2 1
      src/views/book/courseware/preview/components/sort/SortPreview.vue
  23. 2 1
      src/views/book/courseware/preview/components/voice_matrix/VoiceMatrixPreview.vue
  24. 42 1
      src/views/personal_workbench/project/ProductionEditorialManage.vue

+ 1 - 1
.env

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

BIN
src/assets/component/component-answer.png


BIN
src/assets/component/component-correct.png


BIN
src/assets/component/component-remark.png


BIN
src/assets/component/component-retry.png


BIN
src/assets/component/component-view-remark.png


+ 2 - 29
src/components/CommonPreview.vue

@@ -10,12 +10,8 @@
         <span class="flow-nodename">{{ courseware_info.cur_audit_flow_node_name }}</span>
         <slot name="middle" :courseware="courseware_info"></slot>
         <template v-if="type === 'edit_preview'">
-          <i class="el-icon-setting" style="cursor: pointer" @click="openSimulateAnswerPermissionControl()"></i>
           <span class="link" @click="createCoursewarePreviewURL()">生成课件预览链接</span>
         </template>
-        <div class="group">
-          <el-checkbox v-model="isJudgeCorrect">判断对错</el-checkbox>
-        </div>
         <div v-if="isShowGroup">
           <span class="link" @click="isShowGroup = false">取消显示分组</span>
           <span
@@ -466,7 +462,6 @@
       title-text="翻译 (机器翻译)"
     />
 
-    <SimAnswerPermissionControl :visible.sync="visiblePermissionControl" :permission-control.sync="permissionControl" />
     <PreviewURL :url="preview_url" :visible.sync="visiblePreviewURL" />
   </div>
 </template>
@@ -480,7 +475,6 @@ import AudioPlay from '@/views/book/courseware/preview/components/common/AudioPl
 import AuditRemark from '@/components/AuditRemark.vue';
 import ExplanatoryNoteDialog from '@/components/ExplanatoryNoteDialog.vue';
 import TranslateDialog from '@/components/TranslateDialog.vue';
-import SimAnswerPermissionControl from '@/components/SimAnswerPermissionControl.vue';
 import PreviewURL from '@/views/project_manage/common/PreviewURL.vue';
 import Remark from './Remark.vue';
 
@@ -530,7 +524,6 @@ export default {
     AuditRemark,
     ExplanatoryNoteDialog,
     VisNetwork,
-    SimAnswerPermissionControl,
     PreviewURL,
     Remark,
     TranslateDialog,
@@ -685,8 +678,6 @@ export default {
       lang: 'ZH',
       chinese: 'zh-Hans', // 语言简体中文zh-Hans,繁体中文zh-Hant
       showPinYin: 'false',
-      isJudgeCorrect: false,
-      isShowAnswer: false,
       curSelectId: this.id,
       navigationShow: true,
       sidebarShow: true,
@@ -711,11 +702,10 @@ export default {
       multimediaIsAllShow: false,
       isFullScreen: false, // 是否全屏状态
       title_list: [],
-      visiblePermissionControl: false, // 模拟答题权限控制弹窗显示状态
       permissionControl: {
         can_answer: true, // 可作答
-        can_judge_correct: false, // 可判断对错(客观题)
-        can_show_answer: false, // 可查看答案
+        can_judge_correct: true, // 可判断对错(客观题)
+        can_show_answer: true, // 可查看答案
         can_correct: false, // 可批改
         can_check_correct: false, // 可查看批改
       },
@@ -749,15 +739,6 @@ export default {
     },
   },
   watch: {
-    isJudgeCorrect(newVal) {
-      if (!newVal) {
-        this.isShowAnswer = false;
-      }
-      this.simulateAnswer(newVal);
-    },
-    isShowAnswer() {
-      this.simulateAnswer();
-    },
     curSelectId() {
       if (this.curToolbarIcon === 'note') {
         this.getNote();
@@ -1323,10 +1304,6 @@ export default {
       }
     },
 
-    simulateAnswer(disabled = true) {
-      this.$refs.courseware.simulateAnswer(this.isJudgeCorrect, this.isShowAnswer, disabled);
-    },
-
     /**
      * 选择节点
      * @param {string} nodeId - 节点ID
@@ -1752,10 +1729,6 @@ export default {
     fullScreen() {
       toggleFullScreen(this.$refs.previewMain);
     },
-    // 打开模拟答题权限控制弹窗
-    openSimulateAnswerPermissionControl() {
-      this.visiblePermissionControl = true;
-    },
     // 创建课件预览链接
     createCoursewarePreviewURL() {
       CreateCoursewarePreviewURL({ courseware_id: this.select_node, valid_day_count: 20 }).then(({ url }) => {

+ 6 - 2
src/components/PinyinText.vue

@@ -7,7 +7,7 @@
     />
 
     <!-- 新规则:使用 richTextList -->
-    <div v-if="richTextList && richTextList.length > 0" class="rich-text-container">
+    <div v-if="richTextList && richTextList.length > 0" class="rich-text-container" :style="bodyStyles">
       <template v-for="(block, index) in parsedBlocks">
         <!-- 文字块:包含 word_list -->
         <span
@@ -107,7 +107,7 @@
 
     <!-- 老规则:使用 paragraphList -->
     <template v-else-if="paragraphList && paragraphList.length > 0">
-      <div v-for="(paragraph, pIndex) in paragraphList" :key="pIndex" class="pinyin-paragraph">
+      <div v-for="(paragraph, pIndex) in paragraphList" :key="pIndex" class="pinyin-paragraph" :style="bodyStyles">
         <div
           v-for="(sentence, sIndex) in paragraph"
           :key="sIndex"
@@ -236,6 +236,10 @@ export default {
       type: String,
       default: '0px 0px 0px 0px',
     },
+    bodyStyles: {
+      type: Object,
+      default: () => ({}),
+    },
   },
   data() {
     return {

+ 39 - 0
src/components/RichText.vue

@@ -1399,6 +1399,45 @@ export default {
       });
       return walker.nextNode();
     },
+
+    /**
+     * 获取富文本body元素的初始样式信息
+     * @returns {object} 包含字体、字号、颜色等body初始样式属性的对象
+     */
+    getBodyInitialStyles() {
+      const editor = tinymce.activeEditor;
+      if (!editor) return {};
+
+      const body = editor.getBody();
+      if (!body) return {};
+
+      const computed = window.getComputedStyle(body);
+      const styles = {};
+
+      // 获取字体家族
+      if (computed.fontFamily && computed.fontFamily !== 'inherit') {
+        styles.fontFamily = computed.fontFamily;
+      }
+
+      // 获取字体大小(px转pt)
+      if (computed.fontSize && computed.fontSize !== 'inherit') {
+        const fontSize = computed.fontSize;
+        const pxValue = parseFloat(fontSize);
+        if (!isNaN(pxValue)) {
+          const ptValue = Math.round(pxValue * 0.75 * 10) / 10;
+          styles.fontSize = `${ptValue}pt`;
+        } else {
+          styles.fontSize = fontSize;
+        }
+      }
+
+      // 获取字体颜色
+      if (computed.color && computed.color !== 'inherit' && computed.color !== 'rgb(0, 0, 0)') {
+        styles.color = computed.color;
+      }
+
+      return styles;
+    },
   },
 };
 </script>

+ 0 - 69
src/components/SimAnswerPermissionControl.vue

@@ -1,69 +0,0 @@
-<template>
-  <el-dialog
-    :visible="visible"
-    :before-close="handleClose"
-    :close-on-click-modal="false"
-    title="模拟答题权限控制"
-    width="650px"
-  >
-    <div class="permission-control">
-      <el-checkbox v-model="permission.can_answer">可作答</el-checkbox>
-      <el-checkbox v-model="permission.can_judge_correct">可判断对错(客观题)</el-checkbox>
-      <el-checkbox v-model="permission.can_show_answer">可查看答案</el-checkbox>
-      <el-checkbox v-model="permission.can_correct">可批改</el-checkbox>
-      <el-checkbox v-model="permission.can_check_correct">可查看批改</el-checkbox>
-    </div>
-
-    <div slot="footer">
-      <el-button @click="handleClose()">关闭</el-button>
-      <el-button type="primary" @click="confirm()">保存</el-button>
-    </div>
-  </el-dialog>
-</template>
-
-<script>
-export default {
-  name: 'SimAnswerPermissionControl',
-  props: {
-    visible: {
-      type: Boolean,
-      required: true,
-    },
-    permissionControl: {
-      type: Object,
-      required: true,
-    },
-  },
-  data() {
-    return {
-      permission: {
-        can_answer: true, // 可作答
-        can_judge_correct: false, // 可判断对错
-        can_show_answer: false, // 可查看答案
-        can_correct: false, // 可批改
-        can_check_correct: false, // 可查看批改
-      },
-    };
-  },
-  watch: {
-    permissionControl: {
-      handler(newVal) {
-        this.permission = { ...newVal };
-      },
-      immediate: true,
-      deep: true,
-    },
-  },
-  methods: {
-    handleClose() {
-      this.$emit('update:visible', false);
-    },
-    confirm() {
-      this.$emit('update:permissionControl', { ...this.permission });
-      this.handleClose();
-    },
-  },
-};
-</script>
-
-<style lang="scss" scoped></style>

+ 6 - 0
src/views/book/courseware/create/components/base/rich_text/RichText.vue

@@ -11,6 +11,7 @@
           :is-view-pinyin="isEnable(data.property.view_pinyin)"
           :font-size="data?.unified_attrib?.font_size"
           :font-family="data?.unified_attrib?.font"
+          :font-color="data?.unified_attrib?.text_color"
           placeholder="输入内容"
           @view-explanatory-note="viewExplanatoryNote"
           @selectContentSetMemo="selectContentSetMemo"
@@ -103,6 +104,7 @@
           :font-family="data?.unified_attrib?.font"
           :component-type="data.type"
           :pinyin-padding="data.property.pinyin_padding"
+          :body-styles="getBodyStyles()"
           @fillCorrectPinyin="fillCorrectPinyin"
         />
       </div>
@@ -547,6 +549,10 @@ export default {
         }
       });
     },
+    getBodyStyles() {
+      if (!this.$refs.richText) return {};
+      return this.$refs.richText.getBodyInitialStyles();
+    },
   },
 };
 </script>

+ 253 - 14
src/views/book/courseware/create/components/question/fill/Fill.vue

@@ -14,7 +14,7 @@
           :font-size="data?.unified_attrib?.font_size"
           :font-family="data?.unified_attrib?.font"
           :font-color="data?.unified_attrib?.text_color"
-          @handleRichTextBlur="identifyText(false)"
+          @handleRichTextBlur="parsedContentPinyin"
         />
         <div v-if="data.property.fill_type === fillTypeList[1].value" class="select-vocabulary">
           <h5 class="title">选词列表:</h5>
@@ -52,7 +52,7 @@
         </template>
 
         <div>
-          <el-button @click="identifyText">识别</el-button>
+          <el-button @click="parsedContentPinyin">识别</el-button>
           <el-button @click="openMultilingual">多语言</el-button>
         </div>
 
@@ -78,21 +78,18 @@
           icon="el-icon-refresh"
           title="刷新"
           class="refresh-pinyin-btn"
-          @click.native="handleViewPinyin"
+          @click.native="parsedContentPinyin"
         />
       </el-divider>
       <template v-if="isEnable(data.property.view_pinyin)">
         <div v-for="(item, i) in data.model_essay" :key="i" class="pinyin-text-list">
-          <template v-for="(li, j) in item">
-            <PinyinText
-              :key="`${i}-${j}`"
-              ref="PinyinText"
-              :paragraph-list="li.paragraph_list"
-              :rich-text-list="li.rich_text_list"
-              :pinyin-position="data.property.pinyin_position"
-              @fillCorrectPinyin="fillCorrectPinyin($event, i, j, 'model_essay')"
-            />
-          </template>
+          <PinyinText
+            :key="`pinyin-${i}`"
+            ref="PinyinText"
+            :rich-text-list="item.rich_text_list"
+            :pinyin-position="data.property.pinyin_position"
+            @fillCorrectPinyin="fillCorrectPinyin($event, i, -1, 'model_essay')"
+          />
         </div>
       </template>
 
@@ -127,6 +124,7 @@ import { addTone, handleToneValue } from '@/views/book/courseware/data/common';
 import { getRandomNumber } from '@/utils';
 import { TextToAudioFile } from '@/api/app';
 import { sanitizeHTML } from '@/utils/common';
+import { PinyinBuild_OldFormat } from '@/api/book';
 
 export default {
   name: 'FillPage',
@@ -143,6 +141,7 @@ export default {
       fillTypeList,
       sanitizeHTML,
       visibleWord: false,
+      rich_text_list: [],
     };
   },
   watch: {
@@ -150,12 +149,250 @@ export default {
     'data.property.fill_font': 'handleMindMap',
     'data.property': {
       handler() {
-        this.handleViewPinyin();
+        this.parsedContentPinyin();
       },
       deep: true,
     },
   },
   methods: {
+    async parsedContentPinyin() {
+      if (!this.isEnable(this.data.property.view_pinyin)) {
+        return;
+      }
+      PinyinBuild_OldFormat({
+        text: this.data.content,
+        is_first_sentence_first_hz_pinyin_first_char_upper_case:
+          this.data.property.is_first_sentence_first_hz_pinyin_first_char_upper_case,
+        is_fill_space: 'true',
+        is_rich_text: 'true',
+      }).then(({ rich_text }) => {
+        if (rich_text) {
+          this.rich_text_list = rich_text.text_list;
+          this.data.model_essay = this.parseRichText();
+        }
+      });
+    },
+    parseRichText() {
+      let text_list = this.rich_text_list || [];
+      this.data.answer.answer_list = [];
+      const arr = [];
+      let totalText = '';
+      let totalRichText = [];
+      for (let i = 0; i < text_list.length; i++) {
+        const textItem = text_list[i];
+        const text = textItem.text || '';
+        const isStyle = textItem.is_style === 'true';
+        if (isStyle) {
+          const isRichFill = /class=\s*(\\?["'])rich-fill\1/.test(text);
+          if (isRichFill) {
+            if (totalText.length > 0) {
+              arr.push({
+                content: totalText,
+                type: 'text',
+                rich_text_list: totalRichText,
+              });
+              totalText = '';
+              totalRichText = [];
+            }
+            let nextData = text_list[i + 1] || {};
+            let nextTag = text_list[i + 2] || {};
+            const mark = getRandomNumber();
+            arr.push({
+              content: text + (nextData.text || '') + (nextTag.text || ''),
+              type: 'input',
+              input: '',
+              audio_answer_list: [],
+              mark,
+              rich_text_list: [textItem, nextData, nextTag],
+            });
+            this.data.answer.answer_list.push({
+              value: nextData.text || '',
+              mark,
+              type: 'only_one',
+            });
+            // 跳过下一个文本和标签数据
+            i += 2;
+          } else {
+            totalText += text;
+            totalRichText = totalRichText.concat(textItem);
+          }
+        } else {
+          const splitBlocks = this.splitTextItemByUnderline(textItem);
+
+          // 样式标签单独成块会导致 PinyinText 的样式栈断开,需并入紧随其后的文本块。
+          if (totalText.length > 0) {
+            if (splitBlocks.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,
+              });
+            }
+            totalText = '';
+            totalRichText = [];
+          }
+
+          arr.push(...splitBlocks);
+        }
+      }
+
+      if (totalText.length > 0) {
+        arr.push({
+          content: totalText,
+          type: 'text',
+          rich_text_list: totalRichText,
+        });
+      }
+
+      return arr;
+    },
+    splitTextItemByUnderline(textItem) {
+      const text = textItem?.text || '';
+      const matcher = /_{3,}/g;
+      const blocks = [];
+      let lastIndex = 0;
+      let match = matcher.exec(text);
+
+      while (match) {
+        const underlineText = match[0] || '';
+        const start = match.index;
+        const end = start + underlineText.length;
+
+        if (start > lastIndex) {
+          const textContent = text.slice(lastIndex, start);
+          blocks.push({
+            content: textContent,
+            type: 'text',
+            rich_text_list: [
+              {
+                ...textItem,
+                text: textContent,
+                word_list: this.sliceWordListByTextRange(textItem.word_list, lastIndex, start),
+              },
+            ],
+          });
+        }
+
+        const mark = getRandomNumber();
+        blocks.push({
+          content: underlineText,
+          type: 'input',
+          input: '',
+          audio_answer_list: [],
+          mark,
+          rich_text_list: [
+            {
+              ...textItem,
+              text: underlineText,
+              word_list: this.sliceWordListByTextRange(textItem.word_list, start, end),
+            },
+          ],
+        });
+        this.data.answer.answer_list.push({
+          value: '',
+          mark,
+          type: 'any_one',
+        });
+
+        lastIndex = end;
+        match = matcher.exec(text);
+      }
+
+      if (lastIndex < text.length) {
+        const textContent = text.slice(lastIndex);
+        blocks.push({
+          content: textContent,
+          type: 'text',
+          rich_text_list: [
+            {
+              ...textItem,
+              text: textContent,
+              word_list: this.sliceWordListByTextRange(textItem.word_list, lastIndex, text.length),
+            },
+          ],
+        });
+      }
+
+      if (blocks.length === 0) {
+        return [
+          {
+            content: text,
+            type: 'text',
+            rich_text_list: [textItem],
+          },
+        ];
+      }
+
+      return blocks;
+    },
+    sliceWordListByTextRange(wordList = [], rangeStart = 0, rangeEnd = 0) {
+      if (!Array.isArray(wordList) || rangeEnd <= rangeStart) return [];
+
+      const result = [];
+      let cursor = 0;
+
+      wordList.forEach((wordItem) => {
+        const wordText = wordItem?.text || '';
+        const wordStart = cursor;
+        const wordEnd = wordStart + wordText.length;
+        cursor = wordEnd;
+
+        const overlapStart = Math.max(rangeStart, wordStart);
+        const overlapEnd = Math.min(rangeEnd, wordEnd);
+
+        if (overlapStart >= overlapEnd) return;
+
+        const relativeStart = overlapStart - wordStart;
+        const relativeEnd = overlapEnd - wordStart;
+
+        result.push(this.sliceWordItem(wordItem, relativeStart, relativeEnd));
+      });
+
+      return result;
+    },
+    sliceWordItem(wordItem, start, end) {
+      const fullText = wordItem?.text || '';
+      const slicedText = fullText.slice(start, end);
+      const slicedWord = {
+        ...wordItem,
+        text: slicedText,
+      };
+
+      const sourcePinyinList = Array.isArray(wordItem?.pinyin_list) ? wordItem.pinyin_list : [];
+
+      if (sourcePinyinList.length === fullText.length) {
+        slicedWord.pinyin_list = sourcePinyinList.slice(start, end);
+        slicedWord.pinyin = (slicedWord.pinyin_list || []).join(' ').trim();
+        return slicedWord;
+      }
+
+      if (start === 0 && end === fullText.length) {
+        slicedWord.pinyin_list = [...sourcePinyinList];
+        return slicedWord;
+      }
+
+      slicedWord.pinyin_list = new Array(slicedText.length).fill('');
+      slicedWord.pinyin = '';
+      return slicedWord;
+    },
     /**
      * 识别文本中
      * @param {Boolean} isUpdatePinyin 是否更新拼音,默认为 true
@@ -490,6 +727,8 @@ export default {
 }
 
 .pinyin-text-list {
+  display: inline;
+
   :deep .pinyin-area {
     display: inline;
   }

+ 12 - 8
src/views/book/courseware/preview/common/AnswerCorrect.vue

@@ -2,19 +2,14 @@
 <template>
   <el-dialog
     v-dialogDrag
-    title="批改"
+    :title="title"
     custom-class="answer-correct-dialog"
     :visible="visible"
     width="40vw"
     :close-on-click-modal="false"
     :before-close="handleClose"
   >
-    <RichText
-      v-if="!permissionControl.can_check_correct"
-      ref="richText"
-      v-model="correct"
-      placeholder="请输入批改内容"
-    />
+    <RichText v-if="!isCheckCorrect" ref="richText" v-model="correct" placeholder="请输入批改内容" />
     <div v-else class="rich-text" v-html="sanitizeHTML(correct)"></div>
 
     <div slot="footer" class="dialog-footer">
@@ -44,6 +39,10 @@ export default {
       type: String,
       default: '',
     },
+    isCheckCorrect: {
+      type: Boolean,
+      default: false,
+    },
   },
   data() {
     return {
@@ -55,6 +54,9 @@ export default {
     permissionControl() {
       return this.getPermissionControl();
     },
+    title() {
+      return this.isCheckCorrect ? '查看批改' : '批改';
+    },
   },
   watch: {
     answerCorrect: {
@@ -69,7 +71,9 @@ export default {
       this.$emit('update:visible', false);
     },
     confirm() {
-      this.$emit('closeAnswerCorrect', this.correct);
+      if (!this.isCheckCorrect) {
+        this.$emit('closeAnswerCorrect', this.correct);
+      }
       this.handleClose();
     },
   },

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

@@ -1,16 +1,19 @@
 <template>
   <div class="operation">
+    <!-- 重做 -->
     <div v-show="permissionControl.can_answer" class="button retry" @click="retry()"></div>
-    <div
-      v-show="permissionControl.can_correct || permissionControl.can_check_correct"
-      class="button correct"
-      @click="openAnswerCorrect()"
-    ></div>
+    <!-- 判断对错 -->
+    <div v-show="permissionControl.can_judge_correct" class="button correct" @click="judgeCorrect"></div>
+    <!-- 查看答案 -->
     <div
       v-show="permissionControl.can_show_answer && isShowAnswer"
       class="button answer"
       @click="showAnswerAnalysis()"
     ></div>
+    <!-- 批改 -->
+    <div v-show="permissionControl.can_correct" class="button remark" @click="openAnswerCorrect(false)"></div>
+    <!-- 查看批改 -->
+    <div v-show="permissionControl.can_check_correct" class="button view-remark" @click="openAnswerCorrect(true)"></div>
   </div>
 </template>
 
@@ -46,6 +49,9 @@ export default {
     retry() {
       this.$emit('retry');
     },
+    judgeCorrect() {
+      this.$emit('judgeCorrect');
+    },
   },
 };
 </script>
@@ -78,6 +84,14 @@ export default {
     &.answer {
       background: url('@/assets/component/component-answer.png') no-repeat center;
     }
+
+    &.remark {
+      background: url('@/assets/component/component-remark.png') no-repeat center;
+    }
+
+    &.view-remark {
+      background: url('@/assets/component/component-view-remark.png') no-repeat center;
+    }
   }
 }
 </style>

+ 12 - 3
src/views/book/courseware/preview/components/common/PreviewMixin.js

@@ -23,11 +23,12 @@ const mixin = {
       visibleAnswerAnalysis: false, // 是否显示答案解析弹窗
       answerAnalysisState: null, // 答案解析弹窗前的状态快照
       visibleAnswerCorrect: false, // 是否显示批改弹窗
+      isCheckCorrect: false, // 是否查看批改信息
     };
   },
   provide() {
     return {
-      openAnswerCorrect: () => this.openAnswerCorrect(),
+      openAnswerCorrect: (isCheckCorrect) => this.openAnswerCorrect(isCheckCorrect),
     };
   },
   inject: ['getLang', 'getChinese', 'convertText', 'getTitleList', 'getPermissionControl'],
@@ -146,6 +147,10 @@ const mixin = {
       this.disabled = disabled;
       if (userAnswer) this.answer = userAnswer;
     },
+    judgeCorrect() {
+      this.isJudgingRightWrong = !this.isJudgingRightWrong;
+      this.isShowRightAnswer = false;
+    },
     /**
      * 获取批改信息
      * @returns {string} 批改信息
@@ -283,9 +288,13 @@ const mixin = {
         this.isShowRightAnswer = false;
       }
     },
-    // 显示批改页面
-    openAnswerCorrect() {
+    /**
+     * 显示批改页面
+     * @param {boolean} isCheckCorrect 是否查看批改
+     */
+    openAnswerCorrect(isCheckCorrect) {
       this.visibleAnswerCorrect = true;
+      this.isCheckCorrect = isCheckCorrect;
     },
     /**
      * @description 关闭批改页面,并传递批改信息

+ 74 - 82
src/views/book/courseware/preview/components/fill/FillPreview.vue

@@ -10,25 +10,25 @@
         :file-id="data.audio_file_id"
       />
       <div class="fill-wrapper">
-        <p v-for="(item, i) in modelEssay" :key="i">
-          <template v-for="(li, j) in item">
+        <p>
+          <template v-for="(li, i) in modelEssay">
             <template v-if="li.type === 'text'">
               <PinyinText
                 v-if="isEnable(data.property.view_pinyin)"
-                :key="`${i}-${j}`"
+                :key="`text-${i}`"
                 class="content"
                 :paragraph-list="li.paragraph_list"
                 :rich-text-list="li.rich_text_list"
                 :pinyin-position="data.property.pinyin_position"
                 :is-preview="true"
               />
-              <span v-else :key="j" v-html="convertText(sanitizeHTML(li.content))"></span>
+              <span v-else :key="`text-${i}`" class="html-content" v-html="renderTextBlockContent(li)"></span>
             </template>
             <template v-if="li.type === 'input'">
               <!-- 输入填空 -->
               <template v-if="data.property.fill_type === fillTypeList[0].value">
                 <el-input
-                  :key="j"
+                  :key="`input-${i}`"
                   :ref="`input-${li.mark}`"
                   v-model="li.input"
                   :disabled="disabled"
@@ -45,7 +45,7 @@
 
               <!-- 选词填空 -->
               <template v-else-if="data.property.fill_type === fillTypeList[1].value">
-                <el-popover :key="j" placement="top" trigger="click">
+                <el-popover :key="`popover-${i}`" placement="top" trigger="click">
                   <div class="word-list">
                     <span
                       v-for="{ content, mark } in data.word_list"
@@ -68,7 +68,7 @@
 
               <!-- 手写填空 -->
               <template v-else-if="data.property.fill_type === fillTypeList[2].value">
-                <span :key="j" class="write-click" @click="handleWriteClick(li.mark)">
+                <span :key="`write-${i}`" class="write-click" @click="handleWriteClick(li.mark)">
                   <img
                     v-show="li.write_base64"
                     style="background-color: #f4f4f4"
@@ -82,7 +82,7 @@
               <template v-else-if="data.property.fill_type === fillTypeList[3].value">
                 <SoundRecordBox
                   ref="record"
-                  :key="j"
+                  :key="`record-${i}`"
                   type="mini"
                   :many-times="false"
                   class="record-box"
@@ -112,8 +112,12 @@
     </div>
 
     <WriteDialog :visible.sync="writeVisible" @confirm="handleWriteConfirm" />
-    <PreviewOperation :is-show-answer="isShowAnswer" @showAnswerAnalysis="showAnswerAnalysis" @retry="retry" />
-    <AnswerCorrect :visible.sync="visibleAnswerCorrect" @closeAnswerCorrect="closeAnswerCorrect" />
+    <PreviewOperation @showAnswerAnalysis="showAnswerAnalysis" @judgeCorrect="judgeCorrect" @retry="retry" />
+    <AnswerCorrect
+      :visible.sync="visibleAnswerCorrect"
+      :is-check-correct="isCheckCorrect"
+      @closeAnswerCorrect="closeAnswerCorrect"
+    />
     <AnswerAnalysis
       :visible.sync="visibleAnswerAnalysis"
       :answer-list="data.answer_list"
@@ -121,22 +125,22 @@
       @closeAnswerAnalysis="closeAnswerAnalysis"
     >
       <div slot="right-answer" class="fill-wrapper">
-        <p v-for="(item, i) in modelEssay" :key="i">
-          <template v-for="(li, j) in item">
+        <p>
+          <template v-for="(li, i) in modelEssay">
             <template v-if="li.type === 'text'">
               <PinyinText
                 v-if="isEnable(data.property.view_pinyin)"
-                :key="`${i}-${j}`"
+                :key="`answer-text-${i}`"
                 class="content"
                 :paragraph-list="li.paragraph_list"
                 :rich-text-list="li.rich_text_list"
                 :pinyin-position="data.property.pinyin_position"
                 :is-preview="true"
               />
-              <span v-else :key="j" v-html="convertText(sanitizeHTML(li.content))"></span>
+              <span v-else :key="`answer-text-${i}`" class="html-content" v-html="renderTextBlockContent(li)"></span>
             </template>
             <template v-if="li.type === 'input'">
-              <span v-show="computedAnswerText(li.mark).length > 0" :key="`answer-${j}`" class="right-answer">
+              <span v-show="computedAnswerText(li.mark).length > 0" :key="`answer-${i}`" class="right-answer">
                 {{ computedAnswerText(li.mark) }}
               </span>
             </template>
@@ -176,36 +180,13 @@ export default {
       writeMark: '',
     };
   },
-  computed: {
-    isShowAnswer() {
-      return (
-        (Array.isArray(this.data.answer_list) && this.data.answer_list.length > 0) ||
-        (Array.isArray(this.data.analysis_list) && this.data.analysis_list.length > 0)
-      );
-    },
-  },
   watch: {
     'data.model_essay': {
       handler(list) {
         if (!list || !Array.isArray(list)) return;
 
         this.modelEssay = JSON.parse(JSON.stringify(list));
-        this.answer.answer_list = list
-          .map((item) => {
-            return item
-              .map(({ type, content, audio_answer_list, mark }) => {
-                if (type === 'input') {
-                  return {
-                    value: content,
-                    mark,
-                    audio_answer_list,
-                    write_base64: '',
-                  };
-                }
-              })
-              .filter((item) => item);
-          })
-          .flat();
+        this.syncAnswerList(list);
       },
       deep: true,
       immediate: true,
@@ -214,22 +195,7 @@ export default {
       handler(list) {
         if (!list || !Array.isArray(list)) return;
 
-        this.answer.answer_list = list
-          .map((item) => {
-            return item
-              .map(({ type, input, audio_answer_list, mark }) => {
-                if (type === 'input') {
-                  return {
-                    value: input,
-                    mark,
-                    audio_answer_list,
-                    write_base64: '',
-                  };
-                }
-              })
-              .filter((item) => item);
-          })
-          .flat();
+        this.syncAnswerList(list);
       },
       deep: true,
       immediate: true,
@@ -238,13 +204,10 @@ export default {
       if (!val) return;
 
       this.answer.answer_list.forEach(({ mark, value }) => {
-        this.modelEssay.forEach((item) => {
-          item.forEach((li) => {
-            if (li.mark === mark) {
-              li.input = value;
-            }
-          });
-        });
+        const li = this.modelEssay.find((item) => item.mark === mark);
+        if (li) {
+          li.input = value;
+        }
       });
 
       this.handleWav(this.answer.record_list);
@@ -257,6 +220,36 @@ export default {
     handleWav(data) {
       this.data.record_list = data;
     },
+    renderContent(content) {
+      return this.convertText(this.sanitizeHTML(content));
+    },
+    /**
+     * 渲染文本块内容
+     * @param {Object} textBlock 文本块对象,包含纯文本内容和富文本内容
+     */
+    renderTextBlockContent(textBlock) {
+      const richTextList = textBlock?.rich_text_list;
+      if (Array.isArray(richTextList) && richTextList.length > 0) {
+        const richHtml = richTextList.map((item) => item?.text || '').join('');
+        return this.renderContent(richHtml);
+      }
+      return this.renderContent(textBlock?.content || '');
+    },
+    syncAnswerList(list) {
+      this.answer.answer_list = list
+        .map(({ type, input, audio_answer_list, mark }) => {
+          if (type === 'input') {
+            return {
+              value: input,
+              mark,
+              audio_answer_list,
+              write_base64: '',
+            };
+          }
+          return null;
+        })
+        .filter((item) => item);
+    },
     /**
      * 处理小音频录音
      * @param {Object} data 音频数据
@@ -264,12 +257,9 @@ export default {
      */
     handleMiniWav(data, mark) {
       if (!data || !mark) return;
-      for (const item of this.modelEssay) {
-        const li = item.find((li) => li?.mark === mark);
-        if (li) {
-          this.$set(li, 'audio_answer_list', data);
-          break;
-        }
+      const li = this.modelEssay.find((item) => item?.mark === mark);
+      if (li) {
+        this.$set(li, 'audio_answer_list', data);
       }
     },
     /**
@@ -290,12 +280,9 @@ export default {
      */
     handleWriteConfirm(data) {
       if (!data) return;
-      for (const item of this.modelEssay) {
-        const li = item.find((li) => li?.mark === this.writeMark);
-        if (li) {
-          this.$set(li, 'write_base64', data);
-          break;
-        }
+      const li = this.modelEssay.find((item) => item?.mark === this.writeMark);
+      if (li) {
+        this.$set(li, 'write_base64', data);
       }
     },
     handleWriteClick(mark) {
@@ -382,7 +369,7 @@ export default {
       }
       let selectOption = this.answer.answer_list.find((item) => item.mark === mark);
       let answerOption = this.data.answer.answer_list.find((item) => item.mark === mark);
-      if (!selectOption) return '';
+      if (!selectOption || !answerOption) return '';
       let selectValue = selectOption.value;
       let answerValue = answerOption.value;
       let answerType = answerOption.type;
@@ -404,18 +391,17 @@ export default {
      */
     computedAnswerText(mark) {
       let answerOption = this.data.answer.answer_list.find((item) => item.mark === mark);
+      if (!answerOption) return '';
       let answerValue = answerOption.value;
       return `${answerValue}`;
     },
     // 重做
     retry() {
-      this.modelEssay.forEach((item) => {
-        item.forEach((li) => {
-          if (li.type === 'input') {
-            li.input = '';
-            li.write_base64 = '';
-          }
-        });
+      this.modelEssay.forEach((li) => {
+        if (li.type === 'input') {
+          li.input = '';
+          li.write_base64 = '';
+        }
       });
       this.selectedWordList = [];
       this.handleWav([]);
@@ -522,6 +508,12 @@ export default {
       display: inline;
     }
 
+    .html-content {
+      :deep p {
+        display: inline;
+      }
+    }
+
     .record-box {
       display: inline-flex;
       align-items: center;

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

@@ -28,10 +28,11 @@
       </div>
     </div>
 
-    <PreviewOperation @showAnswerAnalysis="showAnswerAnalysis" @retry="retry" />
+    <PreviewOperation @showAnswerAnalysis="showAnswerAnalysis" @judgeCorrect="judgeCorrect" @retry="retry" />
     <AnswerCorrect
       :answer-correct="data?.answer_correct"
       :visible.sync="visibleAnswerCorrect"
+      :is-check-correct="isCheckCorrect"
       @closeAnswerCorrect="closeAnswerCorrect"
     />
     <AnswerAnalysis

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

@@ -62,10 +62,11 @@
       </ul>
     </div>
 
-    <PreviewOperation @showAnswerAnalysis="showAnswerAnalysis" @retry="retry" />
+    <PreviewOperation @showAnswerAnalysis="showAnswerAnalysis" @judgeCorrect="judgeCorrect" @retry="retry" />
     <AnswerCorrect
       :answer-correct="data?.answer_correct"
       :visible.sync="visibleAnswerCorrect"
+      :is-check-correct="isCheckCorrect"
       @closeAnswerCorrect="closeAnswerCorrect"
     />
     <AnswerAnalysis

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

@@ -40,10 +40,11 @@
       </ul>
     </div>
 
-    <PreviewOperation @showAnswerAnalysis="showAnswerAnalysis" @retry="retry" />
+    <PreviewOperation @showAnswerAnalysis="showAnswerAnalysis" @judgeCorrect="judgeCorrect" @retry="retry" />
     <AnswerCorrect
       :answer-correct="data?.answer_correct"
       :visible.sync="visibleAnswerCorrect"
+      :is-check-correct="isCheckCorrect"
       @closeAnswerCorrect="closeAnswerCorrect"
     />
     <AnswerAnalysis

+ 19 - 0
src/views/book/courseware/preview/components/rich_text/RichTextPreview.vue

@@ -20,6 +20,7 @@
           :font-family="data?.unified_attrib?.font"
           :pinyin-padding="data.property.pinyin_padding"
           :is-preview="isPreview"
+          :body-styles="getBodyStyles()"
         />
         <div v-else>
           <AudioPlay
@@ -145,6 +146,24 @@ export default {
 
       return noTextContentData;
     },
+
+    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>

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

@@ -44,7 +44,7 @@
         </li>
       </ul>
 
-      <PreviewOperation @showAnswerAnalysis="showAnswerAnalysis" @retry="retry" />
+      <PreviewOperation @showAnswerAnalysis="showAnswerAnalysis" @judgeCorrect="judgeCorrect" @retry="retry" />
     </div>
 
     <AnswerCorrect
@@ -56,6 +56,7 @@
       :visible.sync="visibleAnswerAnalysis"
       :answer-list="data.answer_list"
       :analysis-list="data.analysis_list"
+      :is-check-correct="isCheckCorrect"
       @closeAnswerAnalysis="closeAnswerAnalysis"
     >
       <ul

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

@@ -40,12 +40,13 @@
           </transition-group>
         </draggable>
       </ul>
-      <PreviewOperation @showAnswerAnalysis="showAnswerAnalysis" @retry="retry" />
+      <PreviewOperation @showAnswerAnalysis="showAnswerAnalysis" @judgeCorrect="judgeCorrect" @retry="retry" />
     </div>
 
     <AnswerCorrect
       :answer-correct="data?.answer_correct"
       :visible.sync="visibleAnswerCorrect"
+      :is-check-correct="isCheckCorrect"
       @closeAnswerCorrect="closeAnswerCorrect"
     />
     <AnswerAnalysis

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

@@ -208,10 +208,11 @@
       </div>
     </div>
 
-    <PreviewOperation @showAnswerAnalysis="showAnswerAnalysis" @retry="retry" />
+    <PreviewOperation @showAnswerAnalysis="showAnswerAnalysis" @judgeCorrect="judgeCorrect" @retry="retry" />
     <AnswerCorrect
       :answer-correct="data?.answer_correct"
       :visible.sync="visibleAnswerCorrect"
+      :is-check-correct="isCheckCorrect"
       @closeAnswerCorrect="closeAnswerCorrect"
     />
     <AnswerAnalysis

+ 42 - 1
src/views/personal_workbench/project/ProductionEditorialManage.vue

@@ -13,6 +13,8 @@
           </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>
         <div class="operator flex">
           <span class="link" @click="openBookUnifiedTitle()">教材标题设置</span>
           <span class="link" @click="openBookUnifiedAttrib()">教材样式设置</span>
@@ -171,6 +173,13 @@
     <UpdateName :name="name.name" :visible.sync="name.visible" @submit="updateName" />
     <BookUnifiedAttr :visible.sync="visibleAttr" :book-id="book_id" />
     <BookUnifiedTitle :visible.sync="visibleTitle" :book-id="book_id" />
+    <SelectMembers
+      :visible.sync="visibleMembers"
+      title="选择项目成员"
+      :selected-list="memberInfoList"
+      type="member"
+      @confirm="handleSelectedMembers"
+    />
   </div>
 </template>
 
@@ -184,8 +193,10 @@ import UpdateName from './components/UpdateName.vue';
 import BookUnifiedAttr from './components/BookUnifiedAttr.vue';
 import BookUnifiedTitle from './components/BookUnifiedTitle.vue';
 import VueDraggable from 'vuedraggable';
+import SelectMembers from '@/views/create_project/selectProjectMembers.vue';
 
-import { GetProjectBaseInfo } from '@/api/project';
+import { GetProjectBaseInfo, SetProjectMember } from '@/api/project';
+import { GetUserList_ID } from '@/api/user';
 import {
   ChapterGetBookChapterStructExpandList,
   ChapterAddChapterToBook,
@@ -213,6 +224,7 @@ export default {
     BookUnifiedAttr,
     BookUnifiedTitle,
     VueDraggable,
+    SelectMembers,
   },
   data() {
     return {
@@ -251,6 +263,8 @@ export default {
         targetId: '',
         dragging: false,
       },
+      memberInfoList: [], // 项目成员信息列表
+      visibleMembers: false,
     };
   },
   created() {
@@ -272,6 +286,7 @@ export default {
         this.member_list = member_list;
         this.book_info_PBE = book_info_PBE;
         this.audit_node_list = audit_node_list;
+        this.getUserList_ID();
       });
     },
     /**
@@ -653,6 +668,32 @@ export default {
           this.dragCtx.targetId = '';
         });
     },
+    /**
+     * 得到用户列表(指定ID)
+     */
+    getUserList_ID() {
+      const id_list = this.member_list.map((member) => member.id);
+      GetUserList_ID({ id_list }).then(({ user_list }) => {
+        this.memberInfoList = user_list;
+      });
+    },
+    selectMembers() {
+      this.visibleMembers = true;
+    },
+    /**
+     * 处理选择的成员
+     * @param {Array} selectedUsers - 选中的用户列表
+     */
+    handleSelectedMembers(selectedUsers) {
+      const user_id_list = selectedUsers.map((user) => user.id);
+      SetProjectMember({
+        project_id: this.book_id,
+        user_id_list,
+      }).then(() => {
+        this.$message.success('项目成员更新成功');
+        this.getProjectBaseInfo();
+      });
+    },
   },
 };
 </script>