TestParticipation.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  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. tabHiddenTime: null,
  105. lastIntervalTime: Date.now()
  106. }
  107. },
  108. computed: {
  109. isCompletionPage () {
  110. return this.$route.name === 'UserPanel.Test.AnswerBook.Participate.SingleQuestion' && this.$route.params.question_number === 'complete'
  111. },
  112. formattedRemainingTime () {
  113. const hours = Math.max(Math.floor(this.remainingTime / 3600), 0)
  114. const minutes = Math.max(Math.floor((this.remainingTime % 3600) / 60), 0)
  115. const seconds = Math.max(this.remainingTime % 60, 0)
  116. // Add zero padding for hours, minutes, and seconds
  117. const formattedHours = hours.toString().padStart(2, '0')
  118. const formattedMinutes = minutes.toString().padStart(2, '0')
  119. const formattedSeconds = seconds.toString().padStart(2, '0')
  120. return formattedHours + ':' + formattedMinutes + ':' + formattedSeconds
  121. }
  122. },
  123. mounted () {
  124. this.getTestQuestions()
  125. this.mounted = true
  126. this.$bus.on('test-participate-single-question-sent-success', () => {
  127. this.onSingleQuestionsSentSuccess()
  128. })
  129. this.$bus.on('test-participate-single-question-sent-failed', () => {
  130. this.onSingleQuestionsSentFailed()
  131. })
  132. this.$bus.on('test-participate-all-question-sent-success', () => {
  133. this.onAllQuestionsSentSuccess()
  134. })
  135. this.$bus.on('test-participate-all-question-sent-failed', () => {
  136. this.onAllQuestionsSentFailed()
  137. })
  138. this.setupVisibilityChangeListener()
  139. },
  140. beforeUnmount() {
  141. this.stopTimer()
  142. this.removeVisibilityChangeListener()
  143. },
  144. methods: {
  145. loadAnswerBookFromStore () {
  146. this.storeAnswerBook = this.$store.getters['Test/answerBook']
  147. },
  148. onAllQuestionsSending () {
  149. this.answerBook.loading = true
  150. },
  151. onAllQuestionsSentSuccess () {
  152. this.answerBook.loading = false
  153. this.getTestQuestions(true)
  154. },
  155. onAllQuestionsSentFailed () {
  156. this.answerBook.loading = false
  157. },
  158. onSingleQuestionsSending () {
  159. this.answerBook.loading = true
  160. },
  161. onSingleQuestionsSentSuccess () {
  162. this.answerBook.loading = false
  163. // this.backToClassList()
  164. },
  165. onSingleQuestionsSentFailed () {
  166. this.answerBook.loading = false
  167. },
  168. toShamsi (miladi) {
  169. return Assist.miladiToShamsi(miladi)
  170. },
  171. startTest () {
  172. this.$router.push({ name: this.participateType, params: { question_number: 1 } })
  173. },
  174. confirmAnswers () {
  175. this.$q.dialog({
  176. title: 'اتمام آزمون',
  177. message: 'آیا از اتمام آزمون و تایید پاسخ های خود اطمینان دارید؟',
  178. cancel: true,
  179. persistent: true
  180. }).onOk(() => {
  181. APIGateway.answerBook.confirmAnswers(this.answerBook.id)
  182. .then(() => {
  183. this.$q.notify({
  184. message: 'تایید آزمون با موفقیت انجام شد.',
  185. type: 'positive'
  186. })
  187. this.$router.push({
  188. name: 'UserPanel.Test.AnswerBook.Participate.SingleQuestion',
  189. params: {
  190. test_id: this.$route.params.test_id,
  191. answer_book_id: this.$route.params.answer_book_id,
  192. question_number: 'complete'
  193. }
  194. })
  195. })
  196. .catch(() => {})
  197. }).onCancel(() => {})
  198. },
  199. getRemainingTimeInSeconds (serverTimeStr, attendingStartTimeStr, durationInMinutes) {
  200. // Convert the strings to Date objects
  201. const serverTime = new Date(serverTimeStr)
  202. const attendingStartTime = new Date(attendingStartTimeStr)
  203. // Calculate the difference in milliseconds
  204. const timeDiffInMilliseconds = serverTime - attendingStartTime
  205. // Convert the difference to seconds
  206. const timeDiffInSeconds = timeDiffInMilliseconds / 1000
  207. // Calculate the remaining time in seconds considering the duration
  208. return Math.floor((durationInMinutes * 60) - timeDiffInSeconds)
  209. },
  210. onTimeout () {
  211. this.$q.notify({
  212. type: 'negative',
  213. message: 'زمان آزمون به اتمام رسیده است.'
  214. })
  215. this.backToClassList()
  216. },
  217. startTimer () {
  218. this.stopTimer()
  219. this.lastIntervalTime = Date.now() // Initialize the last interval time
  220. this.timerInterval = setInterval(() => {
  221. const now = Date.now()
  222. const timeDiff = now - this.lastIntervalTime
  223. // Check if more than 2 seconds have passed since the last interval
  224. if (timeDiff > 2000) {
  225. this.stopTimer()
  226. this.getTestQuestions(true)
  227. }
  228. // Update the last interval time
  229. this.lastIntervalTime = now
  230. // Handle the timer logic
  231. if (this.remainingTime <= 0) {
  232. this.stopTimer()
  233. this.onTimeout()
  234. } else {
  235. this.remainingTime--
  236. this.answerBook.remainingTime = this.remainingTime
  237. this.$store.commit('Test/updateAnswerBook', this.answerBook)
  238. }
  239. this.tabHiddenTime = Date.now()
  240. }, 1000)
  241. },
  242. stopTimer () {
  243. if (!this.timerInterval) {
  244. return
  245. }
  246. clearInterval(this.timerInterval)
  247. this.timerInterval = null
  248. },
  249. backToClassList () {
  250. this.$router.push({ name: 'UserPanel.Profile.AllClassrooms' })
  251. },
  252. initPageFromAnswerBook (answerBook) {
  253. this.answerBook = new AnswerBook(answerBook)
  254. this.answerBook.loading = false
  255. if (this.answerBook.remainingTime === null) {
  256. this.answerBook.remainingTime = this.getRemainingTimeInSeconds(this.answerBook.server_time, this.answerBook.attending_start_time, this.answerBook.test_info.duration_deadline)
  257. }
  258. this.remainingTime = this.answerBook.remainingTime
  259. this.$store.commit('Test/updateAnswerBook', this.answerBook)
  260. this.startTimer()
  261. },
  262. getTestQuestions (fresh = false) {
  263. this.loadAnswerBookFromStore()
  264. if (this.storeAnswerBook && this.storeAnswerBook.id && !fresh) {
  265. this.initPageFromAnswerBook(this.storeAnswerBook, false)
  266. return
  267. }
  268. this.answerBook.loading = true
  269. APIGateway.answerBook.getTestQuestions(this.$route.params.answer_book_id)
  270. .then((answerBook) => {
  271. this.initPageFromAnswerBook(answerBook)
  272. })
  273. .catch(() => {
  274. this.answerBook.loading = false
  275. this.backToClassList()
  276. })
  277. },
  278. setupVisibilityChangeListener () {
  279. document.addEventListener('visibilitychange', this.handleVisibilityChange)
  280. },
  281. removeVisibilityChangeListener () {
  282. document.removeEventListener('visibilitychange', this.handleVisibilityChange)
  283. },
  284. handleVisibilityChange () {
  285. if (document.hidden) {
  286. // Tab is hidden, record the time
  287. this.tabHiddenTime = Date.now()
  288. } else {
  289. // Tab is visible again
  290. if (this.tabHiddenTime && Date.now() - this.tabHiddenTime > 2000) {
  291. // User was away for more than 2 seconds
  292. this.stopTimer() // Stop the timer
  293. this.$q.notify({
  294. type: 'warning',
  295. message: 'شما بیش از ۲ ثانیه از تب خارج شدید.'
  296. })
  297. this.getTestQuestions(true)
  298. }
  299. this.tabHiddenTime = null // Reset the hidden time
  300. }
  301. }
  302. }
  303. }
  304. </script>
  305. <style scoped lang="scss">
  306. .TestParticipation {
  307. :deep(.q-banner) {
  308. background: #FCF9F4 0 0 no-repeat padding-box;
  309. padding: 20px;
  310. .q-banner__content {
  311. font-size: 16px;
  312. font-weight: normal;
  313. }
  314. }
  315. }
  316. </style>