Matching.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  1. <template>
  2. <ModuleBase ref="base" :type="data.type">
  3. <template #content>
  4. <div v-loading="loading">
  5. <ul class="option-list">
  6. <li v-for="(li, i) in data.option_list" :key="i" class="option-item">
  7. <div v-for="(item, j) in li" :key="item.mark" class="option">
  8. <span class="serial-number">{{ computeOptionNumber(i, data.property.serial_number_type_list[j]) }}</span>
  9. <span class="option-content">
  10. <RichText
  11. v-if="property.isGetContent"
  12. ref="richText"
  13. v-model="item.content"
  14. placeholder="请输入"
  15. :inline="true"
  16. :height="32"
  17. :font-size="data?.unified_attrib?.font_size"
  18. :font-family="data?.unified_attrib?.font"
  19. :font-color="data?.unified_attrib?.text_color"
  20. :is-view-pinyin="isEnable(data.property.view_pinyin)"
  21. @createParsedTextInfoPinyin="handleParsedTextPinyin($event, i, j)"
  22. />
  23. </span>
  24. <span class="multilingual" @click="openMultilingual(i, j)">
  25. <SvgIcon icon-class="multilingual" class-name="multilingual" width="12" height="12" />
  26. </span>
  27. </div>
  28. </li>
  29. </ul>
  30. <div class="answer-tips">答案:</div>
  31. <ul class="answer-list">
  32. <li v-for="(li, i) in data.answer.answer_list" :key="i" class="answer-item">
  33. <template v-for="(item, j) in li">
  34. <span v-if="j === 0" :key="`zero-${j}-${item.mark}`">{{ answerList[j][i].label }}</span>
  35. <el-select
  36. v-if="j === 1 || j === 2"
  37. :key="`select-${j}`"
  38. v-model="item.mark"
  39. multiple
  40. placeholder="请选择"
  41. >
  42. <el-option
  43. v-for="answer in answerList[j]"
  44. :key="answer.value"
  45. :label="answer.label"
  46. :value="answer.value"
  47. />
  48. </el-select>
  49. <span v-if="j === 1 && data.property.column_num === 3" :key="`one-${j}-${item.mark}`">
  50. {{ answerList[j][i].label }}
  51. </span>
  52. </template>
  53. </li>
  54. </ul>
  55. <el-divider v-if="isEnable(data.property.view_pinyin)" content-position="left">
  56. <span>拼音效果</span>
  57. <el-button
  58. v-show="isEnable(data.property.view_pinyin)"
  59. type="text"
  60. icon="el-icon-refresh"
  61. title="刷新"
  62. class="refresh-pinyin-btn"
  63. @click.native="handleViewPinyin(true)"
  64. />
  65. </el-divider>
  66. <div v-show="isEnable(data.property.view_pinyin)" class="proofread-table-wrapper">
  67. <table class="proofread-table">
  68. <tbody>
  69. <template v-for="(row, i) in data.option_list">
  70. <tr v-for="(item, j) in row" :key="item.mark">
  71. <td class="pinyin-cell">
  72. <PinyinText
  73. ref="PinyinText"
  74. :paragraph-list="item.paragraph_list"
  75. :rich-text-list="item.rich_text_list"
  76. :pinyin-position="data.property.pinyin_position"
  77. @fillCorrectPinyin="fillCorrectPinyin($event, i, j)"
  78. />
  79. </td>
  80. <td class="proofread-cell">
  81. <WordProofread
  82. button-text="分词校对"
  83. :word-data="getOptionWordData(item)"
  84. @save="saveOptionWord($event, i, j)"
  85. />
  86. </td>
  87. </tr>
  88. </template>
  89. </tbody>
  90. </table>
  91. </div>
  92. </div>
  93. <MultilingualFill
  94. v-if="curSelectRow !== -1 && curSelectColumn !== -1"
  95. :visible.sync="multilingualVisible"
  96. :text="data.option_list[curSelectRow][curSelectColumn].content"
  97. :translations="data.option_list[curSelectRow][curSelectColumn].multilingual"
  98. @SubmitTranslation="handleMultilingualTranslation"
  99. />
  100. <AnswerAnalysisList
  101. v-if="data.answer_list?.length > 0 || data.analysis_list?.length > 0"
  102. :answer-list="data.answer_list"
  103. :analysis-list="data.analysis_list"
  104. :unified-attrib="data.unified_attrib"
  105. @updateAnswerAnalysisFileList="updateAnswerAnalysisFileList"
  106. @deleteAnswerAnalysis="deleteAnswerAnalysis"
  107. />
  108. </template>
  109. </ModuleBase>
  110. </template>
  111. <script>
  112. import ModuleMixin from '../../common/ModuleMixin';
  113. import PinyinText from '@/components/PinyinText.vue';
  114. import WordProofread from '@/views/book/courseware/create/components/common/WordProofread.vue';
  115. import { buildWordProofreadData } from '@/views/book/courseware/create/components/common/wordProofread';
  116. import { getMatchingData, getOption, getOptionItem } from '@/views/book/courseware/data/matching';
  117. import { computeOptionMethods, serialNumberTypeList, computeOptionNumber } from '@/views/book/courseware/data/common';
  118. export default {
  119. name: 'MatchingPage',
  120. components: {
  121. PinyinText,
  122. WordProofread,
  123. },
  124. mixins: [ModuleMixin],
  125. data() {
  126. return {
  127. data: getMatchingData(),
  128. computeOptionMethods,
  129. serialNumberTypeList,
  130. computeOptionNumber,
  131. curSelectRow: -1,
  132. curSelectColumn: -1,
  133. optionWordDataMap: {},
  134. optionWordDataSourceMap: {},
  135. loading: false,
  136. inited: false,
  137. // 用于记录 data.option_list 中 content 的签名
  138. optionContentSignature: '',
  139. };
  140. },
  141. computed: {
  142. answerList() {
  143. let list = Array.from({ length: this.data.property.column_num }, () => []);
  144. for (let i = 0; i < this.data.option_list.length; i++) {
  145. for (let j = 0; j < this.data.option_list[i].length; j++) {
  146. list[j].push({
  147. label: computeOptionMethods[this.data.property.serial_number_type_list[j]](i),
  148. value: this.data.option_list[i][j].mark,
  149. });
  150. }
  151. }
  152. return list;
  153. },
  154. },
  155. watch: {
  156. 'data.property.column_num': [
  157. {
  158. handler(val) {
  159. let optionNum = this.data.option_list[0].length;
  160. if (val > optionNum) {
  161. // 修改序号类型列表
  162. this.data.property.serial_number_type_list.push(
  163. serialNumberTypeList[val]?.value || serialNumberTypeList[0].value,
  164. );
  165. // 增加选项
  166. for (let i = 0; i < this.data.option_list.length; i++) {
  167. this.data.option_list[i].push(getOptionItem());
  168. }
  169. // 修改答案列表
  170. this.data.answer.answer_list.forEach((li) => {
  171. li.push({ mark: [] });
  172. });
  173. return;
  174. }
  175. if (val < optionNum) {
  176. this.data.property.serial_number_type_list.splice(-1, 1);
  177. this.data.option_list.forEach((li) => {
  178. li.splice(-1, 1);
  179. });
  180. // 修改答案列表
  181. this.data.answer.answer_list.forEach((li) => {
  182. li.splice(-1, 1);
  183. });
  184. }
  185. },
  186. },
  187. 'handlerMindMap',
  188. ],
  189. 'data.property.row_num': [
  190. {
  191. handler(val) {
  192. let optionNum = this.data.option_list.length;
  193. if (val > optionNum) {
  194. for (let i = 0; i < val - optionNum; i++) {
  195. this.data.option_list.push(getOption(this.data.property.column_num));
  196. }
  197. // 增加答案列表
  198. this.data.answer.answer_list.push(
  199. Array.from({ length: this.data.property.column_num }, () => ({ mark: [] })),
  200. );
  201. // 将答案列表最后一项的第一个元素设置进答案列表第一项
  202. this.data.answer.answer_list[this.data.answer.answer_list.length - 1][0] = {
  203. mark: this.data.option_list[this.data.option_list.length - 1][0].mark,
  204. };
  205. return;
  206. }
  207. if (val < optionNum) {
  208. this.data.option_list.splice(val);
  209. this.data.answer.answer_list.splice(val);
  210. }
  211. },
  212. },
  213. 'handlerMindMap',
  214. ],
  215. 'data.option_list': {
  216. handler(newVal) {
  217. if (this.loading) return;
  218. if (this.inited || !Array.isArray(newVal) || newVal.length <= 0) return;
  219. if (!this.isEnable(this.data.property.view_pinyin)) return;
  220. this.ensureOptionWordProofreadData();
  221. this.inited = true;
  222. },
  223. deep: true,
  224. },
  225. 'data.property.view_pinyin': {
  226. handler(val) {
  227. if (this.property.isSilentPropertyWatch) return;
  228. if (!this.isEnable(val)) {
  229. this.data.option_list.forEach((row) => {
  230. row.forEach((item) => {
  231. item.paragraph_list = [];
  232. item.paragraph_list_parameter = {
  233. text: '',
  234. pinyin_proofread_word_list: [],
  235. };
  236. });
  237. });
  238. this.optionWordDataMap = {};
  239. this.optionWordDataSourceMap = {};
  240. return;
  241. }
  242. this.handleViewPinyin().finally(() => {
  243. this.inited = true;
  244. });
  245. },
  246. deep: true,
  247. },
  248. },
  249. methods: {
  250. ensureOptionWordProofreadData() {
  251. this.data.option_list.forEach((row) => {
  252. row.forEach((option) => {
  253. if (!option?.mark) return;
  254. const wordData = { detail: [{ sentences: [], segList: [] }] };
  255. option.rich_text_list?.forEach((item) => {
  256. const sentence = (item.word_list || []).reduce((acc, itemss) => `${acc + itemss.text}`, '');
  257. const seg = (item.word_list || []).map((itemss) => itemss.text);
  258. wordData.detail[0].sentences.push(sentence);
  259. wordData.detail[0].segList.push(seg);
  260. });
  261. this.$set(this.optionWordDataMap, option.mark, wordData);
  262. this.$set(this.optionWordDataSourceMap, option.mark, option.content);
  263. });
  264. });
  265. },
  266. async handleViewPinyin(isPrompt = false) {
  267. if (isPrompt) {
  268. try {
  269. await this.$confirm('刷新将会重置分词和拼音,是否刷新?', '提示', {
  270. confirmButtonText: '确定',
  271. cancelButtonText: '取消',
  272. type: 'warning',
  273. });
  274. } catch (e) {
  275. return;
  276. }
  277. }
  278. this.loading = true;
  279. try {
  280. if (!this.isEnable(this.data.property.view_pinyin)) {
  281. this.data.option_list.forEach((row) => {
  282. row.forEach((item) => {
  283. item.paragraph_list = [];
  284. item.paragraph_list_parameter = {
  285. text: '',
  286. pinyin_proofread_word_list: [],
  287. };
  288. });
  289. });
  290. this.optionWordDataMap = {};
  291. this.optionWordDataSourceMap = {};
  292. return;
  293. }
  294. const tasks = [];
  295. this.data.option_list.forEach((row, i) => {
  296. row.forEach((item, j) => {
  297. const text = item.content;
  298. if (!text) return;
  299. item.paragraph_list_parameter.text = text;
  300. tasks.push(
  301. this.createParsedTextInfoPinyin(text, i, j).then(({ parsed_text }) => {
  302. this.$set(this.optionWordDataMap, item.mark, buildWordProofreadData(parsed_text?.paragraph_list || []));
  303. this.$set(this.optionWordDataSourceMap, item.mark, text);
  304. }),
  305. );
  306. });
  307. });
  308. await Promise.all(tasks);
  309. } finally {
  310. this.loading = false;
  311. }
  312. },
  313. getOptionWordData(option) {
  314. if (!option || !option.mark) return { detail: [] };
  315. return this.optionWordDataMap[option.mark] || { detail: [] };
  316. },
  317. async saveOptionWord(saveArr, rowIndex, colIndex) {
  318. const option = this.data.option_list[rowIndex]?.[colIndex];
  319. if (!option) return;
  320. const { parsed_text } = await this.createParsedTextInfoPinyin(
  321. option.content,
  322. rowIndex,
  323. colIndex,
  324. 'option_list',
  325. saveArr,
  326. );
  327. this.$set(this.optionWordDataMap, option.mark, buildWordProofreadData(parsed_text?.paragraph_list || []));
  328. this.$set(this.optionWordDataSourceMap, option.mark, option.content || '');
  329. },
  330. /**
  331. * @description 处理思维导图
  332. */
  333. handlerMindMap() {
  334. this.data.mind_map.node_list = [
  335. { name: `${this.data.property.row_num}行${this.data.property.column_num}列连线组件` },
  336. ];
  337. },
  338. openMultilingual(i, j) {
  339. this.curSelectRow = i;
  340. this.curSelectColumn = j;
  341. this.$nextTick(() => {
  342. this.multilingualVisible = true;
  343. });
  344. },
  345. handleMultilingualTranslation(translations) {
  346. this.$set(this.data.option_list[this.curSelectRow][this.curSelectColumn], 'multilingual', translations);
  347. this.curSelectColumn = -1;
  348. this.curSelectRow = -1;
  349. },
  350. handleParsedTextPinyin(text, i, j) {
  351. if (!this.isEnable(this.data.property.view_pinyin)) return;
  352. if (this.data.option_list?.length > 0 && this.data.option_list[0].paragraph_list?.length > 0) return;
  353. if (!text) return;
  354. this.data.option_list[i][j].paragraph_list_parameter.text = text.replace(/<[^>]+>/g, '').replace(/&nbsp;/g, ' ');
  355. this.createParsedTextInfoPinyin(text, i, j);
  356. },
  357. },
  358. };
  359. </script>
  360. <style lang="scss" scoped>
  361. @use '@/styles/mixin.scss' as *;
  362. .option-list {
  363. display: flex;
  364. flex-direction: column;
  365. row-gap: 16px;
  366. .option-item {
  367. display: flex;
  368. column-gap: 16px;
  369. .option {
  370. display: flex;
  371. flex: 1;
  372. column-gap: 4px;
  373. align-items: center;
  374. .serial-number {
  375. @include serial-number(32px);
  376. }
  377. .option-content {
  378. flex: 1;
  379. padding: 0 4px;
  380. margin-left: 4px;
  381. background-color: $fill-color;
  382. }
  383. .multilingual {
  384. margin-left: 2px;
  385. line-height: 12px;
  386. cursor: pointer;
  387. }
  388. }
  389. }
  390. }
  391. .answer-tips {
  392. margin: 16px 0;
  393. }
  394. .answer-list {
  395. display: flex;
  396. flex-direction: column;
  397. row-gap: 16px;
  398. .answer-item {
  399. display: flex;
  400. column-gap: 16px;
  401. align-items: center;
  402. span {
  403. display: inline-block;
  404. height: 32px;
  405. padding: 4px 8px;
  406. color: #c9cdd4;
  407. background-color: $fill-color;
  408. }
  409. .el-select {
  410. flex: 1;
  411. }
  412. }
  413. }
  414. .proofread-table-wrapper {
  415. margin-top: 8px;
  416. }
  417. .proofread-table {
  418. width: 100%;
  419. border-collapse: collapse;
  420. td {
  421. padding: 8px;
  422. vertical-align: top;
  423. border: 1px solid #ebeef5;
  424. }
  425. .pinyin-cell {
  426. width: calc(100% - 140px);
  427. }
  428. .proofread-cell {
  429. width: 140px;
  430. text-align: center;
  431. white-space: nowrap;
  432. }
  433. }
  434. </style>