TablePreview.vue 60 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615
  1. <!-- eslint-disable vue/no-v-html -->
  2. <template>
  3. <div class="table-preview" :style="[getAreaStyle(), getComponentStyle()]">
  4. <SerialNumberPosition v-if="isEnable(data.property.sn_display_mode)" :property="data.property" />
  5. <div class="main">
  6. <div
  7. class="table-box"
  8. :style="{
  9. width: isMobile ? '100%' : data.property.width + 'px',
  10. }"
  11. >
  12. <table style="width: 100%; table-layout: fixed">
  13. <colgroup>
  14. <col v-for="(item, i) in data.col_width" :key="`col-${i}`" :style="{ width: `${item.value}%` }" />
  15. </colgroup>
  16. <tr v-for="(row, i) in data.option_list" :key="`tr-${i}`" :style="{ height: data.property.height + 'px' }">
  17. <template v-for="(col, j) in row">
  18. <td
  19. v-if="col.is_show_cell"
  20. :key="col.mark"
  21. :colspan="col.cell.colspan"
  22. :rowspan="col.cell.rowspan"
  23. :style="{
  24. borderTop: i === 0 ? '1px solid ' + data.property.border_color : '',
  25. borderBottom: '1px solid ' + data.property.border_color,
  26. borderLeft:
  27. i === 0 && data.property.first_line_color
  28. ? '1px solid ' + data.property.border_color
  29. : j === 0
  30. ? '2px solid ' +
  31. (data.property.decoration_color ? data.property.decoration_color : data.property.border_color)
  32. : '1px dotted ' + data.property.border_color,
  33. borderRight:
  34. i === 0 && data.property.first_line_color
  35. ? '1px solid ' + data.property.border_color
  36. : j === row.length - 1
  37. ? '2px solid ' +
  38. (data.property.decoration_color ? data.property.decoration_color : data.property.border_color)
  39. : '1px dotted ' + data.property.border_color,
  40. borderRadius: i === 0 && data.property.first_line_color ? '4px ' : '0',
  41. background: col.cell.bgColor
  42. ? col.cell.bgColor
  43. : data.cols_bg_list[j]
  44. ? data.cols_bg_list[j]
  45. : data.rows_bg_list[i]
  46. ? data.rows_bg_list[i]
  47. : '',
  48. height: data.property.height + 'px',
  49. }"
  50. >
  51. <div class="cell-wrap">
  52. <!-- , computedRichStyle(col.content) -->
  53. <div class="cell-wrap-content" :style="[tdStyle(col.content)]">
  54. <template v-if="isEnable(data.property.view_pinyin)">
  55. <template v-if="col.rich_text_list">
  56. <div
  57. v-for="(item, index) in col.rich_text_list"
  58. :key="index"
  59. class="pinyin-text pinyin-text-sentence"
  60. :class="[
  61. index === 0 ? 'pinyin-text-left' : '',
  62. item.type === 'audio' ? 'pinyin-text-audio' : '',
  63. ]"
  64. >
  65. <!-- 文字块:包含 word_list -->
  66. <span
  67. v-if="item.type === 'text'"
  68. :key="'text-' + index"
  69. class="pinyin-sentence"
  70. :style="[
  71. { 'align-items': data.property.pinyin_position === 'top' ? 'flex-end' : 'flex-start' },
  72. ]"
  73. >
  74. <span
  75. v-for="(word, wIndex) in item.word_list"
  76. :key="wIndex"
  77. class="pinyin-text"
  78. :style="item.styleObj"
  79. >
  80. <span v-if="word.type === 'text'">
  81. <span
  82. :style="{
  83. flexDirection:
  84. data.property.pinyin_position === 'top' ? 'column' : 'column-reverse',
  85. 'align-items': getWordAlignItems(word, item),
  86. }"
  87. >
  88. <span class="pinyin" :style="getPinyinStyle(word)">
  89. {{ word.pinyin.trim() ? word.pinyin : '&nbsp;' }}</span
  90. >
  91. <span class="py-char" :style="getCharStyle(word, item)">{{
  92. convertText(word.text)
  93. }}</span>
  94. </span>
  95. </span>
  96. <template v-else-if="word.type === 'input'">
  97. <span v-if="data.property.pinyin_position === 'top'" class="pinyin">&nbsp;</span>
  98. <template v-if="data.property.fill_type === fillTypeList[0].value">
  99. <el-input
  100. :key="index"
  101. v-model="word.value"
  102. :disabled="disabled"
  103. :class="[...computedAnswerClass(word, i, j)]"
  104. :style="getInputStyle(item.styleObj, word)"
  105. />
  106. </template>
  107. <template v-else-if="data.property.fill_type === fillTypeList[1].value">
  108. <el-popover :key="index" placement="top" trigger="click">
  109. <div class="word-list">
  110. <span
  111. v-for="{ content, mark } in data.word_list"
  112. :key="mark"
  113. class="word-item"
  114. @click="handleSelectWord(content, mark, word)"
  115. >
  116. {{ convertText(content) }}
  117. </span>
  118. </div>
  119. <el-input
  120. slot="reference"
  121. v-model="word.value"
  122. :readonly="true"
  123. :class="[...computedAnswerClass(word, i, j)]"
  124. :style="getInputStyle(item.styleObj, word)"
  125. />
  126. </el-popover>
  127. </template>
  128. <template v-else-if="data.property.fill_type === fillTypeList[2].value">
  129. <span :key="j" class="write-click" @click="handleWriteClick(word)">
  130. <img
  131. v-show="word.write_base64"
  132. style="background-color: #f4f4f4"
  133. :src="word.write_base64"
  134. alt="write-show"
  135. />
  136. </span>
  137. </template>
  138. <template v-else-if="data.property.fill_type === fillTypeList[3].value">
  139. <SoundRecordBox
  140. ref="record"
  141. :key="j"
  142. type="mini"
  143. :many-times="false"
  144. class="record-box"
  145. :answer-record-list="data.audio_answer_list"
  146. :task-model="isJudgingRightWrong ? 'ANSWER' : ''"
  147. :attrib="data.unified_attrib"
  148. @handleWav="handleMiniWav($event, word)"
  149. />
  150. </template>
  151. <span v-if="data.property.pinyin_position === 'bottom'" class="pinyin">&nbsp;</span>
  152. </template>
  153. </span>
  154. </span>
  155. <!-- 图片块 -->
  156. <div
  157. v-else-if="item.type === 'image'"
  158. :key="'image-' + index"
  159. :style="item.containerStyle"
  160. class="image-container"
  161. >
  162. <img
  163. :src="item.src"
  164. :alt="item.alt"
  165. :width="item.width"
  166. :height="item.height"
  167. :style="item.imageStyle"
  168. class="inline-image"
  169. />
  170. </div>
  171. <!-- 视频块 -->
  172. <div
  173. v-else-if="item.type === 'video'"
  174. :key="'video-' + index"
  175. :style="item.containerStyle"
  176. class="video-container"
  177. >
  178. <video
  179. :poster="item.poster"
  180. :width="item.width"
  181. :height="item.height"
  182. :style="item.mediaStyle"
  183. controls
  184. class="inline-video"
  185. >
  186. <source :src="item.src" :type="item.videoType" />
  187. 您的浏览器不支持视频播放
  188. </video>
  189. </div>
  190. <!-- 音频块 -->
  191. <div
  192. v-else-if="item.type === 'audio'"
  193. :key="'audio-' + index"
  194. :style="item.containerStyle"
  195. class="audio-container"
  196. >
  197. <audio :src="item.src" :style="item.mediaStyle" controls class="inline-audio">
  198. 您的浏览器不支持音频播放
  199. </audio>
  200. </div>
  201. <!-- 换行符 -->
  202. <br v-else-if="item.type === 'newline'" :key="'newline-' + index" />
  203. <!--
  204. <template v-else>
  205. <span v-if="data.property.pinyin_position === 'top'" class="pinyin">
  206. {{ item.pinyin.replace(/\s+/g, '') }}
  207. </span>
  208. <span :style="{ ...item.activeTextStyle }">
  209. {{ convertText(item.text) }}
  210. </span>
  211. <span v-if="data.property.pinyin_position === 'bottom'" class="pinyin">{{
  212. item.pinyin.replace(/\s+/g, '')
  213. }}</span>
  214. </template> -->
  215. </div>
  216. </template>
  217. </template>
  218. <template v-else>
  219. <div v-for="(item, index) in col.model_essay" :key="index" :style="[tdStyle]">
  220. <span
  221. v-if="item.type === 'text'"
  222. :key="index"
  223. :style="{
  224. fontSize:
  225. data.unified_attrib && data.unified_attrib.font_size ? data.unified_attrib.font_size : '',
  226. fontFamily: data.unified_attrib && data.unified_attrib.font ? data.unified_attrib.font : '',
  227. }"
  228. v-html="convertText(sanitizeHTML(item.value))"
  229. ></span>
  230. <template v-if="item.type === 'input'">
  231. <template v-if="data.property.fill_type === fillTypeList[0].value">
  232. <el-input
  233. :key="index"
  234. v-model="item.value"
  235. :disabled="disabled"
  236. :class="[...computedAnswerClass(item, i, j)]"
  237. :style="[
  238. {
  239. width: Math.max(40, item.value.length * 21.3) + 'px',
  240. fontSize:
  241. data.unified_attrib && data.unified_attrib.font_size
  242. ? data.unified_attrib.font_size
  243. : '',
  244. fontFamily:
  245. data.unified_attrib && data.unified_attrib.font ? data.unified_attrib.font : '',
  246. },
  247. ]"
  248. />
  249. </template>
  250. <template v-else-if="data.property.fill_type === fillTypeList[1].value">
  251. <el-popover :key="index" placement="top" trigger="click">
  252. <div class="word-list">
  253. <span
  254. v-for="{ content, mark } in data.word_list"
  255. :key="mark"
  256. class="word-item"
  257. @click="handleSelectWord(content, mark, item)"
  258. >
  259. {{ convertText(content) }}
  260. </span>
  261. </div>
  262. <el-input
  263. slot="reference"
  264. v-model="item.value"
  265. :readonly="true"
  266. :class="[...computedAnswerClass(item, i, j)]"
  267. :style="[{ width: Math.max(40, item.value.length * 21.3) + 'px' }]"
  268. />
  269. </el-popover>
  270. </template>
  271. <template v-else-if="data.property.fill_type === fillTypeList[2].value">
  272. <span :key="j" class="write-click" @click="handleWriteClick(item)">
  273. <img
  274. v-show="item.write_base64"
  275. style="background-color: #f4f4f4"
  276. :src="item.write_base64"
  277. alt="write-show"
  278. />
  279. </span>
  280. </template>
  281. <template v-else-if="data.property.fill_type === fillTypeList[3].value">
  282. <SoundRecordBox
  283. ref="record"
  284. :key="j"
  285. type="mini"
  286. :many-times="false"
  287. class="record-box"
  288. :answer-record-list="data.audio_answer_list"
  289. :task-model="isJudgingRightWrong ? 'ANSWER' : ''"
  290. :attrib="data.unified_attrib"
  291. @handleWav="handleMiniWav($event, item)"
  292. />
  293. </template>
  294. </template>
  295. </div>
  296. </template>
  297. </div>
  298. <i
  299. v-if="col.cell.isCross"
  300. :class="[
  301. { 'el-icon-check': col.crossAnswer === statusList[1] },
  302. { 'el-icon-close': col.crossAnswer === statusList[2] },
  303. ...computedAnswerCrossClass(col, i, j),
  304. ]"
  305. :style="{ display: 'block', width: '18px', height: '18px', border: '1px solid #000' }"
  306. @click="toggle(col)"
  307. ></i>
  308. </div>
  309. <!-- , computedRichStyle(col.content) -->
  310. <span v-if="showLang" class="multilingual" :style="[tdStyle(col.content)]">
  311. {{
  312. multilingualTextList[getLang()] &&
  313. multilingualTextList[getLang()][i] &&
  314. multilingualTextList[getLang()][i][j]
  315. ? multilingualTextList[getLang()][i][j]
  316. : ''
  317. }}
  318. </span>
  319. </td>
  320. </template>
  321. </tr>
  322. </table>
  323. </div>
  324. <PreviewOperation
  325. v-if="isHasInput"
  326. @showAnswerAnalysis="showAnswerAnalysis"
  327. @judgeCorrect="judgeCorrect"
  328. @retry="retry"
  329. />
  330. <AnswerCorrect
  331. :answer-correct="data?.answer_correct"
  332. :visible.sync="visibleAnswerCorrect"
  333. :is-check-correct="isCheckCorrect"
  334. @closeAnswerCorrect="closeAnswerCorrect"
  335. />
  336. <AnswerAnalysis
  337. :visible.sync="visibleAnswerAnalysis"
  338. :answer-list="data.answer_list"
  339. :analysis-list="data.analysis_list"
  340. @closeAnswerAnalysis="closeAnswerAnalysis"
  341. >
  342. <div
  343. slot="right-answer"
  344. class="table-box"
  345. :style="{
  346. width: isMobile ? '100%' : data.property.width + 'px',
  347. }"
  348. >
  349. <table style="width: 100%; table-layout: fixed">
  350. <colgroup>
  351. <col v-for="(item, i) in data.col_width" :key="`col-${i}`" :style="{ width: `${item.value}%` }" />
  352. </colgroup>
  353. <tr v-for="(row, i) in data.option_list" :key="`tr-${i}`" :style="{ height: data.property.height + 'px' }">
  354. <template v-for="(col, j) in row">
  355. <td
  356. v-if="col.is_show_cell"
  357. :key="col.mark"
  358. :colspan="col.colspan"
  359. :rowspan="col.rowspan"
  360. :style="{
  361. borderTop: i === 0 ? '1px solid ' + data.property.border_color : '',
  362. borderBottom: '1px solid ' + data.property.border_color,
  363. borderLeft:
  364. i === 0 && data.property.first_line_color
  365. ? '1px solid ' + data.property.border_color
  366. : j === 0
  367. ? '2px solid ' +
  368. (data.property.decoration_color
  369. ? data.property.decoration_color
  370. : data.property.border_color)
  371. : '1px dotted ' + data.property.border_color,
  372. borderRight:
  373. i === 0 && data.property.first_line_color
  374. ? '1px solid ' + data.property.border_color
  375. : j === row.length - 1
  376. ? '2px solid ' +
  377. (data.property.decoration_color
  378. ? data.property.decoration_color
  379. : data.property.border_color)
  380. : '1px dotted ' + data.property.border_color,
  381. borderRadius: i === 0 && data.property.first_line_color ? '4px ' : '0',
  382. background: col.cell.bgColor
  383. ? col.cell.bgColor
  384. : data.cols_bg_list[j]
  385. ? data.cols_bg_list[j]
  386. : data.rows_bg_list[i]
  387. ? data.rows_bg_list[i]
  388. : '',
  389. height: data.property.height + 'px',
  390. }"
  391. >
  392. <div class="cell-wrap">
  393. <!-- , computedRichStyle(col.content) -->
  394. <div class="cell-wrap-content" :style="[tdStyle(col.content)]">
  395. <template v-if="isEnable(data.property.view_pinyin)">
  396. <template v-if="col.rich_text_list">
  397. <div
  398. v-for="(item, index) in col.rich_text_list"
  399. :key="index"
  400. class="pinyin-text pinyin-text-sentence"
  401. :class="[
  402. index === 0 ? 'pinyin-text-left' : '',
  403. item.type === 'audio' ? 'pinyin-text-audio' : '',
  404. ]"
  405. >
  406. <!-- 文字块:包含 word_list -->
  407. <span
  408. v-if="item.type === 'text'"
  409. :key="'text-' + index"
  410. class="pinyin-sentence"
  411. :style="[
  412. { 'align-items': data.property.pinyin_position === 'top' ? 'flex-end' : 'flex-start' },
  413. ]"
  414. >
  415. <span
  416. v-for="(word, wIndex) in item.word_list"
  417. :key="wIndex"
  418. class="pinyin-text"
  419. :style="item.styleObj"
  420. >
  421. <span v-if="word.type === 'text'">
  422. <span
  423. :style="{
  424. flexDirection:
  425. data.property.pinyin_position === 'top' ? 'column' : 'column-reverse',
  426. 'align-items': getWordAlignItems(word, item),
  427. }"
  428. >
  429. <span class="pinyin" :style="getPinyinStyle(word)">{{
  430. word.pinyin.trim() ? word.pinyin : '&nbsp;'
  431. }}</span>
  432. <span class="py-char" :style="getCharStyle(word, item)">{{
  433. convertText(word.text)
  434. }}</span>
  435. </span>
  436. </span>
  437. <template v-else-if="word.type === 'input'">
  438. <span v-if="data.property.pinyin_position === 'top'" class="pinyin">&nbsp;</span>
  439. <span
  440. v-show="computedAnswerText(word, i, j).length > 0"
  441. :key="`answer-${j}`"
  442. class="right-answer"
  443. >
  444. {{ convertText(computedAnswerText(word, i, j)) }}
  445. </span>
  446. <span v-if="data.property.pinyin_position === 'bottom'" class="pinyin">&nbsp;</span>
  447. </template>
  448. </span>
  449. </span>
  450. <!-- 图片块 -->
  451. <div
  452. v-else-if="item.type === 'image'"
  453. :key="'image-' + index"
  454. :style="item.containerStyle"
  455. class="image-container"
  456. >
  457. <img
  458. :src="item.src"
  459. :alt="item.alt"
  460. :width="item.width"
  461. :height="item.height"
  462. :style="item.imageStyle"
  463. class="inline-image"
  464. />
  465. </div>
  466. <!-- 视频块 -->
  467. <div
  468. v-else-if="item.type === 'video'"
  469. :key="'video-' + index"
  470. :style="item.containerStyle"
  471. class="video-container"
  472. >
  473. <video
  474. :poster="item.poster"
  475. :width="item.width"
  476. :height="item.height"
  477. :style="item.mediaStyle"
  478. controls
  479. class="inline-video"
  480. >
  481. <source :src="item.src" :type="item.videoType" />
  482. 您的浏览器不支持视频播放
  483. </video>
  484. </div>
  485. <!-- 音频块 -->
  486. <div
  487. v-else-if="item.type === 'audio'"
  488. :key="'audio-' + index"
  489. :style="item.containerStyle"
  490. class="audio-container"
  491. >
  492. <audio :src="item.src" :style="item.mediaStyle" controls class="inline-audio">
  493. 您的浏览器不支持音频播放
  494. </audio>
  495. </div>
  496. <!-- 换行符 -->
  497. <br v-else-if="item.type === 'newline'" :key="'newline-' + index" />
  498. </div>
  499. </template>
  500. </template>
  501. <template v-else>
  502. <div v-for="(item, index) in col.model_essay" :key="index" :style="[tdStyle]">
  503. <span
  504. v-if="item.type === 'text'"
  505. :key="index"
  506. :style="{
  507. fontSize:
  508. data.unified_attrib && data.unified_attrib.font_size
  509. ? data.unified_attrib.font_size
  510. : '',
  511. fontFamily:
  512. data.unified_attrib && data.unified_attrib.font ? data.unified_attrib.font : '',
  513. }"
  514. v-html="convertText(sanitizeHTML(item.value))"
  515. ></span>
  516. <template v-if="item.type === 'input'">
  517. <!-- <template v-if="data.property.fill_type === fillTypeList[0].value">
  518. <el-input
  519. :key="index"
  520. v-model="item.value"
  521. :disabled="disabled"
  522. :class="[...computedAnswerClass(item, i, j)]"
  523. :style="[{ width: Math.max(40, item.value.length * 21.3) + 'px' }]"
  524. />
  525. </template>
  526. <template v-else-if="data.property.fill_type === fillTypeList[1].value">
  527. <el-popover :key="index" placement="top" trigger="click">
  528. <div class="word-list">
  529. <span
  530. v-for="{ content, mark } in data.word_list"
  531. :key="mark"
  532. class="word-item"
  533. @click="handleSelectWord(content, mark, item)"
  534. >
  535. {{ convertText(content) }}
  536. </span>
  537. </div>
  538. <el-input
  539. slot="reference"
  540. v-model="item.value"
  541. :readonly="true"
  542. :class="[...computedAnswerClass(item, i, j)]"
  543. :style="[{ width: Math.max(40, item.value.length * 21.3) + 'px' }]"
  544. />
  545. </el-popover>
  546. </template>
  547. <template v-else-if="data.property.fill_type === fillTypeList[2].value">
  548. <span :key="j" class="write-click" @click="handleWriteClick(item)">
  549. <img
  550. v-show="item.write_base64"
  551. style="background-color: #f4f4f4"
  552. :src="item.write_base64"
  553. alt="write-show"
  554. />
  555. </span>
  556. </template>
  557. <template v-else-if="data.property.fill_type === fillTypeList[3].value">
  558. <SoundRecordBox
  559. ref="record"
  560. :key="j"
  561. type="mini"
  562. :many-times="false"
  563. class="record-box"
  564. :answer-record-list="data.audio_answer_list"
  565. :task-model="isJudgingRightWrong ? 'ANSWER' : ''"
  566. :attrib="data.unified_attrib"
  567. @handleWav="handleMiniWav($event, item)"
  568. />
  569. </template> -->
  570. <span
  571. v-show="computedAnswerText(item, i, j).length > 0"
  572. :key="`answer-${j}`"
  573. class="right-answer"
  574. >
  575. {{ convertText(computedAnswerText(item, i, j)) }}
  576. </span>
  577. </template>
  578. </div>
  579. </template>
  580. </div>
  581. <i
  582. v-if="col.cell.isCross"
  583. :class="[
  584. { 'el-icon-check': data.answer_lists[i][j].crossAnswer === statusList[1] },
  585. { 'el-icon-close': data.answer_lists[i][j].crossAnswer === statusList[2] },
  586. ]"
  587. style="display: block; width: 18px; height: 18px; border: 1px solid #30a47d"
  588. ></i>
  589. </div>
  590. <!-- , computedRichStyle(col.content) -->
  591. <span v-if="showLang" class="multilingual" :style="[tdStyle(col.content)]">
  592. {{
  593. multilingualTextList[getLang()] &&
  594. multilingualTextList[getLang()][i] &&
  595. multilingualTextList[getLang()][i][j]
  596. ? multilingualTextList[getLang()][i][j]
  597. : ''
  598. }}
  599. </span>
  600. </td>
  601. </template>
  602. </tr>
  603. </table>
  604. </div>
  605. </AnswerAnalysis>
  606. </div>
  607. <WriteDialog :visible.sync="writeVisible" @confirm="handleWriteConfirm" />
  608. </div>
  609. </template>
  610. <script>
  611. import { getTableData, fillTypeList } from '@/views/book/courseware/data/table';
  612. import PreviewMixin from '../common/PreviewMixin';
  613. import WriteDialog from '../fill/components/WriteDialog.vue';
  614. import SoundRecordBox from '@/views/book/courseware/preview/components/record_input/SoundRecord.vue';
  615. export default {
  616. name: 'TablePreview',
  617. components: { SoundRecordBox, WriteDialog },
  618. mixins: [PreviewMixin],
  619. props: {
  620. isMobile: {
  621. type: Boolean,
  622. default: false,
  623. },
  624. },
  625. data() {
  626. return {
  627. data: getTableData(),
  628. table_width: 0,
  629. multilingualTextList: {}, // 多语言对应的切割后的翻译
  630. fillTypeList,
  631. selectedWordList: [], // 用于存储选中的词汇
  632. writeVisible: false,
  633. writeMark: '',
  634. isHasInput: false,
  635. statusList: ['normal', 'tick', 'cross'],
  636. };
  637. },
  638. computed: {
  639. tdStyle() {
  640. return function (content) {
  641. if (!content) return {};
  642. let styleObject = {};
  643. // 提取首个标签的 style 样式属性
  644. let styleMatch = content.match(/<\w+\s+[^>]*style=["']([^"']*)["'][^>]*>/i);
  645. if (styleMatch && styleMatch[1]) {
  646. let styleString = styleMatch[1];
  647. let styleArray = styleString.split(';').filter((s) => s.trim() !== '');
  648. styleArray.forEach((style) => {
  649. let [key, value] = style.split(':');
  650. if (key && value) {
  651. styleObject[key.trim()] = value.trim();
  652. }
  653. });
  654. if (styleObject.hasOwnProperty('text-align')) {
  655. if (styleObject['text-align'] === 'center') {
  656. this.$set(styleObject, 'justify-content', 'center');
  657. } else if (styleObject['text-align'] === 'right') {
  658. this.$set(styleObject, 'justify-content', 'end');
  659. } else {
  660. this.$set(styleObject, 'justify-content', 'left');
  661. }
  662. }
  663. }
  664. let obj = {};
  665. // if (this.data.mode === 'short') {
  666. let styles = this.data.styles;
  667. obj = {
  668. fontFamily: styles.fontFamily,
  669. fontSize: styles.fontSize,
  670. color: styles.fontColor,
  671. textDecoration: styles.isUnderline ? 'underline' : styles.isStrikethrough ? 'line-through' : '',
  672. fontWeight: styles.isBold ? 'bold' : '',
  673. fontStyle: styles.isItalic ? 'italic' : '',
  674. // justifyContent: styles.textAlign === 'MIDDLE' ? 'center' : styles.textAlign === 'RIGHT' ? 'end' : 'left',
  675. // textAlign: styles.textAlign === 'MIDDLE' ? 'center' : styles.textAlign === 'RIGHT' ? 'end' : 'left',
  676. };
  677. // } else {
  678. // obj = {
  679. // width: '100%',
  680. // };
  681. // }
  682. return { ...obj, ...styleObject };
  683. };
  684. },
  685. },
  686. watch: {
  687. 'data.col_width': {
  688. handler(val) {
  689. this.handleData();
  690. },
  691. },
  692. 'data.property': {
  693. handler(val) {
  694. this.computedTableCellShow();
  695. },
  696. },
  697. isJudgingRightWrong(val) {
  698. if (!val) return;
  699. this.data.answer_lists = this.answer.answer_list;
  700. // this.answer.answer_list.forEach(({ mark, value }) => {
  701. // this.modelEssay.forEach((item) => {
  702. // item.forEach((li) => {
  703. // if (li.mark === mark) {
  704. // li.content = value;
  705. // }
  706. // });
  707. // });
  708. // });
  709. },
  710. },
  711. created() {
  712. this.handleData();
  713. this.computedTableCellShow();
  714. },
  715. methods: {
  716. // 预处理 richTextList,合并分散的视频标签
  717. normalizedRichTextList(col) {
  718. if (!col.rich_text_list || col.rich_text_list.length === 0) return [];
  719. const result = [];
  720. let i = 0;
  721. while (i < col.rich_text_list.length) {
  722. const item = col.rich_text_list[i];
  723. // 检查是否是视频开始标签(且不包含结束标签)
  724. if (
  725. item.text &&
  726. typeof item.text === 'string' &&
  727. item.text.includes('<video') &&
  728. !item.text.includes('</video>')
  729. ) {
  730. let combinedText = item.text;
  731. let j = i + 1;
  732. // 向后查找 source 和结束标签,直到遇到 </video> 或超出范围
  733. while (j < col.rich_text_list.length) {
  734. const nextItem = col.rich_text_list[j];
  735. if (nextItem.text) {
  736. combinedText += `${nextItem.text}`;
  737. }
  738. // 如果找到了结束标签,合并完成,跳出内部循环
  739. if (nextItem.text && nextItem.text.includes('</video>')) {
  740. j++; // 跳过结束标签
  741. break;
  742. }
  743. j++;
  744. }
  745. // 将合并后的内容作为一个新项加入结果
  746. result.push({
  747. ...item,
  748. text: combinedText,
  749. is_merged_video: true, // 标记一下,可选
  750. });
  751. // 更新主循环索引,跳过已合并的项
  752. i = j;
  753. } else {
  754. // 其他项直接加入
  755. result.push(item);
  756. i++;
  757. }
  758. }
  759. return result;
  760. },
  761. // 解析 richTextList 为可渲染的块
  762. parsedBlocks(col) {
  763. const listToParse = this.normalizedRichTextList(col);
  764. if (!listToParse || listToParse.length === 0) return [];
  765. // if (!col.rich_text_list || col.rich_text_list.length === 0) return [];
  766. const blocks = [];
  767. let textBlockIndex = 0;
  768. let oldIndex = -1;
  769. let paragraphIndex = 0;
  770. const tagStack = [];
  771. for (const item of listToParse) {
  772. oldIndex += 1;
  773. if (item.text && typeof item.text === 'string' && item.text.includes('<img')) {
  774. blocks.push(this.parseImageBlock(item, tagStack));
  775. } else if (item.text && typeof item.text === 'string' && item.text.includes('<video')) {
  776. // item.text 是完整的 video 标签串
  777. blocks.push(this.parseVideoBlock(item, tagStack));
  778. } else if (item.text && typeof item.text === 'string' && item.text.includes('<audio')) {
  779. blocks.push(this.parseAudioBlock(item, tagStack));
  780. } else if (item.is_style === 'true' || item.is_style === true) {
  781. if (item.text.includes('<br')) {
  782. blocks.push({ type: 'newline' });
  783. paragraphIndex += 1;
  784. } else {
  785. this.handleStyleTag(item, tagStack);
  786. }
  787. } else if (item.text === '\n') {
  788. blocks.push({ type: 'newline' });
  789. paragraphIndex += 1;
  790. } else if (item.word_list && item.word_list.length > 0) {
  791. const hasEmphasisDot = this.checkHasEmphasisDot(tagStack);
  792. blocks.push(this.createTextBlock(item, tagStack, textBlockIndex, oldIndex, paragraphIndex, hasEmphasisDot));
  793. textBlockIndex += 1;
  794. }
  795. }
  796. return blocks;
  797. },
  798. // 解析图片块
  799. parseImageBlock(item, tagStack) {
  800. const imgMatch = item.text.match(/<img\s+([^>]*)\/?\s*>/i);
  801. if (!imgMatch) return null;
  802. const attrs = imgMatch[1];
  803. const srcMatch = attrs.match(/src=["']([^"']*)["']/i);
  804. const altMatch = attrs.match(/alt=["']([^"']*)["']/i);
  805. const widthMatch = attrs.match(/width=["']?(\d+)["']?/i);
  806. const heightMatch = attrs.match(/height=["']?(\d+)["']?/i);
  807. const styleMatch = attrs.match(/style=["']([^"']*)["']/i);
  808. const containerStyleObj = {};
  809. tagStack.forEach((tagItem) => {
  810. if (tagItem.style) {
  811. this.mergeStyleString(containerStyleObj, tagItem.style);
  812. }
  813. });
  814. const imageStyleObj = {};
  815. if (styleMatch) {
  816. this.mergeStyleString(imageStyleObj, styleMatch[1]);
  817. }
  818. return {
  819. type: 'image',
  820. src: srcMatch ? srcMatch[1] : '',
  821. alt: altMatch ? altMatch[1] : '',
  822. width: widthMatch ? widthMatch[1] : null,
  823. height: heightMatch ? heightMatch[1] : null,
  824. containerStyle: containerStyleObj,
  825. imageStyle: imageStyleObj,
  826. };
  827. },
  828. // 处理样式标签
  829. handleStyleTag(item, tagStack) {
  830. const tagText = item.text || '';
  831. const closeTagMatch = tagText.match(/^<\/(\w+)>$/);
  832. if (closeTagMatch) {
  833. const tagName = closeTagMatch[1];
  834. this.removeTagFromStack(tagStack, tagName);
  835. } else {
  836. const openTagMatch = tagText.match(/^<(\w+)([^>]*)>$/);
  837. if (openTagMatch) {
  838. const tagName = openTagMatch[1];
  839. const attrs = openTagMatch[2];
  840. const { style, className } = this.extractTagInfo(attrs, tagName);
  841. tagStack.push({ tag: tagName, style, className });
  842. }
  843. }
  844. },
  845. // 检查标签栈中是否有着重点样式
  846. checkHasEmphasisDot(tagStack) {
  847. return tagStack.some((tagItem) => {
  848. if (!tagItem.className) return false;
  849. return tagItem.className.includes('rich-text-emphasis-dot');
  850. });
  851. },
  852. // 创建文本块
  853. createTextBlock(item, tagStack, textBlockIndex, oldIndex, paragraphIndex, hasEmphasisDot) {
  854. const combinedStyle = tagStack
  855. .filter((tagItem) => tagItem.style)
  856. .map((tagItem) => tagItem.style)
  857. .join(';');
  858. const styleObj = combinedStyle
  859. ? this.parseStyleToObject(combinedStyle)
  860. : {
  861. fontSize:
  862. this.data.unified_attrib && this.data.unified_attrib.font_size ? this.data.unified_attrib.font_size : '',
  863. fontFamily: this.data.unified_attrib && this.data.unified_attrib.font ? this.data.unified_attrib.font : '',
  864. };
  865. return {
  866. type: 'text',
  867. word_list: item.word_list,
  868. index: textBlockIndex,
  869. oldIndex,
  870. paragraphIndex,
  871. styleObj,
  872. hasEmphasisDot,
  873. };
  874. },
  875. // 拼音固定为拼音字体,跟随汉字的字号、颜色、粗细的样式
  876. getPinyinStyle(item) {
  877. const styles = {};
  878. // 固定使用 League 字体
  879. styles['font-family'] = 'League';
  880. styles['font-size'] =
  881. this.data.unified_attrib && this.data.unified_attrib.pinyin_size ? this.data.unified_attrib.pinyin_size : '';
  882. // 只继承字号和颜色
  883. if (item.activeTextStyle) {
  884. if (item.activeTextStyle.fontSize) {
  885. styles['font-size'] = item.activeTextStyle.fontSize;
  886. }
  887. if (item.activeTextStyle.color) {
  888. styles['color'] = item.activeTextStyle.color;
  889. }
  890. }
  891. // 如果设置了全局字号,优先使用全局字号
  892. if (this.isAllSetting && this.fontSize) {
  893. styles['font-size'] = this.fontSize;
  894. this.isAllSetting = false;
  895. }
  896. // 明确重置不应该继承的样式
  897. styles['text-decoration'] = 'none';
  898. styles['font-style'] = 'normal';
  899. styles['letter-spacing'] = '0';
  900. return styles;
  901. },
  902. // 获取单个字符的样式(包括着重点)
  903. getCharStyle(word, block, charIndex) {
  904. const baseStyle = { ...word.activeTextStyle };
  905. baseStyle['font-size'] = baseStyle.fontSize;
  906. baseStyle['font-family'] = baseStyle.fontFamily;
  907. if (this.isAllSetting) {
  908. baseStyle['font-size'] = this.fontSize;
  909. baseStyle['font-family'] = this.fontFamily;
  910. this.isAllSetting = false;
  911. }
  912. // 如果该文字块有着重点标记,应用到字符上
  913. if (block.hasEmphasisDot) {
  914. const letterSpacing = block.styleObj?.letterSpacing || '0';
  915. const letterSpacingValue = parseFloat(letterSpacing) || 0;
  916. baseStyle['border-bottom'] = 'none';
  917. baseStyle['background-image'] = 'radial-gradient(circle at center, currentColor 0.15em, transparent 0.16em)';
  918. baseStyle['background-repeat'] = 'repeat-x';
  919. baseStyle['background-position'] = `${letterSpacingValue * -0.5}em 100%`;
  920. baseStyle['background-size'] = `${1 + letterSpacingValue}em 0.3em`;
  921. baseStyle['padding-bottom'] = '0.3em';
  922. baseStyle['display'] = 'inline';
  923. }
  924. return baseStyle;
  925. },
  926. // input的高度
  927. getInputStyle(styles, words) {
  928. const baseStyle = {};
  929. baseStyle['font-size'] = styles ? styles.fontSize : '16px';
  930. baseStyle['--input-font-family'] = styles ? styles.fontFamily : '';
  931. baseStyle['width'] = Math.max(40, words.value.length * 21.3) + 'px';
  932. baseStyle['height'] = styles && styles.fontSize ? styles.fontSize.replace('pt', '') * 1.5 + 'pt' : '24px';
  933. return baseStyle;
  934. },
  935. // 如果汉字有字间距,拼音与汉字左对齐,如果没有则居中对齐
  936. getWordAlignItems(word, block) {
  937. const letterSpacing = block.styleObj?.letterSpacing;
  938. if (letterSpacing && letterSpacing !== '0' && letterSpacing !== '0px') {
  939. return 'flex-start';
  940. }
  941. return 'center';
  942. },
  943. // 提取标签信息(样式和类名)
  944. extractTagInfo(attrs, tagName) {
  945. const styleMatch = attrs.match(/style=["']([^"']*)["']/);
  946. let style = styleMatch ? styleMatch[1] : null;
  947. const classMatch = attrs.match(/class=["']([^"']*)["']/);
  948. const className = classMatch ? classMatch[1] : '';
  949. if (!style) {
  950. style = this.getDefaultTagStyle(tagName);
  951. }
  952. return { style, className };
  953. },
  954. // 获取标签默认样式
  955. getDefaultTagStyle(tagName) {
  956. const tagStyleMap = {
  957. strong: 'font-weight: bold',
  958. b: 'font-weight: bold',
  959. em: 'font-style: italic',
  960. i: 'font-style: italic',
  961. u: 'text-decoration: underline',
  962. s: 'text-decoration: line-through',
  963. del: 'text-decoration: line-through',
  964. sub: 'vertical-align: sub',
  965. sup: 'vertical-align: super',
  966. };
  967. return tagStyleMap[tagName] || null;
  968. },
  969. // 解析样式字符串为对象
  970. parseStyleToObject(styleStr) {
  971. const styleObj = {};
  972. if (!styleStr) return styleObj;
  973. this.mergeStyleString(styleObj, styleStr);
  974. return styleObj;
  975. },
  976. // 解析视频块
  977. parseVideoBlock(item, tagStack) {
  978. const videoMatch = item.text.match(/<video\s+([^>]*)>/i);
  979. if (!videoMatch) return null;
  980. const attrs = videoMatch[1];
  981. const posterMatch = attrs.match(/poster=["']([^"']*)["']/i);
  982. const widthMatch = attrs.match(/width=["']?(\d+)["']?/i);
  983. const heightMatch = attrs.match(/height=["']?(\d+)["']?/i);
  984. const styleMatch = attrs.match(/style=["']([^"']*)["']/i);
  985. // 直接从完整的文本中匹配 source
  986. const sourceMatch = item.text.match(/<source\s+([^>]*)\/?\s*>/i);
  987. let src = '';
  988. let videoType = '';
  989. if (sourceMatch) {
  990. const sourceAttrs = sourceMatch[1];
  991. const srcMatch = sourceAttrs.match(/src\s*=\s*["']?([^"'\s>]+)["']?/i);
  992. const typeMatch = sourceAttrs.match(/type\s*=\s*["']?([^"'\s>]+)["']?/i);
  993. src = srcMatch ? srcMatch[1] : '';
  994. videoType = typeMatch ? typeMatch[1] : '';
  995. }
  996. const containerStyleObj = {};
  997. tagStack.forEach((tagItem) => {
  998. if (tagItem.style) {
  999. this.mergeStyleString(containerStyleObj, tagItem.style);
  1000. }
  1001. });
  1002. const mediaStyleObj = {};
  1003. if (styleMatch) {
  1004. this.mergeStyleString(mediaStyleObj, styleMatch[1]);
  1005. }
  1006. return {
  1007. type: 'video',
  1008. poster: posterMatch ? posterMatch[1] : '',
  1009. width: widthMatch ? widthMatch[1] : null,
  1010. height: heightMatch ? heightMatch[1] : null,
  1011. src,
  1012. videoType,
  1013. containerStyle: containerStyleObj,
  1014. mediaStyle: mediaStyleObj,
  1015. };
  1016. },
  1017. // 解析音频块
  1018. parseAudioBlock(item, tagStack) {
  1019. const audioMatch = item.text.match(/<audio\s+([^>]*)>/i);
  1020. if (!audioMatch) return null;
  1021. const attrs = audioMatch[1];
  1022. const srcMatch = attrs.match(/src=["']([^"']*)["']/i);
  1023. const styleMatch = attrs.match(/style=["']([^"']*)["']/i);
  1024. const containerStyleObj = {};
  1025. tagStack.forEach((tagItem) => {
  1026. if (tagItem.style) {
  1027. this.mergeStyleString(containerStyleObj, tagItem.style);
  1028. }
  1029. });
  1030. const mediaStyleObj = {};
  1031. if (styleMatch) {
  1032. this.mergeStyleString(mediaStyleObj, styleMatch[1]);
  1033. }
  1034. return {
  1035. type: 'audio',
  1036. src: srcMatch ? srcMatch[1] : '',
  1037. containerStyle: containerStyleObj,
  1038. mediaStyle: mediaStyleObj,
  1039. };
  1040. },
  1041. // 合并样式字符串到对象
  1042. mergeStyleString(styleObj, styleStr) {
  1043. styleStr.split(';').forEach((rule) => {
  1044. if (rule.trim()) {
  1045. const [prop, value] = rule.split(':').map((s) => s.trim());
  1046. if (prop && value) {
  1047. const camelProp = prop.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
  1048. styleObj[camelProp] = value;
  1049. }
  1050. }
  1051. });
  1052. },
  1053. removeTagFromStack(tagStack, tagName) {
  1054. for (let i = tagStack.length - 1; i >= 0; i--) {
  1055. if (tagStack[i].tag === tagName) {
  1056. tagStack.splice(i, 1);
  1057. break;
  1058. }
  1059. }
  1060. },
  1061. toggle(status) {
  1062. const index = this.statusList.findIndex((item) => status.crossAnswer === item);
  1063. status.crossAnswer = index === this.statusList.length - 1 ? this.statusList[0] : this.statusList[index + 1];
  1064. },
  1065. handleData() {
  1066. this.table_width = 0;
  1067. this.isHasInput = false;
  1068. this.data.col_width.forEach((item) => {
  1069. this.table_width += Number(item.value);
  1070. });
  1071. this.data.multilingual.forEach((item) => {
  1072. let trans_arr = item.translation.split('\n');
  1073. let chunkSize = this.data.property.column_count;
  1074. let chunkedArr = trans_arr.reduce((acc, curr, index) => {
  1075. // 当索引是chunkSize的倍数时,开始一个新的子数组
  1076. if (index % chunkSize === 0) {
  1077. acc.push([curr]); // 开始新的子数组并添加当前元素
  1078. } else {
  1079. acc[acc.length - 1].push(curr); // 将当前元素添加到最后一个子数组中
  1080. }
  1081. return acc;
  1082. }, []);
  1083. this.$set(this.multilingualTextList, item.type, chunkedArr);
  1084. });
  1085. if (!this.isJudgingRightWrong) {
  1086. this.answer.answer_list = this.data.answer_lists;
  1087. }
  1088. this.data.option_list.forEach((item, index) => {
  1089. item.forEach((items, indexs) => {
  1090. items.model_essay.forEach((li) => {
  1091. if (li.type === 'input') {
  1092. this.isHasInput = true;
  1093. }
  1094. });
  1095. if (items.rich_text_list) {
  1096. this.data.option_list[index][indexs].rich_text_list = this.parsedBlocks(items);
  1097. }
  1098. });
  1099. });
  1100. },
  1101. // 计算单元格是否显示
  1102. computedTableCellShow() {
  1103. this.data.option_list.forEach((item, i, arr) => {
  1104. item.forEach(({ cell, is_show_cell }, j, arr2) => {
  1105. let col = cell.colspan === undefined ? 1 : Number(cell.colspan);
  1106. let row = cell.rowspan === undefined ? 1 : Number(cell.rowspan);
  1107. if (col > 1 && row === 1) {
  1108. for (let k = 1; k < col; k++) {
  1109. this.$set(arr[i][j + k], 'is_show_cell', false);
  1110. }
  1111. }
  1112. if (row > 1 && col === 1) {
  1113. for (let k = 1; k < row; k++) {
  1114. this.$set(arr[i + k][j], 'is_show_cell', false);
  1115. }
  1116. }
  1117. if (row > 1 && col > 1) {
  1118. for (let k = 1; k < row; k++) {
  1119. this.$set(arr[i + k][j], 'is_show_cell', false);
  1120. for (let l = 1; l < col; l++) {
  1121. this.$set(arr[i + k][j + l], 'is_show_cell', false);
  1122. this.$set(arr[i][j + l], 'is_show_cell', false);
  1123. }
  1124. }
  1125. }
  1126. if (is_show_cell === undefined) {
  1127. arr2[j].is_show_cell = true;
  1128. }
  1129. });
  1130. });
  1131. },
  1132. computedAnswerText(item, i, j) {
  1133. if (!this.isShowRightAnswer) return '';
  1134. let answerOption =
  1135. this.data.answer_lists[i] && this.data.answer_lists[i][j] ? this.data.answer_lists[i][j].answer : '';
  1136. let answerOptionList = answerOption.split('\n');
  1137. if (!item) return '';
  1138. let answerValue = answerOptionList[item.inputIndex] ? answerOptionList[item.inputIndex].split('/') : '';
  1139. if (!answerValue) return '';
  1140. return `${answerValue.join('/')}`;
  1141. },
  1142. /**
  1143. * 计算答题对错选项字体颜色
  1144. * @param {string} mark 选项标识
  1145. */
  1146. computedAnswerClass(item, i, j) {
  1147. if (!this.isJudgingRightWrong && !this.isShowRightAnswer) {
  1148. return '';
  1149. }
  1150. let answerOption =
  1151. this.data.answer_lists[i] && this.data.answer_lists[i][j] ? this.data.answer_lists[i][j].answer : '';
  1152. let answerOptionList = answerOption.split('\n');
  1153. if (!item) return '';
  1154. let selectValue = item.value ? item.value.trim() : '';
  1155. let answerValue = answerOptionList[item.inputIndex] ? answerOptionList[item.inputIndex].split('/') : '';
  1156. let classList = [];
  1157. let isRight = answerValue && answerValue.includes(selectValue);
  1158. if (this.isJudgingRightWrong && answerValue) {
  1159. isRight ? classList.push('right') : classList.push('wrong');
  1160. }
  1161. if (this.isShowRightAnswer && !isRight) {
  1162. classList.push('show-right-answer');
  1163. }
  1164. return classList;
  1165. },
  1166. computedAnswerCrossClass(item, i, j) {
  1167. if (!this.isJudgingRightWrong && !this.isShowRightAnswer) {
  1168. return '';
  1169. }
  1170. let answerOption =
  1171. this.data.answer_lists[i] && this.data.answer_lists[i][j] ? this.data.answer_lists[i][j].crossAnswer : '';
  1172. if (!item) return '';
  1173. let selectValue = item.crossAnswer;
  1174. let classList = [];
  1175. let isRight = answerOption === selectValue;
  1176. if (this.isJudgingRightWrong && answerOption) {
  1177. isRight ? classList.push('right') : classList.push('wrong');
  1178. }
  1179. if (this.isShowRightAnswer && !isRight) {
  1180. classList.push('show-right-answer');
  1181. }
  1182. return classList;
  1183. },
  1184. /**
  1185. * 处理小音频录音
  1186. * @param {Object} data 音频数据
  1187. * @param {String} mark 选项标识
  1188. */
  1189. handleMiniWav(data, mark) {
  1190. if (!data || !mark) return;
  1191. this.$set(mark, 'audio_answer_list', data);
  1192. },
  1193. /**
  1194. * 处理选中词汇
  1195. * @param {String} content 选中的词汇内容
  1196. * @param {String} mark 选项标识
  1197. * @param {Object} li 当前输入框对象
  1198. */
  1199. handleSelectWord(content, mark, li) {
  1200. if (!content || !mark || !li || this.disabled) return;
  1201. li.value = content;
  1202. this.selectedWordList.push(mark);
  1203. },
  1204. /**
  1205. * 处理书写区确认
  1206. * @param {String} data 书写区数据
  1207. */
  1208. handleWriteConfirm(data) {
  1209. if (!data) return;
  1210. this.$set(this.writeMark, 'write_base64', data);
  1211. },
  1212. handleWriteClick(mark) {
  1213. this.writeVisible = true;
  1214. this.writeMark = mark;
  1215. },
  1216. computedRichStyle(content) {
  1217. // if (this.data.mode === 'short') return {};
  1218. if (!content) return {};
  1219. // 提取首个标签的 style 样式属性
  1220. let styleMatch = content.match(/<\w+\s+[^>]*style=["']([^"']*)["'][^>]*>/i);
  1221. if (styleMatch && styleMatch[1]) {
  1222. let styleString = styleMatch[1];
  1223. let styleArray = styleString.split(';').filter((s) => s.trim() !== '');
  1224. let styleObject = {};
  1225. styleArray.forEach((style) => {
  1226. let [key, value] = style.split(':');
  1227. if (key && value) {
  1228. styleObject[key.trim()] = value.trim();
  1229. }
  1230. });
  1231. if (styleObject.hasOwnProperty('text-align') && styleObject['text-align'] === 'center') {
  1232. this.$set(styleObject, 'justify-content', 'center');
  1233. }
  1234. return styleObject;
  1235. }
  1236. return {};
  1237. },
  1238. // 重做
  1239. retry() {
  1240. this.data.option_list.forEach((item) => {
  1241. item.forEach((items) => {
  1242. items.model_essay.forEach((li) => {
  1243. if (li.type === 'input') {
  1244. li.value = '';
  1245. li.write_base64 = '';
  1246. }
  1247. });
  1248. });
  1249. });
  1250. if (this.$refs.record) {
  1251. if (this.$refs.record.length > 0) {
  1252. this.$refs.record.forEach((item) => {
  1253. item.handleReset();
  1254. });
  1255. } else {
  1256. this.$refs.record.handleReset();
  1257. }
  1258. }
  1259. this.isJudgingRightWrong = false;
  1260. this.isShowRightAnswer = false;
  1261. },
  1262. /**
  1263. * 获取无文本内容的数据结构,用于保存为个人模板时的样式模板
  1264. */
  1265. getNoTextContentData() {
  1266. let noTextContentData = JSON.parse(JSON.stringify(this.data));
  1267. const resetFieldMap = {
  1268. record_list: [],
  1269. vocabulary: '', // 用于选词的词汇
  1270. word_list: [], // 选词列表
  1271. has_identify: 'false', // 是否已识别
  1272. paragraph_list: [],
  1273. paragraph_list_parameter: {
  1274. text: '',
  1275. is_first_sentence_first_hz_pinyin_first_char_upper_case: 'true',
  1276. pinyin_proofread_word_list: [],
  1277. },
  1278. analysis_list: [],
  1279. answer_list: [],
  1280. };
  1281. Object.assign(noTextContentData, resetFieldMap);
  1282. noTextContentData.answer_lists.forEach((item) => {
  1283. item.answer = '';
  1284. item.answer_list = [];
  1285. });
  1286. noTextContentData.option_list.forEach((item) => {
  1287. item.content = '';
  1288. item.model_essay = [];
  1289. });
  1290. if (noTextContentData.answer) {
  1291. noTextContentData.answer.answer_list = [];
  1292. noTextContentData.answer.reference_answer = '';
  1293. }
  1294. return noTextContentData;
  1295. },
  1296. },
  1297. };
  1298. </script>
  1299. <style lang="scss" scoped>
  1300. @use '@/styles/mixin.scss' as *;
  1301. $select-color: #1890ff;
  1302. $border-color: #e6e6e6;
  1303. .table-preview {
  1304. @include preview-base;
  1305. .main {
  1306. color: #262626;
  1307. .table-box {
  1308. margin: 0 auto;
  1309. overflow: auto;
  1310. }
  1311. table {
  1312. // border-spacing: 3px;
  1313. border-collapse: separate;
  1314. }
  1315. .cell-wrap {
  1316. display: flex;
  1317. flex-flow: wrap;
  1318. grid-area: fill;
  1319. // align-items: end;
  1320. align-items: center; // 为了勾选框居中对齐
  1321. &-content {
  1322. display: flex;
  1323. flex: 1;
  1324. flex-flow: wrap;
  1325. grid-area: fill;
  1326. align-items: end;
  1327. }
  1328. p {
  1329. flex-grow: 1; // 为了常规模式时设置居中显示撑满整个单元格
  1330. max-width: 100%;
  1331. margin: 0;
  1332. }
  1333. .record-box {
  1334. display: inline-flex;
  1335. align-items: center;
  1336. background-color: #fff;
  1337. // border-bottom: 1px solid $font-color;
  1338. :deep(.playBack) {
  1339. width: 20px;
  1340. height: 20px;
  1341. }
  1342. }
  1343. .write-click {
  1344. display: inline-block;
  1345. width: 104px;
  1346. height: 32px;
  1347. padding: 0 4px;
  1348. vertical-align: bottom;
  1349. cursor: pointer;
  1350. border-bottom: 1px solid $font-color;
  1351. img {
  1352. width: 100%;
  1353. height: 100%;
  1354. }
  1355. }
  1356. .el-input {
  1357. display: inline-flex;
  1358. align-items: center;
  1359. width: 120px;
  1360. margin: 0 2px;
  1361. font-size: 16px;
  1362. &.pinyin :deep input.el-input__inner {
  1363. font-family: 'PINYIN-B', sans-serif;
  1364. }
  1365. &.chinese :deep input.el-input__inner {
  1366. font-family: 'arial', sans-serif;
  1367. }
  1368. &.english :deep input.el-input__inner {
  1369. font-family: 'arial', sans-serif;
  1370. }
  1371. &.right {
  1372. :deep input.el-input__inner {
  1373. color: $right-color;
  1374. }
  1375. }
  1376. &.wrong {
  1377. :deep input.el-input__inner {
  1378. color: $error-color;
  1379. }
  1380. }
  1381. :deep input.el-input__inner {
  1382. padding: 0;
  1383. font-family: var(--input-font-family);
  1384. // font-size: 16pt;
  1385. color: $font-color;
  1386. text-align: center;
  1387. background-color: transparent;
  1388. border-width: 0;
  1389. border-bottom: 1px solid $font-color;
  1390. border-radius: 0;
  1391. }
  1392. }
  1393. .right {
  1394. border-color: $right-color !important;
  1395. }
  1396. .wrong {
  1397. border-color: $error-color !important;
  1398. }
  1399. &-normal {
  1400. p {
  1401. width: 100%; // 为了解决换行问题
  1402. }
  1403. }
  1404. }
  1405. .right-answer {
  1406. position: relative;
  1407. left: 0;
  1408. display: inline-block;
  1409. min-height: 24px;
  1410. line-height: 24px;
  1411. vertical-align: bottom;
  1412. border-bottom: 1px solid $font-color;
  1413. }
  1414. .multilingual {
  1415. display: block;
  1416. word-break: break-word;
  1417. }
  1418. .pinyin-text {
  1419. display: flex;
  1420. flex-direction: column;
  1421. padding: 0 1px;
  1422. font-size: 16px;
  1423. span {
  1424. // text-align: center; // 注释掉是为了带拼音分词后折行左对齐显示了居中对齐
  1425. }
  1426. .pinyin {
  1427. height: 18px;
  1428. font-family: 'League';
  1429. font-size: 12px;
  1430. }
  1431. &-left {
  1432. span {
  1433. // text-align: left;
  1434. }
  1435. }
  1436. }
  1437. .cell-wrap-content {
  1438. p {
  1439. display: inline-flex;
  1440. flex-wrap: wrap;
  1441. margin: 0.5em 0;
  1442. }
  1443. span {
  1444. display: inline-flex;
  1445. flex-wrap: wrap;
  1446. }
  1447. .pinyin-sentence {
  1448. display: inline;
  1449. white-space: normal;
  1450. }
  1451. .image-container {
  1452. display: block;
  1453. .inline-image {
  1454. display: inline-block;
  1455. max-width: 100%;
  1456. height: auto;
  1457. }
  1458. }
  1459. .video-container {
  1460. display: inline-block;
  1461. margin: 26px 5px 26px 0;
  1462. .inline-video {
  1463. display: inline-block;
  1464. max-width: 100%;
  1465. height: auto;
  1466. }
  1467. }
  1468. .audio-container {
  1469. display: inline-block;
  1470. .inline-audio {
  1471. display: inline-block;
  1472. width: 100%;
  1473. max-width: 500px;
  1474. }
  1475. }
  1476. .pinyin-text-audio {
  1477. width: 100%;
  1478. }
  1479. }
  1480. :deep .el-input--small .el-input__inner {
  1481. height: 100%;
  1482. line-height: 1;
  1483. }
  1484. :deep audio,
  1485. :deep video {
  1486. max-width: 100%;
  1487. }
  1488. }
  1489. }
  1490. .word-list {
  1491. display: flex;
  1492. flex-wrap: wrap;
  1493. gap: 8px;
  1494. align-items: center;
  1495. .word-item {
  1496. cursor: pointer;
  1497. }
  1498. }
  1499. </style>