mx-paper-mixin.js 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. import {
  2. commitExamineePaper,
  3. commitExamineeQuestion,
  4. loadExamineePaper,
  5. openExamineePaper,
  6. scoreExamineeQuestion,
  7. scoreFinish,
  8. teacherScoreExamineeQuestions
  9. } from '@/api/webApi/studentEvaluating.js'
  10. import config from '@/common/mx-config.js'
  11. import consts from '@/common/mx-const'
  12. import { mapGetters } from 'vuex'
  13. import { formatDuration } from '@/utils/index'
  14. import eventMixin from './mx-question-event-mixin'
  15. import EventBus from '@/components/EventBus'
  16. export default {
  17. mixins: [eventMixin],
  18. props: {
  19. options: {
  20. type: Object,
  21. default: _ => ({
  22. paper: null,
  23. navigatorSpan: 6,
  24. extData: {},
  25. currentTab: 0,
  26. questionOptionsCache: {},
  27. committingQuestions: [],
  28. loading: false,
  29. committing: false,
  30. initWithAnswerOfPrevQuestion: false,
  31. // extra
  32. customScoredAction: false,
  33. customCommittedAction: false
  34. })
  35. }
  36. },
  37. computed: {
  38. ...mapGetters(['isFrontTeacher', 'isFrontStudent', 'currentUser', 'firstClassName']),
  39. // properties
  40. paper() {
  41. return this.options.paper
  42. },
  43. extData() {
  44. return this.options.extData
  45. },
  46. questionOptionsCache() {
  47. return this.options.questionOptionsCache
  48. },
  49. allowAnswer() {
  50. return this.paper.allowAnswer
  51. },
  52. allowScore() {
  53. return this.paper.allowScore
  54. },
  55. examineeId() {
  56. return this.extData.examineeId || this.paper.examineeId
  57. },
  58. examineeType() {
  59. return this.extData.examineeType ||
  60. this.paper?.examineeType ||
  61. consts.enum.paper.examineeType.evaluation
  62. },
  63. evaluationClassId() {
  64. return this.extData.evaluationClassId
  65. },
  66. studentName() {
  67. return this.extData.studentName || this.currentUser.nickName
  68. },
  69. studentClassName() {
  70. return this.extData.class || this.firstClassName
  71. },
  72. avatar() {
  73. return this.extData.avatar || config.avatar_default
  74. },
  75. loading() {
  76. return this.options.loading
  77. },
  78. committing() {
  79. return this.options.committing
  80. },
  81. currentTab() {
  82. return this.options.currentTab
  83. },
  84. committingQuestions() {
  85. return this.options.committingQuestions
  86. },
  87. // business data
  88. countdownDisplay() {
  89. return formatDuration(this.paper.remaining)
  90. },
  91. stateStr() {
  92. if (this.paper.allowAnswer || this.paper.allowScore) return ''
  93. return this.paper.stateStr
  94. },
  95. scoreColor() {
  96. return this.paper.allowAnswer ? '' : config.color.error
  97. },
  98. currentQuestion() {
  99. if (this.paper?.questions?.length > this.currentTab) {
  100. return this.paper.questions[this.currentTab]
  101. }
  102. return null
  103. },
  104. currentQuestionOptions() {
  105. if (!this.currentQuestion) return null
  106. return this.questionOptionsCache[this.currentTab]
  107. },
  108. hasPrevious() {
  109. return this.currentTab > 0
  110. },
  111. hasNext() {
  112. return this.currentTab < this.paper.questions?.length - 1
  113. },
  114. queryApi() {
  115. return this.isFrontStudent ? openExamineePaper : loadExamineePaper
  116. },
  117. scoreApi() {
  118. return this.isFrontStudent ? scoreExamineeQuestion : teacherScoreExamineeQuestions
  119. }
  120. },
  121. methods: {
  122. // for shortcut
  123. forceBlur() {
  124. // Give the document focus
  125. if (document.activeElement) {
  126. document.activeElement.blur()
  127. }
  128. this.$el.focus()
  129. },
  130. loadPaper(extData, overrideOptions) {
  131. // set ext
  132. this.options.extData = extData
  133. if (overrideOptions) {
  134. Object.keys(overrideOptions).forEach(key => this.options[key] = overrideOptions[key])
  135. }
  136. // query
  137. let queryApi = this.queryApi
  138. this.options.loading = true
  139. queryApi(extData).then(res => {
  140. this.options.currentTab = 0 // reset position
  141. const paper = res.data
  142. const autoScoreQuestions = this.eventProcessPaper(paper, this.questionOptionsCache)
  143. this.options.paper = paper
  144. this.autoScoreObjectives(autoScoreQuestions)
  145. }).finally(() => {
  146. return this.options.loading = false
  147. })
  148. },
  149. beforeQuestionChangeAction() {
  150. this.triggerAutoCommitQuestion()
  151. },
  152. goIndexAction(index) {
  153. if (this.currentTab != index) {
  154. this.triggerAutoCommitQuestion()
  155. this.options.currentTab = index
  156. }
  157. },
  158. goPreviousAction() {
  159. this.triggerAutoCommitQuestion()
  160. this.options.currentTab = this.currentTab - 1
  161. },
  162. goNextAction() {
  163. this.triggerAutoCommitQuestion()
  164. this.options.currentTab = this.currentTab + 1
  165. },
  166. goUnfinishedQuestionAction(index) {
  167. this.options.currentTab = index
  168. },
  169. commitQuestionAction(commit) {
  170. // 部分提交,断点续传
  171. this.committingQuestions.push(commit)
  172. this.commitQuestionCore()
  173. },
  174. autoScoreObjectives(scoredQuestions) {
  175. if (!scoredQuestions?.length) return
  176. // 静默提交
  177. const func = this.scoreApi
  178. func({
  179. examineeId: this.examineeId,
  180. examineeType: this.examineeType,
  181. questions: scoredQuestions
  182. }).then(res => {
  183. // donothing
  184. })
  185. },
  186. paperTimeUpAction() {
  187. this.$once('singleCommitCompleted', _ => this.commitPaper())
  188. this.triggerAutoCommitQuestion()
  189. },
  190. paperSubmitAction() {
  191. this.$once('singleCommitCompleted', _ => {
  192. const unfinishedList = this.eventUnfinishedQuestions(this.paper)
  193. if (unfinishedList.length) {
  194. const msg = `检测到您还有第(${unfinishedList.map(q => q.seq)})题没有完成,是否做完再交卷?!`
  195. this.$confirm(msg, '提示', {
  196. distinguishCancelAndClose: true,
  197. confirmButtonText: '继续做题',
  198. cancelButtonText: '确认交卷'
  199. }).then(() => {
  200. this.goUnfinishedQuestionAction(unfinishedList.first().seq - 1)
  201. }).catch(action => {
  202. if (action == 'cancel') {
  203. this.commitPaper()
  204. }
  205. })
  206. } else {
  207. const msg = '检测到您已完成答题'
  208. this.$confirm(msg, '提示', {
  209. confirmButtonText: '确认提交',
  210. cancelButtonText: '再检查一遍'
  211. }).then(() => this.commitPaper())
  212. }
  213. })
  214. this.triggerAutoCommitQuestion()
  215. },
  216. triggerAutoCommitQuestion() {
  217. if (!this.paper.allowAnswer) return
  218. const commit = this.eventBuildCommitAnswerData(this.currentQuestion)
  219. this.commitQuestionAction(commit)
  220. },
  221. commitQuestionCore() {
  222. if (this.committingQuestions.length == 0) {
  223. this.$emit('singleCommitCompleted')
  224. return
  225. }
  226. const target = this.committingQuestions[0]
  227. const commit = {
  228. examineeId: this.examineeId,
  229. examineeType: this.examineeType,
  230. questions: [{
  231. ...target,
  232. examineeType: this.examineeType
  233. }]
  234. }
  235. // 对比历史作答与现在提交内容是否有差异,防止切换时重复计时
  236. const rawQuestion = this.paper.questions.find(q => q.questionId == target.questionId)
  237. if (target.answer == rawQuestion.answer &&
  238. target.attachments?.toString() == rawQuestion.attachments?.toString()) {
  239. // 答题没有变化,不需要重复提交
  240. this.committingQuestions.shift()
  241. setTimeout(_ => this.commitQuestionCore(), 200)
  242. return
  243. }
  244. commitExamineeQuestion(commit)
  245. .then(res => {
  246. // 成功后可用提交属性覆盖题目属性
  247. this.eventHandleAnswerSubmitted(target, rawQuestion)
  248. this.committingQuestions.shift()
  249. setTimeout(_ => this.commitQuestionCore(), 200)
  250. }).catch(e => {
  251. // 失败后可尝试重复提交
  252. // TODO: hht 2022.1.2 这里如果一直异常了会陷入死循环
  253. // setTimeout(_ => this.commitQuestionCore(), 200)
  254. })
  255. },
  256. commitPaper() {
  257. if (this.committing) return
  258. this.options.committing = true
  259. const commit = {
  260. examineeId: this.examineeId,
  261. examineeType: this.examineeType,
  262. questions: this.paper.questions.map(q => ({
  263. questionId: q.questionId,
  264. answer: q.answer,
  265. attachments: q.attachments,
  266. duration: q.duration,
  267. examineeType: this.examineeType
  268. }))
  269. }
  270. commitExamineePaper(commit).then(res => {
  271. this.$alert('交卷成功').then(() => this.customCommitCompleted())
  272. }).finally(() => this.options.committing = false)
  273. },
  274. async scoreFinishAction() {
  275. const unScoredQuestions = this.eventUnScoredQuestions(this.paper)
  276. if (unScoredQuestions.length > 0) {
  277. const msg = `第${unScoredQuestions.map(q => q.seq)}题未完成评分`
  278. this.msgError(msg)
  279. this.goUnfinishedQuestionAction(unScoredQuestions.first().seq - 1)
  280. return
  281. }
  282. await this.$confirm('确认提交?')
  283. if (this.committing) return
  284. this.options.committing = true
  285. scoreFinish({
  286. examineeId: this.examineeId,
  287. examineeType: this.examineeType
  288. }).then(res => {
  289. this.$alert('阅卷完毕').then(() => this.customScoreCompleted())
  290. }).finally(() => this.options.committing = false)
  291. },
  292. customCommitCompleted() {
  293. if (this.options.customCommittedAction) {
  294. // NOTE: 用eventBus解耦组件层级限制
  295. // TODO: 这里的if-else分支不是唯一做法,也可以重写包含提交逻辑的组件,用slot装载,不知道哪种方式会更好
  296. this.$nextTick(() => EventBus.instance.$emit('customCommittedAction'))
  297. } else {
  298. this.$router.back()
  299. }
  300. },
  301. customScoreCompleted() {
  302. if (this.options.customScoredAction) {
  303. // NOTE: 用eventBus解耦组件层级限制
  304. // TODO: 这里的if-else分支不是唯一做法,也可以重写包含提交逻辑的组件,用slot装载,不知道哪种方式会更好
  305. this.$nextTick(() => EventBus.instance.$emit('customScoredAction'))
  306. } else {
  307. this.$router.back()
  308. }
  309. }
  310. }
  311. }