TestParticipation.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  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, testEndTimeStr) {
  200. // Convert the strings to Date objects
  201. const serverTime = new Date(serverTimeStr)
  202. const attendingStartTime = new Date(attendingStartTimeStr)
  203. const testEndTime = new Date(testEndTimeStr)
  204. // Calculate the difference in milliseconds
  205. const timeDiffInMilliseconds = serverTime - attendingStartTime
  206. // Convert the difference to seconds
  207. const timeDiffInSeconds = timeDiffInMilliseconds / 1000
  208. if (new Date(serverTime.getTime() + durationInMinutes * 1000 * 60) > testEndTime) {
  209. return Math.floor((testEndTime - serverTime) / 1000)
  210. }
  211. // Calculate the remaining time in seconds considering the duration
  212. return Math.floor((durationInMinutes * 60) - timeDiffInSeconds)
  213. },
  214. onTimeout () {
  215. this.$q.notify({
  216. type: 'negative',
  217. message: 'زمان آزمون به اتمام رسیده است.'
  218. })
  219. this.backToClassList()
  220. },
  221. startTimer () {
  222. this.stopTimer()
  223. this.lastIntervalTime = Date.now() // Initialize the last interval time
  224. this.timerInterval = setInterval(() => {
  225. const now = Date.now()
  226. const timeDiff = now - this.lastIntervalTime
  227. // Check if more than 2 seconds have passed since the last interval
  228. if (timeDiff > 2000) {
  229. this.stopTimer()
  230. this.getTestQuestions(true)
  231. }
  232. // Update the last interval time
  233. this.lastIntervalTime = now
  234. // Handle the timer logic
  235. if (this.remainingTime <= 0) {
  236. this.stopTimer()
  237. this.onTimeout()
  238. } else {
  239. this.remainingTime--
  240. this.answerBook.remainingTime = this.remainingTime
  241. this.$store.commit('Test/updateAnswerBook', this.answerBook)
  242. }
  243. this.tabHiddenTime = Date.now()
  244. }, 1000)
  245. },
  246. stopTimer () {
  247. if (!this.timerInterval) {
  248. return
  249. }
  250. clearInterval(this.timerInterval)
  251. this.timerInterval = null
  252. },
  253. backToClassList () {
  254. this.$router.push({ name: 'UserPanel.Profile.AllClassrooms' })
  255. },
  256. initPageFromAnswerBook (answerBook) {
  257. this.answerBook = new AnswerBook(answerBook)
  258. this.answerBook.loading = false
  259. if (this.answerBook.remainingTime === null) {
  260. this.answerBook.remainingTime = this.getRemainingTimeInSeconds(this.answerBook.server_time, this.answerBook.attending_start_time, this.answerBook.test_info.duration_deadline, this.answerBook.test_info.end_time)
  261. }
  262. this.remainingTime = this.answerBook.remainingTime
  263. this.$store.commit('Test/updateAnswerBook', this.answerBook)
  264. this.startTimer()
  265. },
  266. getTestQuestions (fresh = false) {
  267. this.loadAnswerBookFromStore()
  268. if (this.storeAnswerBook && this.storeAnswerBook.id && !fresh) {
  269. this.initPageFromAnswerBook(this.storeAnswerBook, false)
  270. return
  271. }
  272. this.answerBook.loading = true
  273. APIGateway.answerBook.getTestQuestions(this.$route.params.answer_book_id)
  274. .then((answerBook) => {
  275. this.initPageFromAnswerBook(answerBook)
  276. })
  277. .catch(() => {
  278. this.answerBook.loading = false
  279. this.backToClassList()
  280. })
  281. },
  282. setupVisibilityChangeListener () {
  283. document.addEventListener('visibilitychange', this.handleVisibilityChange)
  284. },
  285. removeVisibilityChangeListener () {
  286. document.removeEventListener('visibilitychange', this.handleVisibilityChange)
  287. },
  288. handleVisibilityChange () {
  289. if (document.hidden) {
  290. // Tab is hidden, record the time
  291. this.tabHiddenTime = Date.now()
  292. } else {
  293. // Tab is visible again
  294. if (this.tabHiddenTime && Date.now() - this.tabHiddenTime > 2000) {
  295. // User was away for more than 2 seconds
  296. this.stopTimer() // Stop the timer
  297. this.$q.notify({
  298. type: 'warning',
  299. message: 'شما بیش از ۲ ثانیه از تب خارج شدید.'
  300. })
  301. this.getTestQuestions(true)
  302. }
  303. this.tabHiddenTime = null // Reset the hidden time
  304. }
  305. }
  306. }
  307. }
  308. </script>
  309. <style scoped lang="scss">
  310. .TestParticipation {
  311. :deep(.q-banner) {
  312. background: #FCF9F4 0 0 no-repeat padding-box;
  313. padding: 20px;
  314. .q-banner__content {
  315. font-size: 16px;
  316. font-weight: normal;
  317. }
  318. }
  319. }
  320. </style>