FillPreview.vue 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779
  1. <!-- eslint-disable vue/no-v-html -->
  2. <template>
  3. <div class="select-preview" :style="[getAreaStyle(), getComponentStyle()]">
  4. <SerialNumberPosition v-if="isEnable(data.property.sn_display_mode)" :property="data.property" />
  5. <div class="main" :style="getMainStyle()">
  6. <AudioFill
  7. v-if="data.audio_file_id.length > 0"
  8. :color="data.unified_attrib?.topic_color"
  9. :file-id="data.audio_file_id"
  10. />
  11. <div class="fill-wrapper">
  12. <div class="fill-content">
  13. <template v-for="(group, gi) in groupedModelEssay">
  14. <span :key="`group-${gi}`" :style="groupStyle(group)">
  15. <template v-for="(li, i) in group.items">
  16. <template v-if="li.type === 'text'">
  17. <PinyinText
  18. :key="`text-${group.indexes[i]}`"
  19. class="content"
  20. :paragraph-list="li.paragraph_list"
  21. :rich-text-list="li.rich_text_list"
  22. :pinyin-position="data.property.pinyin_position"
  23. :body-styles="pinyinBodyStyles"
  24. :is-show-pinyin="isEnable(data.property.view_pinyin)"
  25. :is-preview="true"
  26. />
  27. </template>
  28. <template v-if="li.type === 'input'">
  29. <!-- 输入填空 -->
  30. <template v-if="data.property.fill_type === fillTypeList[0].value">
  31. <el-input
  32. :key="`input-${group.indexes[i]}`"
  33. :ref="`input-${li.mark}`"
  34. v-model="li.input"
  35. :disabled="disabled"
  36. :class="[...computedAnswerClass(li.mark)]"
  37. :style="[
  38. {
  39. '--fill-font-family': data.property.fill_font,
  40. width: (inputWidthMap[li.mark] || data.property.input_default_width) + 'px',
  41. marginBottom: resolveFillControlMargin(li),
  42. ...getFillControlAlignmentStyle(li),
  43. },
  44. ]"
  45. @input="handleInput(li.input, li.mark)"
  46. />
  47. </template>
  48. <!-- 选词填空 -->
  49. <template v-else-if="data.property.fill_type === fillTypeList[1].value">
  50. <el-popover :key="`popover-${group.indexes[i]}`" placement="top" trigger="click">
  51. <div class="word-list">
  52. <span
  53. v-for="{ content, mark } in data.word_list"
  54. :key="mark"
  55. class="word-item"
  56. @click="handleSelectWord(content, mark, li)"
  57. v-html="sanitizeHTML(content)"
  58. >
  59. </span>
  60. </div>
  61. <span
  62. slot="reference"
  63. class="select-content"
  64. :style="[
  65. { minWidth: data.property.input_default_width + 'px' },
  66. { marginBottom: resolveFillControlMargin(li) },
  67. ]"
  68. v-html="sanitizeHTML(li.input)"
  69. ></span>
  70. </el-popover>
  71. </template>
  72. <!-- 手写填空 -->
  73. <template v-else-if="data.property.fill_type === fillTypeList[2].value">
  74. <span
  75. :key="`write-${group.indexes[i]}`"
  76. class="write-click"
  77. :style="{ marginBottom: resolveFillControlMargin(li) }"
  78. @click="handleWriteClick(li.mark)"
  79. >
  80. <img
  81. v-show="li.write_base64"
  82. style="background-color: #f4f4f4"
  83. :src="li.write_base64"
  84. alt="write-show"
  85. />
  86. </span>
  87. </template>
  88. <!-- 语音填空 -->
  89. <template v-else-if="data.property.fill_type === fillTypeList[3].value">
  90. <SoundRecordBox
  91. ref="record"
  92. :key="`record-${group.indexes[i]}`"
  93. type="mini"
  94. :many-times="false"
  95. class="record-box fill"
  96. :attrib="data.unified_attrib"
  97. :answer-record-list="data.audio_answer_list"
  98. :task-model="isJudgingRightWrong ? 'ANSWER' : ''"
  99. @handleWav="handleMiniWav($event, li.mark)"
  100. />
  101. </template>
  102. </template>
  103. </template>
  104. </span>
  105. </template>
  106. </div>
  107. </div>
  108. <SoundRecord
  109. v-if="isEnable(data.property.is_enable_voice_answer)"
  110. ref="record"
  111. type="pro"
  112. :class="['record-box', 'sound-record-wrapper', isHorizontal ? 'horizontal' : 'vertical']"
  113. :attrib="data.unified_attrib"
  114. :answer-record-list="data.record_list"
  115. :task-model="isJudgingRightWrong ? 'ANSWER' : ''"
  116. @handleWav="handleWav"
  117. />
  118. <div v-if="showLang" class="lang">
  119. {{ data.multilingual.find((item) => item.type === getLang())?.translation }}
  120. </div>
  121. <PreviewOperation
  122. :is-show-retry="isHasFill"
  123. :is-show-answer="isHasFill && isShowAnswerFill"
  124. :is-show-judge-correct="isShowJudgeCorrect"
  125. @showAnswerAnalysis="showAnswerAnalysis"
  126. @judgeCorrect="judgeCorrect"
  127. @retry="retry"
  128. />
  129. </div>
  130. <WriteDialog :visible.sync="writeVisible" @confirm="handleWriteConfirm" />
  131. <AnswerCorrect
  132. :visible.sync="visibleAnswerCorrect"
  133. :is-check-correct="isCheckCorrect"
  134. @closeAnswerCorrect="closeAnswerCorrect"
  135. />
  136. <AnswerAnalysis
  137. :visible.sync="visibleAnswerAnalysis"
  138. :answer-list="data.answer_list"
  139. :analysis-list="data.analysis_list"
  140. @closeAnswerAnalysis="closeAnswerAnalysis"
  141. >
  142. <div slot="right-answer" class="fill-wrapper" :style="{ '--fill-font-family': data.property.fill_font }">
  143. <div class="fill-content">
  144. <template v-for="(group, gi) in groupedModelEssay">
  145. <span :key="`answer-group-${gi}`" :style="groupStyle(group)">
  146. <template v-for="(li, i) in group.items">
  147. <template v-if="li.type === 'text'">
  148. <PinyinText
  149. :key="`answer-text-${group.indexes[i]}`"
  150. :is-show-pinyin="isEnable(data.property.view_pinyin)"
  151. class="content"
  152. :paragraph-list="li.paragraph_list"
  153. :rich-text-list="li.rich_text_list"
  154. :body-styles="pinyinBodyStyles"
  155. :pinyin-position="data.property.pinyin_position"
  156. :is-preview="true"
  157. />
  158. </template>
  159. <template v-if="li.type === 'input'">
  160. <span
  161. v-show="computedAnswerText(li.mark).length > 0"
  162. :key="`answer-${group.indexes[i]}`"
  163. class="right-answer"
  164. :style="{ marginBottom: resolveFillControlMargin(li) }"
  165. >
  166. {{ computedAnswerText(li.mark) }}
  167. </span>
  168. </template>
  169. </template>
  170. </span>
  171. </template>
  172. </div>
  173. </div>
  174. </AnswerAnalysis>
  175. </div>
  176. </template>
  177. <script>
  178. import PreviewMixin from '../common/PreviewMixin';
  179. import AudioFill from './components/AudioFillPlay.vue';
  180. import SoundRecord from '../../common/SoundRecord.vue';
  181. import SoundRecordBox from '@/views/book/courseware/preview/components/record_input/SoundRecord.vue';
  182. import WriteDialog from './components/WriteDialog.vue';
  183. import {
  184. getFillData,
  185. fillTypeList,
  186. arrangeTypeList,
  187. audioPositionList,
  188. groupModelEssay,
  189. } from '@/views/book/courseware/data/fill';
  190. export default {
  191. name: 'FillPreview',
  192. components: {
  193. AudioFill,
  194. SoundRecord,
  195. SoundRecordBox,
  196. WriteDialog,
  197. },
  198. mixins: [PreviewMixin],
  199. data() {
  200. return {
  201. data: getFillData(),
  202. fillTypeList,
  203. modelEssay: [],
  204. inputWidthMap: {},
  205. selectedWordList: [], // 用于存储选中的词汇
  206. writeVisible: false,
  207. writeMark: '',
  208. };
  209. },
  210. computed: {
  211. pinyinBodyStyles() {
  212. const unifiedAttrib = this.data?.unified_attrib || {};
  213. return {
  214. fontFamily: unifiedAttrib.font || undefined,
  215. fontSize: unifiedAttrib.font_size || undefined,
  216. color: unifiedAttrib.text_color || undefined,
  217. };
  218. },
  219. isHasFill() {
  220. return this.data.model_essay.some((li) => li.type === 'input');
  221. },
  222. isHorizontal() {
  223. return this.data.property.arrange_type === arrangeTypeList[0].value;
  224. },
  225. // 是否有固定答案(data.answer.answer_list 中存在任意一项 value 非空)
  226. hasFixedAnswer() {
  227. return (this.data.answer?.answer_list || []).some((item) => item?.value);
  228. },
  229. // 是否显示判断对错按钮:有固定答案时才显示
  230. isShowJudgeCorrect() {
  231. return this.hasFixedAnswer;
  232. },
  233. // 是否显示查看答案按钮:有固定答案或参考答案(answer_list)有数据时显示
  234. isShowAnswerFill() {
  235. return this.hasFixedAnswer || (this.data.answer_list || []).length > 0;
  236. },
  237. /**
  238. * 按「段落 + 对齐方式」对 modelEssay 分组,便于整体应用 text-align,
  239. * 让同一段内的文字块与填空连续排列(可换行),而不是每块独占一行;
  240. * 段落之间必须断开,否则相邻的同对齐方式段落会共用同一个块级容器(同一个文本流),
  241. * 被排成一行(例如连续的多个居中段落)。
  242. * @returns {Array} [{ align, items, indexes }]
  243. */
  244. groupedModelEssay() {
  245. return groupModelEssay(this.modelEssay);
  246. },
  247. },
  248. watch: {
  249. 'data.model_essay': {
  250. handler(list) {
  251. if (!list || !Array.isArray(list)) return;
  252. this.modelEssay = JSON.parse(JSON.stringify(list));
  253. this.syncAnswerList(list);
  254. },
  255. deep: true,
  256. immediate: true,
  257. },
  258. modelEssay: {
  259. handler(list) {
  260. if (!list || !Array.isArray(list)) return;
  261. this.syncAnswerList(list);
  262. },
  263. deep: true,
  264. immediate: true,
  265. },
  266. isJudgingRightWrong(val) {
  267. if (!val) return;
  268. this.answer.answer_list.forEach(({ mark, value }) => {
  269. const li = this.modelEssay.find((item) => item.mark === mark);
  270. if (li) {
  271. li.input = value;
  272. }
  273. });
  274. this.handleWav(this.answer.record_list);
  275. },
  276. 'data.record_list'(val) {
  277. this.answer.record_list = val;
  278. },
  279. 'answer.record_list': {
  280. handler(val) {
  281. this.data.record_list = val;
  282. },
  283. immediate: true,
  284. },
  285. },
  286. methods: {
  287. /**
  288. * 分组容器样式:有对齐时应用块级与对齐,无对齐时保持行内流式。
  289. * @param {Object} group 分组对象
  290. * @returns {Object} 样式对象
  291. */
  292. groupStyle(group) {
  293. if (!group || !group.align) return {};
  294. return {
  295. display: 'block',
  296. textAlign: group.align,
  297. };
  298. },
  299. /**
  300. * 计算填空控件需要补偿的 margin-bottom。
  301. * 词级 margin-bottom 会撑高行盒,控件 vertical-align: bottom 会贴到被撑高的行盒底部,
  302. * 因此给控件加上与段落一致的 margin-bottom,使其边框底部回到文本底部;
  303. * 无 margin 时返回空字符串,保持原有显示不变。
  304. * @param {Object} li 填空块对象
  305. * @returns {String} 补偿值,如 '28pt',无则空字符串
  306. */
  307. resolveFillControlMargin(li) {
  308. const tagItem = (li?.rich_text_list || []).find((item) =>
  309. /^<(p|div|h[1-6]|li|blockquote)\b[^>]*>/i.test(String(item?.text || '')),
  310. );
  311. const raw = String(tagItem?.text || '').match(/margin-bottom\s*:\s*([^;"']+)/i);
  312. if (!raw) return '';
  313. const match = raw[1].trim().match(/^([\d.]+)\s*(px|pt|em)?$/i);
  314. if (!match) return '';
  315. const amount = parseFloat(match[1]);
  316. const unit = (match[2] || 'px').toLowerCase();
  317. // px/pt 为绝对单位,可直接使用;em 相对词级字号,换算为绝对 pt 后再补偿
  318. if (unit !== 'em') return raw[1].trim();
  319. const fontSize = String(this.pinyinBodyStyles?.fontSize || '16pt');
  320. const fsMatch = fontSize.match(/^([\d.]+)\s*(px|pt|em)?$/i);
  321. if (!fsMatch) return raw[1].trim();
  322. const fsAmount = parseFloat(fsMatch[1]);
  323. const fsUnit = (fsMatch[2] || 'pt').toLowerCase();
  324. if (fsUnit === 'em') return `${amount * fsAmount}em`;
  325. if (fsUnit === 'pt') return `${amount * fsAmount}pt`;
  326. return `${amount * fsAmount}px`;
  327. },
  328. getFillControlAlignmentStyle(li) {
  329. const tagItem = (li?.rich_text_list || []).find((item) =>
  330. /^<(p|div|h[1-6]|li|blockquote)\b[^>]*>/i.test(String(item?.text || '')),
  331. );
  332. const lineHeight = String(tagItem?.text || '').match(/line-height\s*:\s*([^;"']+)/i);
  333. if (!lineHeight || !/^\d*\.?\d+$/.test(lineHeight[1].trim())) return {};
  334. const ratio = Number(lineHeight[1].trim());
  335. if (ratio < 0 || ratio >= 1.5) return {};
  336. return {
  337. marginBottom: '',
  338. position: 'relative',
  339. top: `${1.5 - ratio}em`,
  340. };
  341. },
  342. handleWav(data) {
  343. this.data.record_list = data;
  344. },
  345. syncAnswerList(list) {
  346. this.answer.answer_list = list
  347. .map(({ type, input, audio_answer_list, mark }) => {
  348. if (type === 'input') {
  349. return {
  350. value: input,
  351. mark,
  352. audio_answer_list,
  353. write_base64: '',
  354. };
  355. }
  356. return null;
  357. })
  358. .filter((item) => item);
  359. },
  360. /**
  361. * 处理小音频录音
  362. * @param {Object} data 音频数据
  363. * @param {String} mark 选项标识
  364. */
  365. handleMiniWav(data, mark) {
  366. if (!data || !mark) return;
  367. const li = this.modelEssay.find((item) => item?.mark === mark);
  368. if (li) {
  369. this.$set(li, 'audio_answer_list', data);
  370. }
  371. },
  372. /**
  373. * 处理选中词汇
  374. * @param {String} content 选中的词汇内容
  375. * @param {String} mark 选项标识
  376. * @param {Object} li 当前输入框对象
  377. */
  378. handleSelectWord(content, mark, li) {
  379. if (!content || !mark || !li) return;
  380. li.input = content;
  381. this.selectedWordList.push(mark);
  382. },
  383. /**
  384. * 处理书写区确认
  385. * @param {String} data 书写区数据
  386. */
  387. handleWriteConfirm(data) {
  388. if (!data) return;
  389. const li = this.modelEssay.find((item) => item?.mark === this.writeMark);
  390. if (li) {
  391. this.$set(li, 'write_base64', data);
  392. }
  393. },
  394. handleWriteClick(mark) {
  395. this.writeVisible = true;
  396. this.writeMark = mark;
  397. },
  398. getMainStyle() {
  399. const isRow = this.data.property.arrange_type === arrangeTypeList[0].value;
  400. const isFront = this.data.property.audio_position === audioPositionList[0].value;
  401. const isEnableVoice = this.data.property.is_enable_voice_answer === 'true';
  402. const isHasAudio = this.data.audio_file_id.length > 0;
  403. let areaList = [
  404. { name: 'audio', value: '24px' },
  405. { name: 'fill', value: '1fr' },
  406. ];
  407. if (!isHasAudio) {
  408. areaList.shift();
  409. }
  410. if (!isFront && isHasAudio) {
  411. areaList = areaList.reverse();
  412. }
  413. let gridArea = '';
  414. let gridTemplateRows = '';
  415. let gridTemplateColumns = '';
  416. if (isRow) {
  417. const rowAreas = areaList.map(({ name }) => name);
  418. const rowCols = areaList.map(({ value }) => value);
  419. const templateRows = ['auto'];
  420. if (isEnableVoice) {
  421. rowAreas.push('record');
  422. rowCols.push('160px');
  423. }
  424. const areaRows = [`"${rowAreas.join(' ')}"`];
  425. if (this.showLang) {
  426. areaRows.push(`"${Array(rowAreas.length).fill('lang').join(' ')}"`);
  427. templateRows.push('auto');
  428. }
  429. gridArea = areaRows.join(' ');
  430. gridTemplateRows = templateRows.join(' ');
  431. gridTemplateColumns = rowCols.join(' ');
  432. } else {
  433. const areaRows = areaList.map(({ name }) => `"${name}"`);
  434. const templateRows = areaList.map(({ value }) => value);
  435. if (isEnableVoice) {
  436. areaRows.push('"record"');
  437. templateRows.push('56px');
  438. }
  439. if (this.showLang) {
  440. areaRows.push('"lang"');
  441. templateRows.push('auto');
  442. }
  443. gridArea = areaRows.join(' ');
  444. gridTemplateRows = templateRows.join(' ');
  445. gridTemplateColumns = '1fr';
  446. }
  447. return {
  448. 'grid-auto-flow': isRow ? 'column' : 'row',
  449. 'column-gap': isRow && isHasAudio ? '16px' : undefined,
  450. 'row-gap': isRow || !isHasAudio ? undefined : '8px',
  451. 'grid-template-areas': gridArea,
  452. 'grid-template-rows': gridTemplateRows,
  453. 'grid-template-columns': gridTemplateColumns,
  454. };
  455. },
  456. /**
  457. * 计算答题对错选项字体颜色
  458. * @param {string} mark 选项标识
  459. */
  460. computedAnswerClass(mark) {
  461. if (!this.isJudgingRightWrong && !this.isShowRightAnswer) {
  462. return '';
  463. }
  464. let selectOption = this.answer.answer_list.find((item) => item.mark === mark);
  465. let answerOption = this.data.answer.answer_list.find((item) => item.mark === mark);
  466. if (!selectOption || !answerOption) return '';
  467. let selectValue = selectOption.value;
  468. let answerValue = answerOption.value;
  469. let answerType = answerOption.type;
  470. let classList = [];
  471. let isRight =
  472. answerType === 'only_one' ? selectValue === answerValue : answerValue.split('/').includes(selectValue);
  473. if (this.isJudgingRightWrong) {
  474. isRight ? classList.push('right') : classList.push('wrong');
  475. }
  476. if (this.isShowRightAnswer && !isRight) {
  477. classList.push('show-right-answer');
  478. }
  479. return classList;
  480. },
  481. /**
  482. * 计算正确答案文本
  483. * @param {string} mark 选项标识
  484. */
  485. computedAnswerText(mark) {
  486. let answerOption = this.data.answer.answer_list.find((item) => item.mark === mark);
  487. if (!answerOption) return '';
  488. let answerValue = answerOption.value;
  489. return `${answerValue}`;
  490. },
  491. // 重做
  492. retry() {
  493. this.modelEssay.forEach((li) => {
  494. if (li.type === 'input') {
  495. li.input = '';
  496. li.write_base64 = '';
  497. li.audio_answer_list = [];
  498. }
  499. });
  500. this.selectedWordList = [];
  501. this.isJudgingRightWrong = false;
  502. this.isShowRightAnswer = false;
  503. this.handleWav([]);
  504. },
  505. /**
  506. * 获取无文本内容的数据结构,用于保存为个人模板时的样式模板
  507. */
  508. getNoTextContentData() {
  509. let noTextContentData = JSON.parse(JSON.stringify(this.data));
  510. const resetFieldMap = {
  511. model_essay: [],
  512. content: '',
  513. audio_file_id: '',
  514. file_list: [],
  515. multilingual: [],
  516. analysis_list: [],
  517. answer_list: [],
  518. record_list: [],
  519. };
  520. Object.assign(noTextContentData, resetFieldMap);
  521. if (noTextContentData.answer) {
  522. noTextContentData.answer.answer_list = [];
  523. noTextContentData.answer.reference_answer = '';
  524. }
  525. return noTextContentData;
  526. },
  527. /**
  528. * 处理输入框内容变化
  529. * @description 根据输入框值计算所需长度,动态调整输入框宽度,输入框宽度不小于默认宽度,且不超过组件最大宽度 1000px,需要考虑输入框前面的宽度,保证输入框不会超出组件范围
  530. * @param {String} value 输入框当前值
  531. * @param {String} mark 选项标识
  532. */
  533. handleInput(value, mark) {
  534. if (!mark) return;
  535. const text = `${value || ''}`;
  536. const defaultWidth = Number(this.data?.property?.input_default_width) || 120;
  537. const fontSize = 16;
  538. const fontFamily = this.data?.property?.fill_font || 'arial';
  539. const canvas = document.createElement('canvas');
  540. const context = canvas.getContext('2d');
  541. let textWidth = 0;
  542. if (context) {
  543. context.font = `${fontSize}pt ${fontFamily}`;
  544. textWidth = context.measureText(text || ' ').width;
  545. }
  546. // 额外留出输入框内边距和边框余量,避免刚好卡边。
  547. const contentWidth = Math.ceil(textWidth + 24);
  548. const inputRef = this.$refs[`input-${mark}`];
  549. const inputVm = Array.isArray(inputRef) ? inputRef[0] : inputRef;
  550. const inputEl = inputVm?.$el;
  551. const containerEl = this.$el;
  552. let availableWidth = 1000;
  553. if (inputEl && containerEl) {
  554. const inputRect = inputEl.getBoundingClientRect();
  555. const containerRect = containerEl.getBoundingClientRect();
  556. const rightLimit = containerRect.left + Math.min(containerRect.width, 1000);
  557. availableWidth = Math.floor(rightLimit - inputRect.left - 12);
  558. }
  559. const maxWidth = Math.max(40, availableWidth);
  560. const nextWidth = Math.min(Math.max(contentWidth, defaultWidth), maxWidth);
  561. this.$set(this.inputWidthMap, mark, nextWidth);
  562. },
  563. },
  564. };
  565. </script>
  566. <style lang="scss" scoped>
  567. @use '@/styles/mixin.scss' as *;
  568. .select-preview {
  569. @include preview-base;
  570. .main {
  571. display: grid;
  572. gap: 10px;
  573. align-items: center;
  574. }
  575. .fill-wrapper {
  576. grid-area: fill;
  577. align-self: start;
  578. font-size: 16pt;
  579. :deep .pinyin-area .rich-text-container,
  580. :deep .pinyin-area .pinyin-paragraph,
  581. :deep .pinyin-area .pinyin-sentence {
  582. display: inline;
  583. }
  584. .fill-content {
  585. width: calc(100% - 82px);
  586. margin: 0;
  587. :deep .el-input__inner {
  588. background-color: transparent !important;
  589. }
  590. }
  591. .content {
  592. display: inline;
  593. }
  594. .record-box.fill {
  595. display: inline-flex;
  596. align-items: center;
  597. background-color: #fff;
  598. border-bottom: 1px solid $font-color;
  599. :deep(.playBack) {
  600. width: 20px;
  601. height: 20px;
  602. }
  603. }
  604. .write-click {
  605. display: inline-block;
  606. width: 104px;
  607. height: 32px;
  608. padding: 0 4px;
  609. vertical-align: bottom;
  610. cursor: pointer;
  611. border-bottom: 1px solid $font-color;
  612. img {
  613. width: 100%;
  614. height: 100%;
  615. }
  616. }
  617. .select-content {
  618. // 选中的词贴到控件底部:文字底部与正文文字底部对齐(下边框即填空横线)
  619. display: inline-flex;
  620. align-items: flex-end;
  621. height: 32px;
  622. margin: 0 10px;
  623. vertical-align: bottom;
  624. cursor: pointer;
  625. border-bottom: 1px solid $font-color;
  626. :deep p {
  627. margin: 0;
  628. }
  629. }
  630. .el-input {
  631. display: inline-flex;
  632. align-items: center;
  633. width: 120px;
  634. margin: 0 10px;
  635. margin-bottom: -2px;
  636. vertical-align: bottom;
  637. &.pinyin :deep input.el-input__inner {
  638. font-family: 'PINYIN-B', sans-serif;
  639. }
  640. &.chinese :deep input.el-input__inner {
  641. font-family: 'arial', sans-serif;
  642. }
  643. &.english :deep input.el-input__inner {
  644. font-family: 'arial', sans-serif;
  645. }
  646. &.right {
  647. :deep input.el-input__inner {
  648. color: $right-color;
  649. }
  650. }
  651. &.wrong {
  652. :deep input.el-input__inner {
  653. color: $error-color;
  654. }
  655. }
  656. :deep .el-input__inner {
  657. // 1em 行盒:输入文字基线与正文文字对齐,下边框即填空横线
  658. height: 1em;
  659. padding: 0;
  660. font-family: var(--fill-font-family, 'arial'), sans-serif;
  661. font-size: 16pt;
  662. line-height: 1em;
  663. color: $font-color;
  664. text-align: center;
  665. background-color: #fff;
  666. border-width: 0;
  667. border-bottom: 1px solid $font-color;
  668. border-radius: 0;
  669. }
  670. }
  671. .right-answer {
  672. // 盒模型/行高与填空输入框一致:文字底部贴正文文字底部,下边框即填空横线
  673. // 字体与学生输入保持一致(--fill-font-family 由模板传入)
  674. position: relative;
  675. box-sizing: border-box;
  676. display: inline-block;
  677. height: 1em;
  678. margin: 0 4px;
  679. font-family: var(--fill-font-family, 'arial'), sans-serif;
  680. line-height: 1em;
  681. vertical-align: bottom;
  682. border-bottom: 1px solid $font-color;
  683. }
  684. }
  685. .record-box {
  686. padding: 6px 12px;
  687. vertical-align: bottom;
  688. background-color: $fill-color;
  689. :deep .record-time {
  690. width: 100px;
  691. }
  692. }
  693. .sound-record-wrapper {
  694. width: 200px;
  695. max-width: 200px;
  696. &.vertical {
  697. margin: 24px 0 0 16px;
  698. }
  699. }
  700. }
  701. </style>
  702. <style lang="scss" scoped>
  703. .word-list {
  704. display: flex;
  705. flex-wrap: wrap;
  706. gap: 8px;
  707. align-items: center;
  708. .word-item {
  709. cursor: pointer;
  710. }
  711. }
  712. </style>