Ver código fonte

1、填空题问题修复 2、判断题无答案项不判断正误

dsy 6 dias atrás
pai
commit
32ab85317e

+ 1 - 1
.env

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

+ 4 - 7
src/api/project.js

@@ -378,19 +378,16 @@ export function DeleteProjectInvitePerson(data) {
  * @description 得到背景色列表(常用)
  * @param {object} data
  */
-export function GetBackgroundColorList_Common(data) {
-  return http.post(
-    `${process.env.VUE_APP_EepServer}?MethodName=project_resource_manager-GetBackgroundColorList_Common`,
-    data,
-  );
+export function GetColorList_Common(data) {
+  return http.post(`${process.env.VUE_APP_EepServer}?MethodName=project_resource_manager-GetColorList_Common`, data);
 }
 /**
  * @description 得到背景色列表(最近使用)
  * @param {object} data
  */
-export function GetBackgroundColorList_RecentlyUsed(data) {
+export function GetColorList_RecentlyUsed(data) {
   return http.post(
-    `${process.env.VUE_APP_EepServer}?MethodName=project_resource_manager-GetBackgroundColorList_RecentlyUsed`,
+    `${process.env.VUE_APP_EepServer}?MethodName=project_resource_manager-GetColorList_RecentlyUsed`,
     data,
   );
 }

+ 14 - 7
src/components/ColorPicker.vue

@@ -8,11 +8,18 @@
 </template>
 
 <script>
-import { GetBackgroundColorList_Common, GetBackgroundColorList_RecentlyUsed } from '@/api/project';
+import { GetColorList_Common, GetColorList_RecentlyUsed } from '@/api/project';
 
 export default {
   name: 'ColorPicker',
   inheritAttrs: false,
+  props: {
+    // 常用颜色类型
+    type: {
+      type: Number,
+      default: 0,
+    },
+  },
   data() {
     return {
       commonList: [], // 常用颜色
@@ -28,17 +35,17 @@ export default {
     },
   },
   created() {
-    this.getBackgroundColorList_Common();
-    this.getBackgroundColorList_RecentlyUsed();
+    this.getColorList_Common();
+    this.getColorList_RecentlyUsed();
   },
   methods: {
-    getBackgroundColorList_Common() {
-      GetBackgroundColorList_Common().then(({ color_list }) => {
+    getColorList_Common() {
+      GetColorList_Common({ type: this.type }).then(({ color_list }) => {
         this.commonList = color_list;
       });
     },
-    getBackgroundColorList_RecentlyUsed() {
-      GetBackgroundColorList_RecentlyUsed().then(({ color_list }) => {
+    getColorList_RecentlyUsed() {
+      GetColorList_RecentlyUsed({ type: this.type }).then(({ color_list }) => {
         this.recentlyUsedList = color_list;
       });
     },

+ 3 - 1
src/components/CommonPreview.vue

@@ -1274,7 +1274,9 @@ export default {
     },
 
     /**
-     * 使用OpenCC解析HTML并仅转换文本节点,保留属性(包括内联样式/font-family)保持不变。防止字体名称像“微软雅黑' 正在转换为'微軟雅黑'.
+     * 使用OpenCC解析HTML并仅转换文本节点,保留属性(包括内联样式/font-family)保持不变。防止字体名称如:“微软雅黑' 转换为 '微軟雅黑'
+     * @param {string} html - 要转换的HTML字符串
+     * @returns {string} - 转换后的HTML字符串
      */
     convertHtmlPreserveAttributes(html) {
       try {

+ 1 - 1
src/components/PinyinText.vue

@@ -611,7 +611,7 @@ export default {
     },
 
     // 获取单个字符的样式(包括着重点)
-    getCharStyle(word, block, charIndex) {
+    getCharStyle(word, block) {
       const baseStyle = { ...word.activeTextStyle };
       baseStyle['font-size'] = baseStyle.fontSize;
       baseStyle['font-family'] = baseStyle.fontFamily;

+ 13 - 8
src/components/RichText.vue

@@ -1325,6 +1325,11 @@ export default {
       }
     },
 
+    /**
+     * 获取富文本内容中第一个字符的样式信息,返回一个包含字体、字号、颜色、加粗、下划线、删除线等样式属性的对象
+     * 该函数通过查找编辑器内容中的第一个文本节点,并逐级向上遍历其父元素,获取计算样式来确定最终的样式属性值
+     * @returns {object} 包含字体、字号、颜色、加粗、下划线、删除线等样式属性的对象
+     */
     getFirstCharStyles() {
       const editor = tinymce.activeEditor;
       if (!editor) return {};
@@ -1383,15 +1388,15 @@ export default {
       return styles;
     },
 
+    /**
+     * 在编辑器内容中查找第一个文本节点的函数
+     * @param {HTMLElement} element - 要搜索的根元素,通常是编辑器的内容区域
+     * @returns {Text|null} - 返回找到的第一个文本节点,如果没有找到则返回null
+     */
     findFirstTextNode(element) {
-      const walker = document.createTreeWalker(
-        element,
-        NodeFilter.SHOW_TEXT,
-        {
-          acceptNode: (node) => (node.textContent.trim() ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT),
-        },
-        false,
-      );
+      const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, {
+        acceptNode: (node) => (node.textContent.trim() ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT),
+      });
       return walker.nextNode();
     },
   },

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

@@ -10,7 +10,7 @@
     <el-form ref="form" :model="unified_attrib" label-width="80px" size="small">
       <el-form-item label="主题色">
         <div class="color-group">
-          <ColorPicker v-model="unified_attrib.topic_color" />
+          <ColorPicker v-model="unified_attrib.topic_color" :type="2" />
           <span>辅助色</span>
           <el-color-picker v-model="unified_attrib.assist_color" />
           <span class="link" @click="generateAssistColor">自动生成辅助色</span>
@@ -79,7 +79,7 @@
         </el-button>
       </el-form-item>
       <el-form-item label="文字颜色">
-        <ColorPicker v-model="unified_attrib.text_color" />
+        <ColorPicker v-model="unified_attrib.text_color" :type="1" />
         <el-button type="primary" class="row-button" @click="applySingleAttrToSelectedComponents('text_color')">
           应用选中组件
         </el-button>

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

@@ -96,7 +96,7 @@
             <el-checkbox v-model="background.has_color">颜色</el-checkbox>
           </div>
           <div class="setup-content">
-            <ColorPicker v-model="background.color" show-alpha />
+            <ColorPicker v-model="background.color" show-alpha :type="0" />
           </div>
         </div>
         <div class="setup-item">

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

@@ -87,7 +87,7 @@
             <el-radio v-model="background.mode" label="color">颜色</el-radio>
           </div>
           <div class="setup-content">
-            <ColorPicker v-model="background.color" show-alpha />
+            <ColorPicker v-model="background.color" show-alpha :type="0" />
           </div>
         </div>
         <div class="setup-item">

+ 72 - 55
src/views/book/courseware/create/components/question/fill/Fill.vue

@@ -9,17 +9,23 @@
           ref="richText"
           v-model="data.content"
           :is-fill="true"
-          toolbar="fontselect fontsizeselect forecolor backcolor | underline | bold italic strikethrough alignleft aligncenter alignright"
+          toolbar="fontselect fontsizeselect forecolor backcolor | lineheight paragraphSpacing underline | bold italic strikethrough alignleft aligncenter alignright bullist numlist dotEmphasis"
           :wordlimit-num="false"
           :font-size="data?.unified_attrib?.font_size"
           :font-family="data?.unified_attrib?.font"
           :font-color="data?.unified_attrib?.text_color"
+          @handleRichTextBlur="identifyText"
         />
         <div v-if="data.property.fill_type === fillTypeList[1].value" class="select-vocabulary">
           <h5 class="title">选词列表:</h5>
           <el-button size="mini" @click="openAddWord">添加词汇</el-button>
           <ul class="word-list">
-            <li v-for="item in data.word_list" :key="item.mark" v-html="sanitizeHTML(item.content)"></li>
+            <li v-for="(item, index) in data.word_list" :key="item.mark" class="word-item">
+              <span v-html="sanitizeHTML(item.content)"></span>
+              <el-button type="text" size="mini" class="delete-word" @click="removeWord(index)">
+                <SvgIcon icon-class="delete-black" size="12" />
+              </el-button>
+            </li>
           </ul>
         </div>
 
@@ -69,7 +75,6 @@
         <div v-for="(item, i) in data.model_essay" :key="i" class="pinyin-text-list">
           <template v-for="(li, j) in item">
             <PinyinText
-              v-if="li.type === 'text'"
               :key="`${i}-${j}`"
               ref="PinyinText"
               :paragraph-list="li.paragraph_list"
@@ -107,7 +112,7 @@ import UploadAudio from '@/views/book/courseware/create/components/question/fill
 import PinyinText from '@/components/PinyinText.vue';
 import AddWord from './components/AddWord.vue';
 
-import { getFillData, arrangeTypeList, fillFontList, fillTypeList } from '@/views/book/courseware/data/fill';
+import { getFillData, arrangeTypeList, fillTypeList } from '@/views/book/courseware/data/fill';
 import { addTone, handleToneValue } from '@/views/book/courseware/data/common';
 import { getRandomNumber } from '@/utils';
 import { TextToAudioFile } from '@/api/app';
@@ -143,21 +148,33 @@ export default {
   methods: {
     // 识别文本
     identifyText() {
-      this.data.model_essay = [];
       this.data.answer.answer_list = [];
+      const content = this.data.content || '';
+
+      // 使用 class 为 rich-fill 的 span 以及连续 3 个及以上下划线作为分割符
+      if (!content || !content.match(/<span[^>]*class="[^"]*rich-fill[^"]*"[^>]*>(.*?)<\/span>|_{3,}/gi)) {
+        this.data.model_essay = [
+          [
+            {
+              content,
+              type: 'text',
+              paragraph_list: [],
+              paragraph_list_parameter: { text: '', pinyin_proofread_word_list: [] },
+            },
+          ],
+        ];
+        return;
+      }
+
+      const splitSource = content.split('\n').map((item) => {
+        // rich-fill 和 ___ 均转为统一占位符,交给 splitRichText 处理
+        return this.splitRichText(
+          item.replace(/<span[^>]*class="[^"]*rich-fill[^"]*"[^>]*>.*?<\/span>|_{3,}/gi, '###$&###'),
+        );
+      });
+
+      this.data.model_essay = splitSource;
 
-      this.data.content
-        .split(/<(p|div)[^>]*>(.*?)<\/(p|div)>/g)
-        .filter((s) => s && !s.match(/^(p|div)$/))
-        .forEach((item) => {
-          if (item.charCodeAt() === 10) return;
-          let str = item
-            // 去除所有的 font-size 样式
-            .replace(/font-size:\s*\d+(\.\d+)?px;/gi, '')
-            // 匹配 class 名为 rich-fill 的 span 标签和三个以上的_,并将它们组成数组
-            .replace(/<span class="rich-fill".*?>(.*?)<\/span>|([_]{3,})/gi, '###$1$2###');
-          this.data.model_essay.push(this.splitRichText(str));
-        });
       this.handleViewPinyin();
     },
     /**
@@ -194,25 +211,32 @@ export default {
           continue;
         }
 
-        // 奇数索引为输入段(被 ### 包裹的内容)
-        const isUnderline = /^_{3,}$/.test(content);
+        // 奇数索引为输入段(被 ### 包裹的分割符)
+        const separatorContent = content;
+        const isUnderline = /^_{3,}$/.test(separatorContent);
+        const richFillMatch = separatorContent.match(/^<span[^>]*class="[^"]*rich-fill[^"]*"[^>]*>(.*?)<\/span>$/i);
+        const answerValue = isUnderline ? '' : richFillMatch ? richFillMatch[1] : separatorContent;
         const mark = getRandomNumber();
+
         arr.push({
-          content: isUnderline ? '' : content,
+          content: separatorContent,
           type: 'input',
+          input: '',
           audio_answer_list: [],
           mark,
+          paragraph_list: [],
+          paragraph_list_parameter: {
+            text: '',
+            pinyin_proofread_word_list: [],
+          },
         });
 
         // 同步更新答案列表
         this.data.answer.answer_list.push({
-          value: isUnderline ? '' : content,
+          value: answerValue,
           mark,
           type: isUnderline ? 'any_one' : 'only_one',
         });
-
-        // 将 content 设置为空,为预览准备
-        arr[arr.length - 1].content = '';
       }
       return arr;
     },
@@ -222,18 +246,17 @@ export default {
      * @param {Number} index 当前索引
      * @param {Array} parts 分割后的数组
      * @param {String} content 当前内容
-     * @returns {String} 包含前一个标签的内容
+     * @returns {String} 包含两个标签内容中最后一个html标签的内容
      */
     setTag(index, parts, content) {
+      if (index < 2) return content;
       let _content = content;
-      const isEndWithTag = /<\/[^>]+>$/.test(_content);
+      const isEndWithTag = /<\/[^>]+>$/.test(_content); // 判断是否以标签结尾
       let startTag = '';
-      for (let j = index - 1; j >= 0; j--) {
-        const prev = parts[j] ?? '';
-        const m = prev.match(/^<[^>]+>/);
-        if (m) {
-          startTag = m[0];
-        }
+      const part = parts[index - 2] ?? '';
+      const tagMatch = part.match(/<[^>]+>/g);
+      if (tagMatch) {
+        startTag = tagMatch[tagMatch.length - 1]; // 获取最后一个标签
       }
       _content = `${startTag}${_content}`;
       if (!isEndWithTag) {
@@ -282,43 +305,24 @@ export default {
      * @description 处理思维导图数据
      */
     handleMindMap() {
-      const { fill_font, arrange_type } = this.data.property;
-      const fontLabel = fillFontList.find((item) => item.value === fill_font)?.label || '';
+      const { arrange_type } = this.data.property;
       const arrangeLabel = arrangeTypeList.find((item) => item.value === arrange_type)?.label || '';
       this.data.mind_map.node_list = [
         {
-          name: `${arrangeLabel}${fontLabel}填空组件`,
+          name: `${arrangeLabel}填空组件`,
         },
       ];
     },
     handleViewPinyin() {
       if (!this.isEnable(this.data.property.view_pinyin)) {
-        this.data.model_essay.forEach((item) => {
-          item.forEach((option) => {
-            option.paragraph_list = [];
-            option.paragraph_list_parameter = {
-              text: '',
-              pinyin_proofread_word_list: [],
-            };
-          });
-        });
-        return;
-      }
-
-      if (
-        this.data.model_essay.length > 0 &&
-        this.data.model_essay[0].length > 0 &&
-        this.data.model_essay[0][0].paragraph_list.length > 0
-      ) {
         return;
       }
 
       this.data.model_essay.forEach((item, i) => {
         item.forEach((option, j) => {
           const text = option.content;
-
-          if (!text) return;
           option.paragraph_list_parameter.text = text;
+
           this.createParsedTextInfoPinyin(text, i, j, 'model_essay');
         });
       });
@@ -337,6 +341,9 @@ export default {
         mark: getRandomNumber(),
       });
     },
+    removeWord(index) {
+      this.data.word_list.splice(index, 1);
+    },
   },
 };
 </script>
@@ -363,7 +370,10 @@ export default {
       gap: 8px;
       margin-top: 8px;
 
-      li {
+      .word-item {
+        display: flex;
+        gap: 6px;
+        align-items: center;
         padding: 4px 6px;
         border: $border;
         border-radius: 4px;
@@ -371,6 +381,13 @@ export default {
         :deep p {
           margin: 0;
         }
+
+        .delete-word {
+          display: flex;
+          align-items: center;
+          padding: 0;
+          line-height: 1;
+        }
       }
     }
   }

+ 3 - 4
src/views/book/courseware/create/components/question/fill/FillSetting.vue

@@ -55,7 +55,7 @@
       </template>
       <el-form-item label="填空字体">
         <el-select v-model="property.fill_font" placeholder="请选择">
-          <el-option v-for="{ value, label } in fillFontList" :key="value" :label="label" :value="value" />
+          <el-option v-for="{ value, label } in fontList" :key="value" :label="label" :value="value" />
         </el-select>
       </el-form-item>
       <el-form-item label="横线长度">
@@ -109,13 +109,12 @@ import {
   arrangeTypeList,
   audioPositionList,
   audioGenerationMethodList,
-  fillFontList,
   switchOption,
   fillTypeList,
 } from '@/views/book/courseware/data/fill';
 
 import { GetTextToAudioConfParamList } from '@/api/app';
-import { speedRatioList } from '@/views/book/courseware/data/common';
+import { speedRatioList, fontList } from '@/views/book/courseware/data/common';
 
 export default {
   name: 'FillSetting',
@@ -126,7 +125,7 @@ export default {
       arrangeTypeList,
       audioPositionList,
       audioGenerationMethodList,
-      fillFontList,
+      fontList,
       switchOption,
       fillTypeList,
       voice_type_list: [],

+ 0 - 2
src/views/book/courseware/create/components/question/record_input/RecordInputSetting.vue

@@ -16,7 +16,6 @@ import {
   arrangeTypeList,
   audioPositionList,
   audioGenerationMethodList,
-  fillFontList,
   switchOption,
 } from '@/views/book/courseware/data/recordInput';
 
@@ -29,7 +28,6 @@ export default {
       arrangeTypeList,
       audioPositionList,
       audioGenerationMethodList,
-      fillFontList,
       switchOption,
     };
   },

+ 12 - 10
src/views/book/courseware/create/components/question/table/Table.vue

@@ -117,7 +117,7 @@
               :font-family="data?.unified_attrib?.font"
               :font-color="data?.unified_attrib?.text_color"
               :inline="true"
-              :itemIndex="i + '#' + j"
+              :item-index="i + '#' + j"
               toolbar="fontselect fontsizeselect forecolor backcolor | underline | bold italic strikethrough alignleft aligncenter alignright image media link"
               @handleRichTextBlur="handleBlurCon"
             />
@@ -156,13 +156,14 @@
                 v-if="data.option_list[i][j].cell.isCross"
                 style="display: flex; align-items: center; font-size: 14px"
                 >勾叉答案<i
-                  @click="toggle(li)"
                   :class="[
                     { 'el-icon-check': li.crossAnswer === statusList[1] },
                     { 'el-icon-close': li.crossAnswer === statusList[2] },
                   ]"
                   style="display: block; width: 14px; height: 14px; border: 1px solid #000"
-              /></span>
+                  @click="toggle(li)"
+                ></i
+              ></span>
             </div>
           </div>
         </div>
@@ -181,8 +182,9 @@
         :translations="data.multilingual"
         @SubmitTranslation="handleMultilingualTranslation"
       />
-      <el-divider v-if="isEnable(data.property.view_pinyin)" content-position="left"
-        >拼音效果<el-button
+      <el-divider v-if="isEnable(data.property.view_pinyin)" content-position="left">
+        拼音效果
+        <el-button
           v-show="isEnable(data.property.view_pinyin)"
           type="text"
           icon="el-icon-refresh"
@@ -194,8 +196,8 @@
         <template v-for="(item, index) in data.option_list">
           <PinyinText
             v-for="(items, indexs) in item"
-            :key="index + '_' + indexs"
             :id="'table_pinyin_text_' + index + '_' + indexs"
+            :key="index + '_' + indexs"
             ref="PinyinText"
             :rich-text-list="items.rich_text_list"
             :pinyin-position="data.property.pinyin_position"
@@ -207,11 +209,11 @@
         </template>
       </template>
       <el-dialog
+        v-if="editCellFlag"
         title="配置单元格"
         :visible="editCellFlag"
         width="460px"
         :close-on-click-modal="false"
-        v-if="editCellFlag"
         @close="editCellFlag = false"
       >
         <el-form :model="activeCell" :inline="true">
@@ -340,7 +342,7 @@ export default {
             this.data.property.is_first_sentence_first_hz_pinyin_first_char_upper_case;
           this.data.option_list.forEach((item, index) => {
             item.forEach((items, indexs) => {
-              this.createParsedTextInfoPinyin(items.content, index + '#' + indexs);
+              this.createParsedTextInfoPinyin(items.content, `${index}#${indexs}`);
             });
           });
         }
@@ -365,7 +367,7 @@ export default {
           this.data.paragraph_list_parameter.is_first_sentence_first_hz_pinyin_first_char_upper_case = val;
           this.data.option_list.forEach((item, index) => {
             item.forEach((items, indexs) => {
-              this.createParsedTextInfoPinyin(items.content, index + '#' + indexs);
+              this.createParsedTextInfoPinyin(items.content, `${index}#${indexs}`);
             });
           });
         }
@@ -468,7 +470,7 @@ export default {
         } else {
           this.data.option_list.forEach((item, index) => {
             item.forEach((items, indexs) => {
-              this.createParsedTextInfoPinyin(items.content, index + '#' + indexs);
+              this.createParsedTextInfoPinyin(items.content, `${index}#${indexs}`);
             });
           });
         }

+ 3 - 9
src/views/book/courseware/data/fill.js

@@ -6,6 +6,7 @@ import {
   switchOption,
   pinyinPositionList,
   serialNumberStyleList,
+  fontList,
 } from '@/views/book/courseware/data/common';
 
 export { arrangeTypeList, switchOption };
@@ -40,13 +41,6 @@ export const fillTypeList = [
   { value: 'voice', label: '语音' },
 ];
 
-// 填空字体
-export const fillFontList = [
-  { value: 'chinese', label: '中文', font: 'arial' },
-  { value: 'english', label: '英文', font: 'arial' },
-  { value: 'pinyin', label: '拼音', font: 'PINYIN-B' },
-];
-
 export function getFillProperty() {
   return {
     serial_number: 1,
@@ -59,7 +53,7 @@ export function getFillProperty() {
     arrange_type: arrangeTypeList[1].value,
     audio_position: audioPositionList[0].value,
     audio_generation_method: audioGenerationMethodList[0].value,
-    fill_font: fillFontList[0].value,
+    fill_font: fontList[0].value,
     is_enable_voice_answer: switchOption[1].value,
     view_pinyin: 'false', // 显示拼音
     pinyin_position: pinyinPositionList[0].value,
@@ -67,7 +61,7 @@ export function getFillProperty() {
     voice_type: '', // 音色
     emotion: '', // 风格,情感
     speed_ratio: '', // 语速
-    input_default_width: 80, // 输入框默认宽度
+    input_default_width: 160, // 输入框默认宽度
   };
 }
 

+ 125 - 38
src/views/book/courseware/preview/components/fill/FillPreview.vue

@@ -25,16 +25,25 @@
               <span v-else :key="j" v-html="convertText(sanitizeHTML(li.content))"></span>
             </template>
             <template v-if="li.type === 'input'">
+              <!-- 输入填空 -->
               <template v-if="data.property.fill_type === fillTypeList[0].value">
                 <el-input
                   :key="j"
-                  v-model="li.content"
+                  :ref="`input-${li.mark}`"
+                  v-model="li.input"
                   :disabled="disabled"
-                  :class="[data.property.fill_font, ...computedAnswerClass(li.mark)]"
-                  :style="[{ width: Math.max(data.property.input_default_width, li.content.length * 21.3) + 'px' }]"
+                  :class="[...computedAnswerClass(li.mark)]"
+                  :style="[
+                    {
+                      fontFamily: data.property.fill_font,
+                      width: (inputWidthMap[li.mark] || data.property.input_default_width) + 'px',
+                    },
+                  ]"
+                  @input="handleInput(li.input, li.mark)"
                 />
               </template>
 
+              <!-- 选词填空 -->
               <template v-else-if="data.property.fill_type === fillTypeList[1].value">
                 <el-popover :key="j" placement="top" trigger="click">
                   <div class="word-list">
@@ -48,17 +57,16 @@
                     </span>
                   </div>
 
-                  <el-input
+                  <span
                     slot="reference"
-                    v-model="li.content"
-                    :readonly="true"
-                    :class="[data.property.fill_font, ...computedAnswerClass(li.mark)]"
-                    class="pinyin"
-                    :style="[{ width: Math.max(data.property.input_default_width, li.content.length * 21.3) + 'px' }]"
-                  />
+                    class="select-content"
+                    :style="[{ minWidth: data.property.input_default_width + 'px' }]"
+                    v-html="sanitizeHTML(li.input)"
+                  ></span>
                 </el-popover>
               </template>
 
+              <!-- 手写填空 -->
               <template v-else-if="data.property.fill_type === fillTypeList[2].value">
                 <span :key="j" class="write-click" @click="handleWriteClick(li.mark)">
                   <img
@@ -70,6 +78,7 @@
                 </span>
               </template>
 
+              <!-- 语音填空 -->
               <template v-else-if="data.property.fill_type === fillTypeList[3].value">
                 <SoundRecordBox
                   ref="record"
@@ -145,14 +154,7 @@ import SoundRecord from '../../common/SoundRecord.vue';
 import SoundRecordBox from '@/views/book/courseware/preview/components/record_input/SoundRecord.vue';
 import WriteDialog from './components/WriteDialog.vue';
 
-import {
-  getFillData,
-  fillFontList,
-  fillTypeList,
-  arrangeTypeList,
-  audioPositionList,
-} from '@/views/book/courseware/data/fill';
-import { getPlainText } from '@/utils/common';
+import { getFillData, fillTypeList, arrangeTypeList, audioPositionList } from '@/views/book/courseware/data/fill';
 
 export default {
   name: 'FillPreview',
@@ -168,16 +170,13 @@ export default {
       data: getFillData(),
       fillTypeList,
       modelEssay: [],
+      inputWidthMap: {},
       selectedWordList: [], // 用于存储选中的词汇
       writeVisible: false,
       writeMark: '',
     };
   },
   computed: {
-    fontFamily() {
-      const fontItem = fillFontList.find(({ value }) => this.data.property.fill_font === value);
-      return fontItem ? fontItem.font : '';
-    },
     isShowAnswer() {
       return (
         (Array.isArray(this.data.answer_list) && this.data.answer_list.length > 0) ||
@@ -218,10 +217,10 @@ export default {
         this.answer.answer_list = list
           .map((item) => {
             return item
-              .map(({ type, content, audio_answer_list, mark }) => {
+              .map(({ type, input, audio_answer_list, mark }) => {
                 if (type === 'input') {
                   return {
-                    value: content,
+                    value: input,
                     mark,
                     audio_answer_list,
                     write_base64: '',
@@ -242,7 +241,7 @@ export default {
         this.modelEssay.forEach((item) => {
           item.forEach((li) => {
             if (li.mark === mark) {
-              li.content = value;
+              li.input = value;
             }
           });
         });
@@ -282,7 +281,7 @@ export default {
     handleSelectWord(content, mark, li) {
       if (!content || !mark || !li) return;
 
-      li.content = getPlainText(content);
+      li.input = content;
       this.selectedWordList.push(mark);
     },
     /**
@@ -308,32 +307,63 @@ export default {
       const isFront = this.data.property.audio_position === audioPositionList[0].value;
       const isEnableVoice = this.data.property.is_enable_voice_answer === 'true';
       const isHasAudio = this.data.audio_file_id.length > 0;
-      let _list = [
+      let areaList = [
         { name: 'audio', value: '24px' },
         { name: 'fill', value: '1fr' },
       ];
 
       if (!isHasAudio) {
-        _list[0].value = '0px';
+        areaList.shift();
       }
 
-      if (!isFront) {
-        _list = _list.reverse();
+      if (!isFront && isHasAudio) {
+        areaList = areaList.reverse();
       }
+
       let gridArea = '';
       let gridTemplateRows = '';
       let gridTemplateColumns = '';
+
       if (isRow) {
-        gridArea = `"${_list[0].name} ${_list[1].name}${isEnableVoice ? ' record' : ''}" ${this.showLang ? ' "lang lang lang"' : ''}`;
-        gridTemplateRows = `auto ${this.showLang ? 'auto' : ''}`;
-        gridTemplateColumns = `${_list[0].value} ${_list[1].value}${isEnableVoice ? ' 160px' : ''}`;
+        const rowAreas = areaList.map(({ name }) => name);
+        const rowCols = areaList.map(({ value }) => value);
+        const templateRows = ['auto'];
+
+        if (isEnableVoice) {
+          rowAreas.push('record');
+          rowCols.push('160px');
+        }
+
+        const areaRows = [`"${rowAreas.join(' ')}"`];
+
+        if (this.showLang) {
+          areaRows.push(`"${Array(rowAreas.length).fill('lang').join(' ')}"`);
+          templateRows.push('auto');
+        }
+
+        gridArea = areaRows.join(' ');
+        gridTemplateRows = templateRows.join(' ');
+        gridTemplateColumns = rowCols.join(' ');
       } else {
-        gridArea = `"${_list[0].name}" "${_list[1].name}" ${isEnableVoice ? `" record" ` : ''} ${this.showLang ? ' "lang"' : ''}`;
-        gridTemplateRows = `${_list[0].value} ${_list[1].value} ${isEnableVoice ? ' 32px' : ''} ${this.showLang ? 'auto' : ''}`;
+        const areaRows = areaList.map(({ name }) => `"${name}"`);
+        const templateRows = areaList.map(({ value }) => value);
+
+        if (isEnableVoice) {
+          areaRows.push('"record"');
+          templateRows.push('32px');
+        }
+
+        if (this.showLang) {
+          areaRows.push('"lang"');
+          templateRows.push('auto');
+        }
+
+        gridArea = areaRows.join(' ');
+        gridTemplateRows = templateRows.join(' ');
         gridTemplateColumns = '1fr';
       }
 
-      let style = {
+      return {
         'grid-auto-flow': isRow ? 'column' : 'row',
         'column-gap': isRow && isHasAudio ? '16px' : undefined,
         'row-gap': isRow || !isHasAudio ? undefined : '8px',
@@ -341,7 +371,6 @@ export default {
         'grid-template-rows': gridTemplateRows,
         'grid-template-columns': gridTemplateColumns,
       };
-      return style;
     },
     /**
      * 计算答题对错选项字体颜色
@@ -383,7 +412,7 @@ export default {
       this.modelEssay.forEach((item) => {
         item.forEach((li) => {
           if (li.type === 'input') {
-            li.content = '';
+            li.input = '';
             li.write_base64 = '';
           }
         });
@@ -416,6 +445,50 @@ export default {
 
       return noTextContentData;
     },
+    /**
+     * 处理输入框内容变化
+     * @description 根据输入框值计算所需长度,动态调整输入框宽度,输入框宽度不小于默认宽度,且不超过组件最大宽度 1000px,需要考虑输入框前面的宽度,保证输入框不会超出组件范围
+     * @param {String} value 输入框当前值
+     * @param {String} mark 选项标识
+     */
+    handleInput(value, mark) {
+      if (!mark) return;
+
+      const text = `${value || ''}`;
+      const defaultWidth = Number(this.data?.property?.input_default_width) || 120;
+      const fontSize = 16;
+      const fontFamily = this.data?.property?.fill_font || 'arial';
+
+      const canvas = document.createElement('canvas');
+      const context = canvas.getContext('2d');
+      let textWidth = 0;
+
+      if (context) {
+        context.font = `${fontSize}pt ${fontFamily}`;
+        textWidth = context.measureText(text || ' ').width;
+      }
+
+      // 额外留出输入框内边距和边框余量,避免刚好卡边。
+      const contentWidth = Math.ceil(textWidth + 24);
+
+      const inputRef = this.$refs[`input-${mark}`];
+      const inputVm = Array.isArray(inputRef) ? inputRef[0] : inputRef;
+      const inputEl = inputVm?.$el;
+      const containerEl = this.$el;
+
+      let availableWidth = 1000;
+      if (inputEl && containerEl) {
+        const inputRect = inputEl.getBoundingClientRect();
+        const containerRect = containerEl.getBoundingClientRect();
+        const rightLimit = containerRect.left + Math.min(containerRect.width, 1000);
+        availableWidth = Math.floor(rightLimit - inputRect.left - 12);
+      }
+
+      const maxWidth = Math.max(40, availableWidth);
+      const nextWidth = Math.min(Math.max(contentWidth, defaultWidth), maxWidth);
+
+      this.$set(this.inputWidthMap, mark, nextWidth);
+    },
   },
 };
 </script>
@@ -428,6 +501,7 @@ export default {
 
   .main {
     display: grid;
+    gap: 10px;
     align-items: center;
   }
 
@@ -465,6 +539,19 @@ export default {
       }
     }
 
+    .select-content {
+      display: inline-block;
+      height: 32px;
+      margin: 0 10px;
+      vertical-align: bottom;
+      cursor: pointer;
+      border-bottom: 1px solid $font-color;
+
+      :deep p {
+        margin: 0;
+      }
+    }
+
     .el-input {
       display: inline-flex;
       align-items: center;

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

@@ -202,9 +202,10 @@ export default {
       if (!this.isJudgingRightWrong) return '';
       let selectOption = this.answer.answer_list.find((item) => item.mark === mark); // 查找是否已选中的选项
       if (!selectOption) return '';
-      return this.data.answer.answer_list.find((item) => item.mark === mark)?.option_type === selectOption.option_type
-        ? 'right'
-        : 'wrong';
+      // 正确选项的类型
+      const correctOption = this.data.answer.answer_list.find((item) => item.mark === mark)?.option_type;
+      if (!correctOption) return '';
+      return correctOption === selectOption.option_type ? 'right' : 'wrong';
     },
     // 计算是否显示正确答案的样式
     computedIsShowRightAnswer(mark, option_type) {

+ 7 - 2
src/views/personal_workbench/project/components/BookUnifiedAttr.vue

@@ -12,7 +12,7 @@
         <el-form ref="form" :model="unified_attrib" label-width="80px" size="small">
           <el-form-item label="主题色">
             <div class="color-group">
-              <el-color-picker v-model="unified_attrib.topic_color" />
+              <ColorPicker v-model="unified_attrib.topic_color" :type="2" />
               <span>辅助色</span>
               <el-color-picker v-model="unified_attrib.assist_color" />
               <span class="link" @click="generateAssistColor">自动生成辅助色</span>
@@ -45,7 +45,7 @@
             <el-input-number v-model="unified_attrib.line_height" :min="0" :max="20" :step="0.1" />
           </el-form-item>
           <el-form-item label="文字颜色">
-            <el-color-picker v-model="unified_attrib.text_color" />
+            <ColorPicker v-model="unified_attrib.text_color" :type="1" />
           </el-form-item>
           <el-form-item label="对齐方式">
             <el-select v-model="unified_attrib.align" placeholder="请选择对齐方式">
@@ -76,6 +76,8 @@
 </template>
 
 <script>
+import ColorPicker from '@/components/ColorPicker.vue';
+
 import { pinyinPositionList, isEnable } from '@/views/book/courseware/data/common';
 import { GetBookUnifiedAttrib, ApplyBookUnifiedAttrib, SaveBookUnifiedAttrib } from '@/api/book';
 import { ToAuxiliaryColor } from '@/api/app';
@@ -83,6 +85,9 @@ import { unified_attrib } from '@/common/data';
 
 export default {
   name: 'BookUnifiedAttrPage',
+  components: {
+    ColorPicker,
+  },
   props: {
     visible: {
       type: Boolean,