|
|
@@ -11,88 +11,101 @@
|
|
|
/>
|
|
|
<div class="fill-wrapper">
|
|
|
<div class="fill-content">
|
|
|
- <template v-for="(li, i) in modelEssay">
|
|
|
- <template v-if="li.type === 'text'">
|
|
|
- <PinyinText
|
|
|
- :key="`text-${i}`"
|
|
|
- class="content"
|
|
|
- :paragraph-list="li.paragraph_list"
|
|
|
- :rich-text-list="li.rich_text_list"
|
|
|
- :pinyin-position="data.property.pinyin_position"
|
|
|
- :body-styles="pinyinBodyStyles"
|
|
|
- :is-show-pinyin="isEnable(data.property.view_pinyin)"
|
|
|
- :is-preview="true"
|
|
|
- />
|
|
|
- </template>
|
|
|
- <template v-if="li.type === 'input'">
|
|
|
- <!-- 输入填空 -->
|
|
|
- <template v-if="data.property.fill_type === fillTypeList[0].value">
|
|
|
- <el-input
|
|
|
- :key="`input-${i}`"
|
|
|
- :ref="`input-${li.mark}`"
|
|
|
- v-model="li.input"
|
|
|
- :disabled="disabled"
|
|
|
- :class="[...computedAnswerClass(li.mark)]"
|
|
|
- :style="[
|
|
|
- {
|
|
|
- '--fill-font-family': 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="`popover-${i}`" placement="top" trigger="click">
|
|
|
- <div class="word-list">
|
|
|
+ <template v-for="(group, gi) in groupedModelEssay">
|
|
|
+ <span :key="`group-${gi}`" :style="groupStyle(group)">
|
|
|
+ <template v-for="(li, i) in group.items">
|
|
|
+ <template v-if="li.type === 'text'">
|
|
|
+ <PinyinText
|
|
|
+ :key="`text-${group.indexes[i]}`"
|
|
|
+ class="content"
|
|
|
+ :paragraph-list="li.paragraph_list"
|
|
|
+ :rich-text-list="li.rich_text_list"
|
|
|
+ :pinyin-position="data.property.pinyin_position"
|
|
|
+ :body-styles="pinyinBodyStyles"
|
|
|
+ :is-show-pinyin="isEnable(data.property.view_pinyin)"
|
|
|
+ :is-preview="true"
|
|
|
+ />
|
|
|
+ </template>
|
|
|
+ <template v-if="li.type === 'input'">
|
|
|
+ <!-- 输入填空 -->
|
|
|
+ <template v-if="data.property.fill_type === fillTypeList[0].value">
|
|
|
+ <el-input
|
|
|
+ :key="`input-${group.indexes[i]}`"
|
|
|
+ :ref="`input-${li.mark}`"
|
|
|
+ v-model="li.input"
|
|
|
+ :disabled="disabled"
|
|
|
+ :class="[...computedAnswerClass(li.mark)]"
|
|
|
+ :style="[
|
|
|
+ {
|
|
|
+ '--fill-font-family': data.property.fill_font,
|
|
|
+ width: (inputWidthMap[li.mark] || data.property.input_default_width) + 'px',
|
|
|
+ marginBottom: resolveFillControlMargin(li),
|
|
|
+ },
|
|
|
+ ]"
|
|
|
+ @input="handleInput(li.input, li.mark)"
|
|
|
+ />
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <!-- 选词填空 -->
|
|
|
+ <template v-else-if="data.property.fill_type === fillTypeList[1].value">
|
|
|
+ <el-popover :key="`popover-${group.indexes[i]}`" placement="top" trigger="click">
|
|
|
+ <div class="word-list">
|
|
|
+ <span
|
|
|
+ v-for="{ content, mark } in data.word_list"
|
|
|
+ :key="mark"
|
|
|
+ class="word-item"
|
|
|
+ @click="handleSelectWord(content, mark, li)"
|
|
|
+ v-html="sanitizeHTML(content)"
|
|
|
+ >
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <span
|
|
|
+ slot="reference"
|
|
|
+ class="select-content"
|
|
|
+ :style="[
|
|
|
+ { minWidth: data.property.input_default_width + 'px' },
|
|
|
+ { marginBottom: resolveFillControlMargin(li) },
|
|
|
+ ]"
|
|
|
+ v-html="sanitizeHTML(li.input)"
|
|
|
+ ></span>
|
|
|
+ </el-popover>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <!-- 手写填空 -->
|
|
|
+ <template v-else-if="data.property.fill_type === fillTypeList[2].value">
|
|
|
<span
|
|
|
- v-for="{ content, mark } in data.word_list"
|
|
|
- :key="mark"
|
|
|
- class="word-item"
|
|
|
- @click="handleSelectWord(content, mark, li)"
|
|
|
- v-html="sanitizeHTML(content)"
|
|
|
+ :key="`write-${group.indexes[i]}`"
|
|
|
+ class="write-click"
|
|
|
+ :style="{ marginBottom: resolveFillControlMargin(li) }"
|
|
|
+ @click="handleWriteClick(li.mark)"
|
|
|
>
|
|
|
+ <img
|
|
|
+ v-show="li.write_base64"
|
|
|
+ style="background-color: #f4f4f4"
|
|
|
+ :src="li.write_base64"
|
|
|
+ alt="write-show"
|
|
|
+ />
|
|
|
</span>
|
|
|
- </div>
|
|
|
-
|
|
|
- <span
|
|
|
- slot="reference"
|
|
|
- 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[3].value">
|
|
|
+ <SoundRecordBox
|
|
|
+ ref="record"
|
|
|
+ :key="`record-${group.indexes[i]}`"
|
|
|
+ type="mini"
|
|
|
+ :many-times="false"
|
|
|
+ class="record-box fill"
|
|
|
+ :attrib="data.unified_attrib"
|
|
|
+ :answer-record-list="data.audio_answer_list"
|
|
|
+ :task-model="isJudgingRightWrong ? 'ANSWER' : ''"
|
|
|
+ @handleWav="handleMiniWav($event, li.mark)"
|
|
|
+ />
|
|
|
+ </template>
|
|
|
+ </template>
|
|
|
</template>
|
|
|
-
|
|
|
- <!-- 手写填空 -->
|
|
|
- <template v-else-if="data.property.fill_type === fillTypeList[2].value">
|
|
|
- <span :key="`write-${i}`" class="write-click" @click="handleWriteClick(li.mark)">
|
|
|
- <img
|
|
|
- v-show="li.write_base64"
|
|
|
- style="background-color: #f4f4f4"
|
|
|
- :src="li.write_base64"
|
|
|
- alt="write-show"
|
|
|
- />
|
|
|
- </span>
|
|
|
- </template>
|
|
|
-
|
|
|
- <!-- 语音填空 -->
|
|
|
- <template v-else-if="data.property.fill_type === fillTypeList[3].value">
|
|
|
- <SoundRecordBox
|
|
|
- ref="record"
|
|
|
- :key="`record-${i}`"
|
|
|
- type="mini"
|
|
|
- :many-times="false"
|
|
|
- class="record-box fill"
|
|
|
- :attrib="data.unified_attrib"
|
|
|
- :answer-record-list="data.audio_answer_list"
|
|
|
- :task-model="isJudgingRightWrong ? 'ANSWER' : ''"
|
|
|
- @handleWav="handleMiniWav($event, li.mark)"
|
|
|
- />
|
|
|
- </template>
|
|
|
- </template>
|
|
|
+ </span>
|
|
|
</template>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -133,24 +146,33 @@
|
|
|
>
|
|
|
<div slot="right-answer" class="fill-wrapper">
|
|
|
<div class="fill-content">
|
|
|
- <template v-for="(li, i) in modelEssay">
|
|
|
- <template v-if="li.type === 'text'">
|
|
|
- <PinyinText
|
|
|
- :key="`answer-text-${i}`"
|
|
|
- :is-show-pinyin="isEnable(data.property.view_pinyin)"
|
|
|
- class="content"
|
|
|
- :paragraph-list="li.paragraph_list"
|
|
|
- :rich-text-list="li.rich_text_list"
|
|
|
- :body-styles="pinyinBodyStyles"
|
|
|
- :pinyin-position="data.property.pinyin_position"
|
|
|
- :is-preview="true"
|
|
|
- />
|
|
|
- </template>
|
|
|
- <template v-if="li.type === 'input'">
|
|
|
- <span v-show="computedAnswerText(li.mark).length > 0" :key="`answer-${i}`" class="right-answer">
|
|
|
- {{ computedAnswerText(li.mark) }}
|
|
|
- </span>
|
|
|
- </template>
|
|
|
+ <template v-for="(group, gi) in groupedModelEssay">
|
|
|
+ <span :key="`answer-group-${gi}`" :style="groupStyle(group)">
|
|
|
+ <template v-for="(li, i) in group.items">
|
|
|
+ <template v-if="li.type === 'text'">
|
|
|
+ <PinyinText
|
|
|
+ :key="`answer-text-${group.indexes[i]}`"
|
|
|
+ :is-show-pinyin="isEnable(data.property.view_pinyin)"
|
|
|
+ class="content"
|
|
|
+ :paragraph-list="li.paragraph_list"
|
|
|
+ :rich-text-list="li.rich_text_list"
|
|
|
+ :body-styles="pinyinBodyStyles"
|
|
|
+ :pinyin-position="data.property.pinyin_position"
|
|
|
+ :is-preview="true"
|
|
|
+ />
|
|
|
+ </template>
|
|
|
+ <template v-if="li.type === 'input'">
|
|
|
+ <span
|
|
|
+ v-show="computedAnswerText(li.mark).length > 0"
|
|
|
+ :key="`answer-${group.indexes[i]}`"
|
|
|
+ class="right-answer"
|
|
|
+ :style="{ marginBottom: resolveFillControlMargin(li) }"
|
|
|
+ >
|
|
|
+ {{ computedAnswerText(li.mark) }}
|
|
|
+ </span>
|
|
|
+ </template>
|
|
|
+ </template>
|
|
|
+ </span>
|
|
|
</template>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -214,6 +236,25 @@ export default {
|
|
|
isShowAnswerFill() {
|
|
|
return this.hasFixedAnswer || (this.data.answer_list || []).length > 0;
|
|
|
},
|
|
|
+ /**
|
|
|
+ * 按段落对齐方式对 modelEssay 分组,便于整体应用 text-align,
|
|
|
+ * 让同一段内的文字块与填空连续排列(可换行),而不是每块独占一行。
|
|
|
+ * @returns {Array} [{ align, items, indexes }]
|
|
|
+ */
|
|
|
+ groupedModelEssay() {
|
|
|
+ const groups = [];
|
|
|
+ (this.modelEssay || []).forEach((item, index) => {
|
|
|
+ const align = item.align || '';
|
|
|
+ const last = groups[groups.length - 1];
|
|
|
+ if (last && last.align === align) {
|
|
|
+ last.items.push(item);
|
|
|
+ last.indexes.push(index);
|
|
|
+ } else {
|
|
|
+ groups.push({ align, items: [item], indexes: [index] });
|
|
|
+ }
|
|
|
+ });
|
|
|
+ return groups;
|
|
|
+ },
|
|
|
},
|
|
|
watch: {
|
|
|
'data.model_essay': {
|
|
|
@@ -258,6 +299,47 @@ export default {
|
|
|
},
|
|
|
},
|
|
|
methods: {
|
|
|
+ /**
|
|
|
+ * 分组容器样式:有对齐时应用块级与对齐,无对齐时保持行内流式。
|
|
|
+ * @param {Object} group 分组对象
|
|
|
+ * @returns {Object} 样式对象
|
|
|
+ */
|
|
|
+ groupStyle(group) {
|
|
|
+ if (!group || !group.align) return {};
|
|
|
+ return {
|
|
|
+ display: 'block',
|
|
|
+ textAlign: group.align,
|
|
|
+ };
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * 计算填空控件需要补偿的 margin-bottom。
|
|
|
+ * 词级 margin-bottom 会撑高行盒,控件 vertical-align: bottom 会贴到被撑高的行盒底部,
|
|
|
+ * 因此给控件加上与段落一致的 margin-bottom,使其边框底部回到文本底部;
|
|
|
+ * 无 margin 时返回空字符串,保持原有显示不变。
|
|
|
+ * @param {Object} li 填空块对象
|
|
|
+ * @returns {String} 补偿值,如 '28pt',无则空字符串
|
|
|
+ */
|
|
|
+ resolveFillControlMargin(li) {
|
|
|
+ const tagItem = (li?.rich_text_list || []).find((item) =>
|
|
|
+ /^<(p|div|h[1-6]|li|blockquote)\b[^>]*>/i.test(String(item?.text || '')),
|
|
|
+ );
|
|
|
+ const raw = String(tagItem?.text || '').match(/margin-bottom\s*:\s*([^;"']+)/i);
|
|
|
+ if (!raw) return '';
|
|
|
+ const match = raw[1].trim().match(/^([\d.]+)\s*(px|pt|em)?$/i);
|
|
|
+ if (!match) return '';
|
|
|
+ const amount = parseFloat(match[1]);
|
|
|
+ const unit = (match[2] || 'px').toLowerCase();
|
|
|
+ // px/pt 为绝对单位,可直接使用;em 相对词级字号,换算为绝对 pt 后再补偿
|
|
|
+ if (unit !== 'em') return raw[1].trim();
|
|
|
+ const fontSize = String(this.pinyinBodyStyles?.fontSize || '16pt');
|
|
|
+ const fsMatch = fontSize.match(/^([\d.]+)\s*(px|pt|em)?$/i);
|
|
|
+ if (!fsMatch) return raw[1].trim();
|
|
|
+ const fsAmount = parseFloat(fsMatch[1]);
|
|
|
+ const fsUnit = (fsMatch[2] || 'pt').toLowerCase();
|
|
|
+ if (fsUnit === 'em') return `${amount * fsAmount}em`;
|
|
|
+ if (fsUnit === 'pt') return `${amount * fsAmount}pt`;
|
|
|
+ return `${amount * fsAmount}px`;
|
|
|
+ },
|
|
|
handleWav(data) {
|
|
|
this.data.record_list = data;
|
|
|
},
|