TestParticipation.vue 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. <template>
  2. <div class="TestParticipation"
  3. :style="localOptions.style">
  4. <completion-page v-if="isCompletionPage"
  5. :answer-book="answerBook" />
  6. <q-card v-else-if="!answerBook.loading && answerBook.id">
  7. <q-card-section class="flex justify-between">
  8. <div>{{ answerBook.test_info.title }}</div>
  9. <div>
  10. زمان باقیمانده تا پایان آزمون
  11. {{ formattedRemainingTime }}
  12. </div>
  13. </q-card-section>
  14. <q-separator />
  15. <q-banner>
  16. <div class="row">
  17. <div class="col-md-4 col-12">
  18. {{ toShamsi(answerBook.attending_start_time) }}
  19. </div>
  20. <div class="col-md-4 col-12">
  21. {{ answerBook.test_info.test_questions_info.list.length }}
  22. سوال
  23. </div>
  24. </div>
  25. </q-banner>
  26. <template v-if="mounted">
  27. <all-questions v-if="$route.name === 'UserPanel.Test.AnswerBook.Participate.AllQuestions'"
  28. :answer-book="answerBook"
  29. @sentsuccess="onAllQuestionsSentSuccess"
  30. @sending="onAllQuestionsSending"
  31. @sentfailed="onAllQuestionsSentFailed" />
  32. <single-question v-else-if="$route.name === 'UserPanel.Test.AnswerBook.Participate.SingleQuestion'"
  33. :answer-book="answerBook"
  34. :question-number="parseInt($route.params.question_number)"
  35. @sending="onSingleQuestionsSending"
  36. @sentSuccess="onSingleQuestionsSentSuccess"
  37. @sentFailed="onSingleQuestionsSentFailed" />
  38. <div class="flex justify-end q-pa-lg">
  39. <q-btn color="primary"
  40. outline
  41. @click="confirmAnswers">
  42. اتمام آزمون
  43. </q-btn>
  44. </div>
  45. </template>
  46. </q-card>
  47. <div v-else>
  48. کمی صبر کنید...
  49. </div>
  50. <q-dialog v-model="participateTypeDialog">
  51. <q-card>
  52. <q-card-actions>
  53. نحوه پاسخ به آزمون
  54. </q-card-actions>
  55. <q-separator />
  56. <q-card-actions>
  57. <div>
  58. اندیشه جوی عزیز لطفا نحوه پاسخ به سوالات آزمون را مشخص کنید
  59. </div>
  60. <div>
  61. <q-radio v-model="participateType"
  62. val="UserPanel.Test.AnswerBook.Participate.AllQuestions"
  63. label="مایلم پاسخ سوالات را به صورت یک فایل جامع بارگذاری کنم" />
  64. <q-radio v-model="participateType"
  65. val="UserPanel.Test.AnswerBook.Participate.SingleQuestion"
  66. label="مایلم پاسخ سوالات را به صورت جداگانه ذیل همان سوال وارد کنم" />
  67. </div>
  68. </q-card-actions>
  69. <q-card-actions>
  70. <q-btn v-close-popup
  71. outline
  72. label="انصراف از آزمون"
  73. color="red" />
  74. <q-btn :disable="!participateType"
  75. label="تایید و شروع آزمون"
  76. color="primary"
  77. @click="startTest" />
  78. </q-card-actions>
  79. </q-card>
  80. </q-dialog>
  81. </div>
  82. </template>
  83. <script>
  84. import Assist from 'src/assets/js/Assist.js'
  85. import { mixinWidget } from 'src/mixin/Mixins.js'
  86. import { APIGateway } from 'src/api/APIGateway.js'
  87. import { AnswerBook } from 'src/models/AnswerBook.js'
  88. import AllQuestions from './components/AllQuestions.vue'
  89. import SingleQuestion from './components/SingleQuestion.vue'
  90. import CompletionPage from './components/CompletionPage.vue'
  91. export default {
  92. name: 'TestParticipation',
  93. components: { AllQuestions, SingleQuestion, CompletionPage },
  94. mixins: [mixinWidget],
  95. data () {
  96. return {
  97. mounted: false,
  98. participateType: null,
  99. participateTypeDialog: false,
  100. storeAnswerBook: null,
  101. answerBook: new AnswerBook(),
  102. remainingTime: 0,
  103. timerInterval: null
  104. }
  105. },
  106. computed: {
  107. isCompletionPage () {
  108. return this.$route.name === 'UserPanel.Test.AnswerBook.Participate.SingleQuestion' && this.$route.params.question_number === 'complete'
  109. },
  110. formattedRemainingTime () {
  111. const hours = Math.max(Math.floor(this.remainingTime / 3600), 0)
  112. const minutes = Math.max(Math.floor((this.remainingTime % 3600) / 60), 0)
  113. const seconds = Math.max(this.remainingTime % 60, 0)
  114. // Add zero padding for hours, minutes, and seconds
  115. const formattedHours = hours.toString().padStart(2, '0')
  116. const formattedMinutes = minutes.toString().padStart(2, '0')
  117. const formattedSeconds = seconds.toString().padStart(2, '0')
  118. return formattedHours + ':' + formattedMinutes + ':' + formattedSeconds
  119. }
  120. },
  121. mounted () {
  122. this.getTestQuestions()
  123. this.mounted = true
  124. this.$bus.on('test-participate-single-question-sent-success', () => {
  125. this.onSingleQuestionsSentSuccess()
  126. })
  127. this.$bus.on('test-participate-single-question-sent-failed', () => {
  128. this.onSingleQuestionsSentFailed()
  129. })
  130. this.$bus.on('test-participate-all-question-sent-success', () => {
  131. this.onAllQuestionsSentSuccess()
  132. })
  133. this.$bus.on('test-participate-all-question-sent-failed', () => {
  134. this.onAllQuestionsSentFailed()
  135. })
  136. },
  137. beforeUnmount() {
  138. this.stopTimer()
  139. },
  140. methods: {
  141. loadAnswerBookFromStore () {
  142. this.storeAnswerBook = this.$store.getters['Test/answerBook']
  143. },
  144. onAllQuestionsSending () {
  145. this.answerBook.loading = true
  146. },
  147. onAllQuestionsSentSuccess () {
  148. this.answerBook.loading = false
  149. this.getTestQuestions(true)
  150. },
  151. onAllQuestionsSentFailed () {
  152. this.answerBook.loading = false
  153. },
  154. onSingleQuestionsSending () {
  155. this.answerBook.loading = true
  156. },
  157. onSingleQuestionsSentSuccess () {
  158. this.answerBook.loading = false
  159. // this.backToClassList()
  160. },
  161. onSingleQuestionsSentFailed () {
  162. this.answerBook.loading = false
  163. },
  164. toShamsi (miladi) {
  165. return Assist.miladiToShamsi(miladi)
  166. },
  167. startTest () {
  168. this.$router.push({ name: this.participateType, params: { question_number: 1 } })
  169. },
  170. confirmAnswers () {
  171. this.$q.dialog({
  172. title: 'اتمام آزمون',
  173. message: 'آیا از اتمام آزمون و تایید پاسخ های خود اطمینان دارید؟',
  174. cancel: true,
  175. persistent: true
  176. }).onOk(() => {
  177. APIGateway.answerBook.confirmAnswers(this.answerBook.id)
  178. .then(() => {
  179. this.$q.notify({
  180. message: 'تایید آزمون با موفقیت انجام شد.',
  181. type: 'positive'
  182. })
  183. this.$router.push({
  184. name: 'UserPanel.Test.AnswerBook.Participate.SingleQuestion',
  185. params: {
  186. test_id: this.$route.params.test_id,
  187. answer_book_id: this.$route.params.answer_book_id,
  188. question_number: 'complete'
  189. }
  190. })
  191. })
  192. .catch(() => {})
  193. }).onCancel(() => {})
  194. },
  195. getRemainingTimeInSeconds (serverTimeStr, attendingStartTimeStr, durationInMinutes) {
  196. // Convert the strings to Date objects
  197. const serverTime = new Date(serverTimeStr)
  198. const attendingStartTime = new Date(attendingStartTimeStr)
  199. // Calculate the difference in milliseconds
  200. const timeDiffInMilliseconds = serverTime - attendingStartTime
  201. // Convert the difference to seconds
  202. const timeDiffInSeconds = timeDiffInMilliseconds / 1000
  203. // Calculate the remaining time in seconds considering the duration
  204. return Math.floor((durationInMinutes * 60) - timeDiffInSeconds)
  205. },
  206. onTimeout () {
  207. this.$q.notify({
  208. type: 'negative',
  209. message: 'زمان آزمون به اتمام رسیده است.'
  210. })
  211. this.backToClassList()
  212. },
  213. startTimer () {
  214. this.stopTimer()
  215. this.timerInterval = setInterval(() => {
  216. if (this.remainingTime <= 0) {
  217. this.stopTimer()
  218. this.onTimeout()
  219. } else {
  220. this.remainingTime--
  221. this.answerBook.remainingTime = this.remainingTime
  222. this.$store.commit('Test/updateAnswerBook', this.answerBook)
  223. }
  224. }, 1000)
  225. },
  226. stopTimer () {
  227. if (!this.timerInterval) {
  228. return
  229. }
  230. clearInterval(this.timerInterval)
  231. },
  232. backToClassList () {
  233. this.$router.push({ name: 'UserPanel.Profile.AllClassrooms' })
  234. },
  235. initPageFromAnswerBook (answerBook) {
  236. this.answerBook = new AnswerBook(answerBook)
  237. this.answerBook.loading = false
  238. if (this.answerBook.remainingTime === null) {
  239. this.answerBook.remainingTime = this.getRemainingTimeInSeconds(this.answerBook.server_time, this.answerBook.attending_start_time, this.answerBook.test_info.duration_deadline)
  240. }
  241. this.remainingTime = this.answerBook.remainingTime
  242. this.$store.commit('Test/updateAnswerBook', this.answerBook)
  243. this.startTimer()
  244. },
  245. getTestQuestions (fresh = false) {
  246. this.loadAnswerBookFromStore()
  247. if (this.storeAnswerBook && this.storeAnswerBook.id && !fresh) {
  248. this.initPageFromAnswerBook(this.storeAnswerBook, false)
  249. return
  250. }
  251. this.answerBook.loading = true
  252. APIGateway.answerBook.getTestQuestions(this.$route.params.answer_book_id)
  253. .then((answerBook) => {
  254. this.initPageFromAnswerBook(answerBook)
  255. })
  256. .catch(() => {
  257. this.answerBook.loading = false
  258. this.backToClassList()
  259. })
  260. }
  261. }
  262. }
  263. </script>
  264. <style scoped lang="scss">
  265. .TestParticipation {
  266. :deep(.q-banner) {
  267. background: #FCF9F4 0 0 no-repeat padding-box;
  268. padding: 20px;
  269. .q-banner__content {
  270. font-size: 16px;
  271. font-weight: normal;
  272. }
  273. }
  274. }
  275. </style>