DialogueQuestion.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602
  1. <template>
  2. <QuestionBase>
  3. <template #content>
  4. <div class="stem">
  5. <RichText v-model="data.stem" :font-size="18" placeholder="输入题干" />
  6. <RichText
  7. v-if="isEnable(data.property.is_enable_description)"
  8. v-model="data.description"
  9. placeholder="输入提示"
  10. />
  11. </div>
  12. <div class="content-wrapper">
  13. <div class="content">
  14. <div
  15. v-for="({ role, file_id, type, text }, i) in data.option_list"
  16. :key="i"
  17. class="option-list"
  18. :style="{ flexDirection: type === 'input' ? 'row-reverse' : 'row' }"
  19. >
  20. <span
  21. class="avatar"
  22. :style="{
  23. backgroundColor: data.property.role_list.find((item) => item.mark === role).color,
  24. }"
  25. >
  26. {{ data.property.role_list.find((item) => item.mark === role).name }}
  27. </span>
  28. <div v-if="type === 'text'" class="text">{{ text }}</div>
  29. <div v-else-if="type === 'image'" class="image">
  30. <img :src="file_map_list[file_id]" />
  31. </div>
  32. <div v-else-if="type === 'audio'" class="audio">
  33. <AudioPlay
  34. :file-id="file_id"
  35. :show-slider="true"
  36. :show-progress="false"
  37. :background-color="data.property.role_list.find((item) => item.mark === role).color"
  38. />
  39. </div>
  40. <div v-else-if="type === 'input'">
  41. <SoundRecordPreview type="small" :disabled="true" />
  42. </div>
  43. <div class="content-operation" :style="{ flexDirection: type === 'input' ? 'row-reverse' : 'row' }">
  44. <div class="up-down">
  45. <span :style="{ borderBottomColor: i === 0 ? '#c2c2c4' : '#000' }" @click="moveOption('up', i)"></span>
  46. <span
  47. :style="{ borderTopColor: i < data.option_list.length - 1 ? '#000' : '#c2c2c4' }"
  48. @click="moveOption('down', i)"
  49. ></span>
  50. </div>
  51. <SvgIcon icon-class="delete" @click="deleteOption(i)" />
  52. </div>
  53. </div>
  54. </div>
  55. <div class="operation">
  56. <div class="operation-left">
  57. <el-select v-model="curRole" placeholder="请选择">
  58. <el-option
  59. v-for="(item, i) in data.property.role_list"
  60. :key="i"
  61. :label="item.name.length === 0 ? ' ' : item.name"
  62. :value="item.mark"
  63. />
  64. </el-select>
  65. <SoundRecord @saveWav="saveWav" />
  66. </div>
  67. <div class="operation-right">
  68. <el-upload
  69. action="no"
  70. accept="audio/*"
  71. :show-file-list="false"
  72. :http-request="handleAudio"
  73. :before-upload="handleBeforeAudio"
  74. >
  75. <SvgIcon icon-class="music" />
  76. <span>上传音频</span>
  77. </el-upload>
  78. <el-upload
  79. action="no"
  80. accept="image/*"
  81. :show-file-list="false"
  82. :http-request="handleImage"
  83. :before-upload="handleBeforeImage"
  84. >
  85. <SvgIcon icon-class="picture" />
  86. <span>上传图片</span>
  87. </el-upload>
  88. <div class="insert-student" @click="insertStudent">
  89. <SvgIcon icon-class="add-circle" size="14" />
  90. <span>插入学生</span>
  91. </div>
  92. </div>
  93. </div>
  94. <el-input
  95. v-model="textInput"
  96. placeholder="输入"
  97. type="textarea"
  98. resize="none"
  99. :autosize="{ minRows: 3 }"
  100. @keyup.enter.native="handleText"
  101. />
  102. <span class="tips">输入对话内容后请按回车上屏</span>
  103. </div>
  104. <div v-if="isEnable(data.property.is_enable_reference_answer)" class="reference-answer">
  105. <div class="reference-title">参考答案:</div>
  106. <el-input v-model="data.reference_answer" type="textarea" rows="3" placeholder="输入参考答案" />
  107. </div>
  108. <div v-if="isEnable(data.property.is_enable_analysis)" class="analysis">
  109. <div class="analysis-title">解析:</div>
  110. <RichText v-model="data.analysis" :is-border="true" :font-size="14" placeholder="输入解析" />
  111. </div>
  112. </template>
  113. <template #property>
  114. <el-form :model="data.property" label-width="72px" label-position="left">
  115. <el-form-item label="题号">
  116. <el-input v-model="data.property.question_number" />
  117. </el-form-item>
  118. <el-form-item>
  119. <el-radio
  120. v-for="{ value, label } in questionNumberTypeList"
  121. :key="value"
  122. v-model="data.other.question_number_type"
  123. :label="value"
  124. >
  125. {{ label }}
  126. </el-radio>
  127. </el-form-item>
  128. <el-form-item label="题干题号">
  129. <el-select v-model="data.property.stem_question_number_font_size">
  130. <el-option v-for="item in fontSizeList" :key="item" :label="item" :value="item" />
  131. </el-select>
  132. </el-form-item>
  133. <el-form-item label="提示">
  134. <el-radio
  135. v-for="{ value, label } in switchOption"
  136. :key="value"
  137. v-model="data.property.is_enable_description"
  138. :label="value"
  139. >
  140. {{ label }}
  141. </el-radio>
  142. </el-form-item>
  143. <el-form-item label="参考答案">
  144. <el-radio
  145. v-for="{ value, label } in switchOption"
  146. :key="value"
  147. v-model="data.property.is_enable_reference_answer"
  148. :label="value"
  149. >
  150. {{ label }}
  151. </el-radio>
  152. </el-form-item>
  153. <el-form-item label="解析">
  154. <el-radio
  155. v-for="{ value, label } in switchOption"
  156. :key="value"
  157. v-model="data.property.is_enable_analysis"
  158. :label="value"
  159. >
  160. {{ label }}
  161. </el-radio>
  162. </el-form-item>
  163. <el-form-item label="分值">
  164. <el-radio
  165. v-for="{ value, label } in scoreTypeList"
  166. :key="value"
  167. v-model="data.property.score_type"
  168. :label="value"
  169. >
  170. {{ label }}
  171. </el-radio>
  172. </el-form-item>
  173. <el-form-item>
  174. <el-input-number
  175. v-model="data.property.score"
  176. :min="0"
  177. :step="data.property.score_type === scoreTypeList[0].value ? 1 : 0.1"
  178. />
  179. </el-form-item>
  180. <el-form-item label="语音作答">
  181. <el-radio
  182. v-for="{ value, label } in switchOption"
  183. :key="value"
  184. v-model="data.property.is_enable_voice_answer"
  185. :label="value"
  186. >
  187. {{ label }}
  188. </el-radio>
  189. </el-form-item>
  190. <el-form-item label="角色数">
  191. <el-select v-model="data.property.role_number" placeholder="请选择">
  192. <el-option v-for="item in 5" :key="item" :label="item" :value="item" />
  193. </el-select>
  194. </el-form-item>
  195. <el-form-item v-for="(item, i) in data.property.role_list" :key="i" :label="`角色 ${i + 1}`" class="role">
  196. <el-input v-model="item.name" />
  197. <el-color-picker v-model="item.color" />
  198. </el-form-item>
  199. </el-form>
  200. </template>
  201. </QuestionBase>
  202. </template>
  203. <script>
  204. import QuestionMixin from '../common/QuestionMixin.js';
  205. import AudioPlay from '../common/AudioPlay.vue';
  206. import SoundRecord from '@/components/common/SoundRecord.vue';
  207. import SoundRecordPreview from '@/views/exercise_questions/preview/components/common/SoundRecordPreview.vue';
  208. import { getRandomNumber } from '@/utils';
  209. import { fileUpload, GetFileURLMap } from '@/api/app';
  210. import { analysisRecognitionDialogueData, getDialogueData, getRole } from '@/views/exercise_questions/data/dialogue';
  211. export default {
  212. name: 'DialogueQuestion',
  213. components: {
  214. AudioPlay,
  215. SoundRecord,
  216. SoundRecordPreview,
  217. },
  218. mixins: [QuestionMixin],
  219. data() {
  220. return {
  221. curRole: '',
  222. textInput: '',
  223. file_id_list: [],
  224. file_map_list: [],
  225. data: getDialogueData(),
  226. };
  227. },
  228. watch: {
  229. 'data.property.role_number'(val) {
  230. if (this.data.property.role_list.length > val) {
  231. this.data.property.role_list = this.data.property.role_list.slice(0, val);
  232. } else {
  233. for (let i = this.data.property.role_list.length; i < val; i++) {
  234. this.data.property.role_list.push(getRole(i));
  235. }
  236. }
  237. },
  238. 'data.property.role_list': {
  239. handler(val) {
  240. // 如果当前角色(curRole)不在角色列表中,将当前角色(curRole)设置为角色列表中的第一个角色
  241. if (val.find((item) => item.mark === this.curRole) === undefined && val.length > 0) {
  242. this.curRole = val[0].mark;
  243. }
  244. },
  245. immediate: true,
  246. },
  247. 'data.option_list': {
  248. handler(val) {
  249. let file_id_list = [];
  250. val.forEach(({ type, file_id }) => {
  251. if (type === 'image' || type === 'audio') {
  252. file_id_list.push(file_id);
  253. }
  254. });
  255. // 判断 this.file_id_list 和 file_id_list 两个数组中的内容是否相等,位置可能不同
  256. if (
  257. this.file_id_list.length === file_id_list.length &&
  258. this.file_id_list.every((item) => file_id_list.includes(item))
  259. ) {
  260. return;
  261. }
  262. this.file_id_list = file_id_list;
  263. GetFileURLMap({ file_id_list }).then(({ url_map }) => {
  264. this.file_map_list = url_map;
  265. });
  266. },
  267. },
  268. },
  269. methods: {
  270. /**
  271. * 智能识别
  272. * @param {String} text 识别数据
  273. */
  274. recognition(text) {
  275. let arr = this.recognitionCommon(text);
  276. let obj = analysisRecognitionDialogueData(arr);
  277. this.recognitionCommonSetObj(obj);
  278. },
  279. // 音频上传前处理
  280. handleBeforeAudio(file) {
  281. if (this.curRole.length <= 0) {
  282. this.$message.error('请先选择角色');
  283. return false;
  284. }
  285. // 判断文件是否为音频
  286. if (!file.type.includes('audio')) {
  287. this.$message.error('请选择音频文件');
  288. return false;
  289. }
  290. },
  291. // 图片上传前处理
  292. handleBeforeImage(file) {
  293. if (this.curRole.length <= 0) {
  294. this.$message.error('请先选择角色');
  295. return false;
  296. }
  297. // 判断文件是否为图片
  298. if (!file.type.includes('image')) {
  299. this.$message.error('请选择图片文件');
  300. return false;
  301. }
  302. },
  303. // 音频上传
  304. handleAudio(file) {
  305. this.upload('audio', file);
  306. },
  307. // 图片上传
  308. handleImage(file) {
  309. this.upload('image', file);
  310. },
  311. // 上传
  312. upload(type, file) {
  313. fileUpload('Mid', file, { isGlobalprogress: true }).then(({ file_info_list }) => {
  314. if (file_info_list.length > 0) {
  315. const { file_id } = file_info_list[0];
  316. this.data.option_list.push({
  317. role: this.curRole,
  318. file_id,
  319. type,
  320. });
  321. }
  322. });
  323. },
  324. // 处理输入文本
  325. handleText() {
  326. if (this.curRole.length <= 0) {
  327. return this.$message.error('请先选择角色');
  328. }
  329. this.data.option_list.push({
  330. role: this.curRole,
  331. text: this.textInput.replace(/\n/, ''),
  332. file_id: '',
  333. mark: getRandomNumber(),
  334. type: 'text',
  335. });
  336. this.textInput = '';
  337. },
  338. /**
  339. * 移动选项
  340. * @param {'up'|'down'} type 类型
  341. * @param {number} i 索引
  342. */
  343. moveOption(type, i) {
  344. if ((type === 'up' && i === 0) || (type === 'down' && i === this.data.option_list.length - 1)) return;
  345. const item = this.data.option_list[i];
  346. this.data.option_list.splice(i, 1);
  347. this.data.option_list.splice(type === 'up' ? i - 1 : i + 1, 0, item);
  348. },
  349. /**
  350. * 删除选项
  351. * @param {number} i 索引
  352. */
  353. deleteOption(i) {
  354. this.data.option_list.splice(i, 1);
  355. },
  356. // 插入学生
  357. insertStudent() {
  358. if (this.curRole.length <= 0) {
  359. return this.$message.error('请先选择角色');
  360. }
  361. this.data.option_list.push({
  362. mark: getRandomNumber(),
  363. role: this.curRole,
  364. file_id: '',
  365. type: 'input',
  366. });
  367. },
  368. /**
  369. * 保存音频
  370. * @param {string} file_id 文件id
  371. */
  372. saveWav(file_id) {
  373. this.data.option_list.push({
  374. role: this.curRole,
  375. file_id,
  376. type: 'audio',
  377. });
  378. },
  379. },
  380. };
  381. </script>
  382. <style lang="scss" scoped>
  383. .content-wrapper {
  384. display: flex;
  385. flex-direction: column;
  386. row-gap: 8px;
  387. padding-bottom: 8px;
  388. margin-bottom: 8px;
  389. border-bottom: $border;
  390. .content {
  391. display: flex;
  392. flex-direction: column;
  393. row-gap: 16px;
  394. padding: 16px;
  395. background-color: $fill-color;
  396. .option-list {
  397. display: flex;
  398. column-gap: 8px;
  399. .avatar {
  400. display: flex;
  401. align-items: center;
  402. justify-content: center;
  403. width: 40px;
  404. min-width: 40px;
  405. height: 40px;
  406. min-height: 40px;
  407. margin-right: 8px;
  408. font-size: 12px;
  409. color: #fff;
  410. border-radius: 50%;
  411. }
  412. .content-operation {
  413. display: flex;
  414. column-gap: 8px;
  415. align-items: center;
  416. .up-down {
  417. display: flex;
  418. flex-direction: column;
  419. row-gap: 12px;
  420. // span 三角形
  421. :first-child {
  422. width: 0;
  423. height: 0;
  424. cursor: pointer;
  425. border-color: transparent;
  426. border-style: solid;
  427. border-width: 4px;
  428. }
  429. :last-child {
  430. width: 0;
  431. height: 0;
  432. cursor: pointer;
  433. border-color: transparent;
  434. border-style: solid;
  435. border-width: 4px;
  436. }
  437. }
  438. .svg-icon {
  439. cursor: pointer;
  440. }
  441. }
  442. .sound-record-preview {
  443. padding: 4px;
  444. background-color: #fff;
  445. border-radius: 40px;
  446. :deep .sound-item .sound-item-span {
  447. color: #1d1d1d;
  448. background-color: #fff;
  449. }
  450. :deep .sound-item-luyin .sound-item-span {
  451. color: #fff;
  452. background-color: $light-main-color;
  453. }
  454. }
  455. .text {
  456. padding: 8px 12px;
  457. word-break: break-all;
  458. background-color: #fff;
  459. border-radius: 8px;
  460. }
  461. .image img {
  462. height: 180px;
  463. border-radius: 8px;
  464. }
  465. }
  466. }
  467. .operation {
  468. display: flex;
  469. justify-content: space-between;
  470. font-size: 14px;
  471. &-left {
  472. display: flex;
  473. column-gap: 8px;
  474. align-items: center;
  475. .el-select {
  476. width: 130px;
  477. }
  478. }
  479. &-right {
  480. display: flex;
  481. column-gap: 36px;
  482. align-items: center;
  483. color: $main-color;
  484. .insert-student {
  485. display: flex;
  486. column-gap: 8px;
  487. align-items: center;
  488. cursor: pointer;
  489. }
  490. :deep .el-upload {
  491. display: flex;
  492. column-gap: 8px;
  493. align-items: center;
  494. }
  495. }
  496. }
  497. .tips {
  498. font-size: 14px;
  499. color: #e82d2d;
  500. }
  501. }
  502. .correct-answer {
  503. display: flex;
  504. flex-wrap: wrap;
  505. gap: 8px 8px;
  506. margin-top: 8px;
  507. .el-input {
  508. width: 180px;
  509. :deep &__prefix {
  510. display: flex;
  511. align-items: center;
  512. color: $text-color;
  513. }
  514. }
  515. }
  516. .reference-answer {
  517. margin-top: 8px;
  518. .reference-title {
  519. margin-bottom: 8px;
  520. font-size: 14px;
  521. }
  522. }
  523. .el-form {
  524. .role {
  525. :deep .el-form-item__content {
  526. display: flex;
  527. flex: 1;
  528. .el-input {
  529. width: 100px;
  530. margin-right: 16px;
  531. }
  532. .el-color-picker {
  533. flex: 1;
  534. &__trigger {
  535. width: 100%;
  536. background-color: $fill-color;
  537. }
  538. &__color {
  539. width: 22px;
  540. height: 22px;
  541. border-width: 0;
  542. }
  543. &__icon {
  544. left: 88%;
  545. width: 20px;
  546. color: $font-color;
  547. }
  548. }
  549. }
  550. }
  551. }
  552. </style>