answerTheResult.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694
  1. <template>
  2. <div class="answerTheResult">
  3. <div class="atr_detail">
  4. <div class="atr_d_btn" @click="lookDetail()">{{ isShowDialog ? lang.ssViewQ : lang.ssViewRes }}</div>
  5. <div class="atr_d_msg">
  6. <div>{{ lang.ssParticipants }}</div>
  7. <span
  8. >{{ workArrayLength }}/{{
  9. workArrayLength + unsubmittedStudentsLength
  10. }}</span
  11. >
  12. </div>
  13. <div class="atr_d_msg" v-if="workDetail && workDetail.type === '45'">
  14. <div>{{ lang.ssAccuracy }}</div>
  15. <span v-if="choiceQuestionListData[workIndex]">{{choiceQuestionListData[workIndex].accuracyRate}}%({{choiceQuestionListData[workIndex].yes}}/{{choiceQuestionListData[workIndex].all}})</span>
  16. </div>
  17. <div class="atr_d_msg" v-if="choiceQuestionAnswer">
  18. <div>{{ lang.ssCorrectAns }}</div>
  19. <span style="color: #03ae2b">{{ choiceQuestionAnswer }}</span>
  20. </div>
  21. <span class="atr_d_line" v-if="props.unsubmittedStudents && props.unsubmittedStudents?.length > 0"></span>
  22. <div class="no_submit" v-if="props.unsubmittedStudents && props.unsubmittedStudents?.length > 0">
  23. <div>{{ lang.ssUnsubStu }}</div>
  24. <img
  25. @click="showNoSubmitDetail = !showNoSubmitDetail"
  26. :class="{ no_submit_active: !showNoSubmitDetail }"
  27. src="../../../assets/img/arrow_up.png"
  28. />
  29. </div>
  30. <div
  31. class="no_submitList"
  32. v-if="props.unsubmittedStudents && props.unsubmittedStudents?.length > 0"
  33. :class="{ no_submitList_active: showNoSubmitDetail }"
  34. >
  35. <div
  36. v-for="(student, idx) in props.unsubmittedStudents"
  37. :key="student.id ?? idx"
  38. >
  39. {{ student.name ?? "" }}
  40. </div>
  41. </div>
  42. <span class="atr_d_line" v-if="props.workArray && props.workArray?.length > 0"></span>
  43. <div class="is_submit" v-if="props.workArray && props.workArray?.length > 0">
  44. <div>{{ lang.ssSubStu }}</div>
  45. <img
  46. @click="showIsSubmitDetail = !showIsSubmitDetail"
  47. :class="{ is_submit_active: !showIsSubmitDetail }"
  48. src="../../../assets/img/arrow_up.png"
  49. />
  50. </div>
  51. <div
  52. class="is_submitList"
  53. v-if="props.workArray && props.workArray?.length > 0"
  54. :class="{ is_submitList_active: showIsSubmitDetail }"
  55. >
  56. <div
  57. v-for="(student, idx) in props.workArray"
  58. :key="student.id ?? idx"
  59. @click="emit('openWorkModal', student)"
  60. >
  61. {{ student.name ?? "" }}
  62. </div>
  63. </div>
  64. </div>
  65. <div
  66. class="atr_type45Area"
  67. v-if="
  68. workDetail &&
  69. workDetail.type === '45' &&
  70. choiceQuestionListData[workIndex] &&
  71. choiceQuestionListData[workIndex].choiceUser
  72. "
  73. >
  74. <!-- <div class="atr_t45a_title" >
  75. <span>{{ workIndex+1 }}、{{ choiceQuestionListData[workIndex].type=='1'?'单选题':'多选题' }}:</span>
  76. <span>{{ choiceQuestionListData[workIndex].teststitle }}</span>
  77. <img @click="previewImage(choiceQuestionListData[workIndex].timuList[0].src)" :src="choiceQuestionListData[workIndex].timuList[0].src" v-if="choiceQuestionListData[workIndex].timuList.length>0">
  78. </div> -->
  79. <template
  80. v-for="(op, idx) in choiceQuestionListData[workIndex].choiceUser"
  81. :key="`${workIndex}_${idx}`"
  82. >
  83. <div class="atr_t45a_item">
  84. <div class="atr_t45a_i_top">
  85. <div class="atr_t45a_i_left">
  86. <span>{{ serialNumber[idx] }}</span>
  87. <div>
  88. <img
  89. v-if="op.option.imgType"
  90. :src="op.option.src"
  91. @click="previewImage(op.option.src)"
  92. />
  93. <span v-else>{{ op.option }}</span>
  94. </div>
  95. <span v-if="op.isAnswer">{{ lang.ssCorrectTag }}</span>
  96. </div>
  97. <img
  98. @click="changeShow(op, idx)"
  99. v-if="op.user.length > 0"
  100. :class="{ show_active: !op.show }"
  101. src="../../../assets/img/arrow_up.png"
  102. />
  103. </div>
  104. <div
  105. class="atr_t45a_i_bottom"
  106. v-if="op.user.length > 0 && op.show"
  107. :class="{ atr_t45a_i_b_active: op.show }"
  108. >
  109. <div
  110. v-for="(name, uIdx) in op.user"
  111. :key="`${workIndex}_${idx}_${uIdx}`"
  112. >
  113. {{ name }}
  114. </div>
  115. </div>
  116. </div>
  117. </template>
  118. <div class="nextAndUpBtn" v-if="choiceQuestionListData.length>1">
  119. <span :class="{no_active:workIndex==0}" @click="changeWorkIndex(0)">{{ lang.ssPrevQ }}</span>
  120. <span :class="{no_active:choiceQuestionListData.length-1<=workIndex}" @click="changeWorkIndex(1)">{{ lang.ssNextQ }}</span>
  121. </div>
  122. </div>
  123. </div>
  124. <!-- 预览放大(带缩放/拖拽/旋转/工具栏) -->
  125. <previewImageTool ref="previewImageToolRef"/>
  126. </template>
  127. <script lang="ts" setup>
  128. import { ref, computed, watch, inject, type Ref } from 'vue'
  129. import previewImageTool from '../../components/tool/previewImageTool.vue'
  130. import api from '../../../services/course'
  131. import { lang } from '@/main'
  132. interface Props {
  133. workArray?: object[] | null;
  134. unsubmittedStudents?: object[] | null;
  135. workId?: string | null;
  136. slideIndex?:number
  137. toolType?:number
  138. }
  139. const props = withDefaults(defineProps<Props>(), {
  140. workArray: () => [],
  141. unsubmittedStudents: () => [],
  142. workId: '',
  143. toolType: 0,
  144. slideIndex: 0
  145. })
  146. const emit = defineEmits<{
  147. (e: 'openChoiceQuestionDetail', v: number): void,
  148. (e: 'openWorkModal', v:any):void
  149. }>()
  150. const choiceQuestionDetailDialogOpenList = inject<Ref<number[]>>('choiceQuestionDetailDialogOpenList', ref<number[]>([]))
  151. const isShowDialog = computed(() => {
  152. return choiceQuestionDetailDialogOpenList.value.includes(props.slideIndex)
  153. })
  154. // 已提交的作业数量
  155. const workArrayLength = computed(() => {
  156. let _result = 0
  157. if (props.workArray) {
  158. _result = props.workArray.length
  159. }
  160. return _result
  161. })
  162. // 未提交的作业数量
  163. const unsubmittedStudentsLength = computed(() => {
  164. let _result = 0
  165. if (props.unsubmittedStudents) {
  166. _result = props.unsubmittedStudents.length
  167. }
  168. return _result
  169. })
  170. // 选项序号
  171. const serialNumber = ref<string[]>([
  172. 'A',
  173. 'B',
  174. 'C',
  175. 'D',
  176. 'E',
  177. 'F',
  178. 'G',
  179. 'H',
  180. 'I',
  181. 'J',
  182. 'K',
  183. 'L',
  184. 'M',
  185. 'N',
  186. 'O',
  187. 'P',
  188. 'Q',
  189. 'R',
  190. 'S',
  191. 'T',
  192. 'U',
  193. 'V',
  194. 'W',
  195. 'X',
  196. 'Y',
  197. 'Z',
  198. ])
  199. // 第几题
  200. const workIndex = ref<number>(0)
  201. // 是否显示未提交人员
  202. const showNoSubmitDetail = ref<boolean>(false)
  203. const showIsSubmitDetail = ref<boolean>(false)
  204. // 作业详细
  205. const workDetail = ref<any>({})
  206. // 获取作业详细
  207. const getWorkDetail = async () => {
  208. if (props.workId) {
  209. const _res = await api.getWorkDetail({ id: props.workId })
  210. const _data = _res[0][0]
  211. if (_data) {
  212. _data.json = JSON.parse(_data.json)
  213. workDetail.value = _data
  214. choiceQuestionListData.value = choiceQuestionList()
  215. }
  216. }
  217. }
  218. // 选择题题目数组
  219. const choiceQuestionList = () => {
  220. let _result: any[] = []
  221. if (workDetail.value && workDetail.value.type === '45') {
  222. const _workData = workDetail.value.json.testJson
  223. _workData.forEach((item: any, index: number) => {
  224. // 修复 props.workArray 可能为 null 的问题
  225. item.choiceUser = []
  226. item.yes = 0
  227. item.no = 0
  228. item.checkList.forEach((op: any, oidx: number) =>
  229. item.choiceUser.push({
  230. index: serialNumber.value[oidx],
  231. option: op,
  232. user: [],
  233. show: false,
  234. isAnswer: Array.isArray(item.answer)
  235. ? item.answer.includes(oidx)
  236. : item.answer === oidx,
  237. })
  238. )
  239. });
  240. (props.workArray ?? []).forEach(
  241. (studentWork: any, studentIndex: number) => {
  242. const _studentContent: any = JSON.parse(
  243. decodeURIComponent(studentWork.content)
  244. ).testJson
  245. // let _studentContentCorrection = []
  246. // console.log(_workData)
  247. // console.log(_studentContent)
  248. // _studentContentCorrection = _workData.map((i:any) => {
  249. // return _studentContent.find((i2:any) => {
  250. // return (JSON.stringify({testtitle: i2.testtitle, checkList: i2.checkList, timuList: i2.timuList}) === JSON.stringify({testtitle: i.testtitle, checkList: i.checkList, timuList: i.timuList}))
  251. // })
  252. // })
  253. // console.log(_studentContentCorrection)
  254. _studentContent.forEach((test: any, testIndex: number) => {
  255. if (_workData[testIndex] && JSON.stringify({testtitle: test.testtitle, checkList: test.checkList, timuList: test.timuList}) === JSON.stringify({testtitle: _workData[testIndex].testtitle, checkList: _workData[testIndex].checkList, timuList: _workData[testIndex].timuList}) && (test.userAnswer || test.userAnswer === 0)) {
  256. if (Array.isArray(test.userAnswer)) {
  257. test.userAnswer.forEach((ch: number) => {
  258. _workData[testIndex].choiceUser[ch].user.push(studentWork.name)
  259. })
  260. if (
  261. JSON.stringify(
  262. test.userAnswer.sort((a: number, b: number) => a - b)
  263. ) === JSON.stringify(_workData[testIndex].answer.sort((a: number, b: number) => a - b))
  264. ) {
  265. _workData[testIndex].yes += 1
  266. }
  267. else {
  268. _workData[testIndex].no += 1
  269. }
  270. }
  271. else {
  272. _workData[testIndex].choiceUser[test.userAnswer].user.push(
  273. studentWork.name
  274. )
  275. if (test.userAnswer === _workData[testIndex].answer) {
  276. _workData[testIndex].yes += 1
  277. }
  278. else {
  279. _workData[testIndex].no += 1
  280. }
  281. }
  282. }
  283. })
  284. }
  285. )
  286. _workData.forEach((item:any) => {
  287. item.all = item.yes + item.no
  288. if (item.all === 0) {
  289. item.accuracyRate = 0
  290. }
  291. else {
  292. const rate = (item.yes / item.all) * 100
  293. item.accuracyRate = Number.isInteger(rate) ? rate : Number(rate.toFixed(2))
  294. }
  295. })
  296. _result = _workData
  297. }
  298. return _result
  299. }
  300. const choiceQuestionListData = ref<any[]>([])
  301. // 选择题题目正确答案
  302. const choiceQuestionAnswer = computed(() => {
  303. let _result = ''
  304. if (
  305. workDetail.value &&
  306. workDetail.value.type === '45' &&
  307. workDetail.value.json.testJson[workIndex.value]
  308. ) {
  309. const _answer = workDetail.value.json.testJson[workIndex.value].answer
  310. if (Array.isArray(_answer)) {
  311. _result = _answer.map((i: any) => serialNumber.value[i]).join('、')
  312. }
  313. else {
  314. _result = [serialNumber.value[_answer]].join('、')
  315. }
  316. }
  317. return _result
  318. })
  319. // //当前题目正确率
  320. // const choiceQuestionAccuracyRate = computed
  321. // 监听作业Id
  322. watch(
  323. () => props.workId,
  324. (newVal, oldVal) => {
  325. console.log('props.workId变化', { newVal, oldVal })
  326. if (newVal && newVal !== oldVal) {
  327. getWorkDetail()
  328. }
  329. },
  330. { immediate: true }
  331. )
  332. // 监听已提交的作业
  333. watch(() => props.workArray, (newVal, oldVal) => {
  334. if (newVal && newVal !== oldVal) {
  335. choiceQuestionListData.value = choiceQuestionList()
  336. // choiceQuestionAnswerData.value = choiceQuestionAnswer()
  337. }
  338. }, { immediate: true })
  339. // 切换题目
  340. const changeWorkIndex = (type:number) => {
  341. if (type === 0) {
  342. if (workIndex.value === 0) return
  343. workIndex.value -= 1
  344. }
  345. else if (type === 1) {
  346. if (choiceQuestionListData.value.length - 1 <= workIndex.value) return
  347. workIndex.value += 1
  348. }
  349. }
  350. // 切换题目展示所有人选项
  351. const changeShow = (op: any, idx: number) => {
  352. choiceQuestionListData.value[workIndex.value].choiceUser[idx].show = !choiceQuestionListData.value[workIndex.value].choiceUser[idx].show
  353. }
  354. const lookDetail = () => {
  355. console.log(props.toolType)
  356. if ([45, 15, 72, 73].includes(props.toolType)) {
  357. emit('openChoiceQuestionDetail', props.slideIndex)
  358. }
  359. }
  360. const previewImageToolRef = ref<any>(null)
  361. const previewImage = (url:string) => {
  362. previewImageToolRef.value.previewImage(url)
  363. }
  364. defineExpose({
  365. workDetail,
  366. choiceQuestionListData,
  367. workIndex,
  368. workArray: props.workArray,
  369. unsubmittedStudents: props.unsubmittedStudents,
  370. toolType: props.toolType,
  371. changeWorkIndex
  372. })
  373. </script>
  374. <style lang="scss" scoped>
  375. .answerTheResult {
  376. width: 100%;
  377. height: auto;
  378. max-height: 100%;
  379. overflow: auto;
  380. box-sizing: border-box;
  381. padding: 12px;
  382. .atr_detail {
  383. width: 100%;
  384. height: auto;
  385. border-radius: 4px;
  386. padding: 10px 15px 10px 15px;
  387. box-sizing: border-box;
  388. border: solid 1px rgba(0, 0, 0, 0.1);
  389. .atr_d_btn {
  390. width: 100%;
  391. height: 40px;
  392. display: flex;
  393. align-items: center;
  394. justify-content: center;
  395. background: rgba(0, 0, 0, 0.9);
  396. color: #fff;
  397. font-weight: 500;
  398. font-size: 14px;
  399. border-radius: 4px;
  400. cursor: pointer;
  401. }
  402. .atr_d_msg {
  403. width: 100%;
  404. display: flex;
  405. align-items: center;
  406. margin-top: 15px;
  407. font-size: 14px;
  408. color: rgba(0, 0, 0, 0.9);
  409. & > div {
  410. width: 4em;
  411. margin-right: 35px;
  412. font-weight: 500;
  413. }
  414. }
  415. .atr_d_line {
  416. width: 100%;
  417. height: 2px;
  418. display: block;
  419. background: rgba(242, 243, 245, 1);
  420. margin: 20px 0;
  421. border-radius: 2px;
  422. }
  423. .no_submit {
  424. width: 100%;
  425. display: flex;
  426. align-items: center;
  427. justify-content: space-between;
  428. font-size: 14px;
  429. font-weight: 500;
  430. margin-bottom: 20px;
  431. .no_submit_active {
  432. transform: rotate(180deg);
  433. }
  434. & > img {
  435. cursor: pointer;
  436. }
  437. }
  438. .no_submitList {
  439. width: 100%;
  440. display: flex;
  441. align-items: center;
  442. flex-wrap: nowrap;
  443. overflow: hidden;
  444. gap: 10px;
  445. & > div {
  446. padding: 5px 10px;
  447. background: rgba(255, 236, 232, 1);
  448. color: rgba(245, 63, 63, 1);
  449. font-size: 14px;
  450. font-weight: 500;
  451. border-radius: 10px;
  452. white-space: nowrap;
  453. }
  454. }
  455. .no_submitList_active {
  456. flex-wrap: wrap;
  457. }
  458. .is_submit {
  459. width: 100%;
  460. display: flex;
  461. align-items: center;
  462. justify-content: space-between;
  463. font-size: 14px;
  464. font-weight: 500;
  465. margin-bottom: 20px;
  466. .is_submit_active {
  467. transform: rotate(180deg);
  468. }
  469. & > img {
  470. cursor: pointer;
  471. }
  472. }
  473. .is_submitList {
  474. width: 100%;
  475. display: flex;
  476. align-items: center;
  477. flex-wrap: nowrap;
  478. overflow: hidden;
  479. gap: 10px;
  480. & > div {
  481. padding: 5px 10px;
  482. background: #fff;
  483. color: #2F80ED;
  484. font-size: 14px;
  485. font-weight: 500;
  486. border-radius: 10px;
  487. white-space: nowrap;
  488. box-sizing: border-box;
  489. border: solid 1px #2F80ED;
  490. cursor: pointer;
  491. }
  492. }
  493. .is_submitList_active {
  494. flex-wrap: wrap;
  495. }
  496. }
  497. .atr_type45Area {
  498. width: 100%;
  499. height: auto;
  500. .atr_t45a_title{
  501. width: 100%;
  502. height: auto;
  503. box-sizing: border-box;
  504. padding: 10px 15px 10px 15px;
  505. border: solid 1px #0000001a;
  506. border-radius: 4px;
  507. margin-top: 20px;
  508. font-size: 14px;
  509. font-weight: 600;
  510. img{
  511. width: auto;
  512. max-width: 100%;
  513. height: auto;
  514. object-fit: cover;
  515. cursor: pointer;
  516. display: block;
  517. }
  518. }
  519. .atr_t45a_item {
  520. width: 100%;
  521. height: auto;
  522. box-sizing: border-box;
  523. padding: 10px 15px 10px 15px;
  524. border: solid 1px #0000001a;
  525. border-radius: 4px;
  526. margin-top: 20px;
  527. .atr_t45a_i_top {
  528. width: 100%;
  529. height: auto;
  530. display: flex;
  531. align-items: center;
  532. justify-content: space-between;
  533. .atr_t45a_i_left {
  534. width: calc(100% - 50px);
  535. height: auto;
  536. display: flex;
  537. align-items: center;
  538. & > span:nth-child(1) {
  539. display: block;
  540. width: 20px;
  541. height: 20px;
  542. background: #fccf00;
  543. border-radius: 50%;
  544. padding: auto;
  545. box-sizing: border-box;
  546. display: flex;
  547. align-items: center;
  548. justify-content: center;
  549. font-size: 14px;
  550. font-weight: 500;
  551. color: #fff;
  552. margin-right: 10px;
  553. }
  554. & > div {
  555. font-size: 14px;
  556. font-weight: bold;
  557. max-width: calc(100% - 20px - 10px - 45px);
  558. overflow: hidden;
  559. text-overflow: ellipsis;
  560. white-space: nowrap;
  561. display: block;
  562. position: relative;
  563. &>img{
  564. width: 20px;
  565. height: 20px;
  566. object-fit: cover;
  567. cursor: pointer;
  568. }
  569. }
  570. & > span:nth-of-type(2) {
  571. display: block;
  572. padding: 2px 4px;
  573. border-radius: 2px;
  574. background: #aff0b580;
  575. color: #03ae2b;
  576. font-size: 12px;
  577. font-weight: 400;
  578. margin-left: 10px;
  579. }
  580. }
  581. & > img {
  582. width: 16px;
  583. height: 16px;
  584. margin-left: 20px;
  585. cursor: pointer;
  586. }
  587. .show_active {
  588. transform: rotate(180deg);
  589. }
  590. }
  591. .atr_t45a_i_bottom {
  592. width: 100%;
  593. display: flex;
  594. align-items: center;
  595. flex-wrap: nowrap;
  596. overflow: hidden;
  597. gap: 10px;
  598. margin-top: 10px;
  599. & > div {
  600. padding: 5px 10px;
  601. background: #0000000f;
  602. color: #222222;
  603. font-size: 14px;
  604. font-weight: 500;
  605. border-radius: 10px;
  606. }
  607. }
  608. .atr_t45a_i_b_active {
  609. flex-wrap: wrap !important;
  610. }
  611. }
  612. .nextAndUpBtn{
  613. width: 100%;
  614. height: auto;
  615. display: grid;
  616. grid-template-columns: 1fr 1fr;
  617. margin-top: 20px;
  618. gap: 15px;
  619. &>span{
  620. display: flex;
  621. justify-content: center;
  622. align-items: center;
  623. width: 100%;
  624. height: 40px;
  625. border-radius: 5px;
  626. font-size: 14px;
  627. font-weight: 500;
  628. color: #fff;
  629. background: #3681FC;
  630. cursor: pointer;
  631. }
  632. &>.no_active{
  633. background: #cccccc !important;
  634. color: #999999 !important;
  635. cursor: not-allowed !important;
  636. pointer-events: none !important;
  637. }
  638. }
  639. }
  640. }
  641. </style>