index.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631
  1. <!-- 在线考试列表 -->
  2. <template>
  3. <view class="safetyEducationExamination-exam">
  4. <!-- 顶部 Tab -->
  5. <view class="tab-bar">
  6. <view
  7. v-for="(tab, index) in tabs"
  8. :key="index"
  9. class="tab-item"
  10. :class="{ active: activeTab === index }"
  11. @click="switchTab(index)"
  12. >
  13. {{ tab }}
  14. </view>
  15. </view>
  16. <!-- 内容区 -->
  17. <scroll-view scroll-y class="scroll-content" @scrolltolower="onScrollToLower">
  18. <!-- 考试任务 -->
  19. <view v-if="activeTab === 0" class="panel">
  20. <view
  21. v-for="(item, index) in examTaskList"
  22. :key="index"
  23. class="exam-card"
  24. @click="goToExamDetail(item)"
  25. >
  26. <view class="card-top">
  27. <view class="card-top-left">
  28. <text class="tag tag-blue">{{ item.examTypeName }}</text>
  29. <view class="status-dot" :class="item.examStatus == 1 ? 'dot-green' : 'dot-orange'"></view>
  30. <text class="status-text" :class="item.examStatus == 1 ? 'text-gray' : 'text-orange'">
  31. {{ item.examStatus == 1 ? '未开始' : '进行中' }}
  32. </text>
  33. </view>
  34. <text class="arrow-icon">›</text>
  35. </view>
  36. <text class="exam-name">{{ item.examName }}</text>
  37. <!-- 有学时要求:三列 -->
  38. <view v-if="item.needStudy || item.examStudy || item.mockExam" class="info-grid col3">
  39. <view class="info-item">
  40. <text class="info-label">学时要求</text>
  41. <text class="info-value" :class="item.needStudy ? 'text-orange' : 'text-green'">
  42. {{ item.completedStudyHours }}/{{ item.studyHoursRequirement }}
  43. <text v-if="item.needStudy"> 未达标</text>
  44. <text v-else> 已达标</text>
  45. </text>
  46. </view>
  47. <view class="info-item">
  48. <text class="info-label">学习任务</text>
  49. <text class="info-value" :class="item.examStudy ? 'text-orange' : 'text-green'">
  50. {{ item.examStudy ? '待完成' : '已完成' }}
  51. </text>
  52. </view>
  53. <view class="info-item">
  54. <text class="info-label">模拟考试</text>
  55. <text class="info-value" :class="item.mockExam ? 'text-orange' : 'text-green'">
  56. {{ item.mockExam ? '待完成' : '已完成' }}
  57. </text>
  58. </view>
  59. </view>
  60. <!-- 无前置条件:两列 -->
  61. <view v-else class="info-grid col2">
  62. <view class="info-item">
  63. <text class="info-label">满分/及格</text>
  64. <text class="info-value">{{ item.totalScore }} / {{ item.passScore }} 分</text>
  65. </view>
  66. <view class="info-item">
  67. <text class="info-label">可考次数</text>
  68. <text class="info-value text-gray">{{ item.attemptLimit === 0 ? '不限' : item.attemptLimit + ' 次' }}</text>
  69. </view>
  70. </view>
  71. </view>
  72. <view v-if="!examTaskList[0] && !examTaskLoading" class="empty-tip">暂无考试任务</view>
  73. <view v-if="examTaskLoading" class="loading-tip">加载中...</view>
  74. </view>
  75. <!-- 模拟考试 -->
  76. <view v-if="activeTab === 1" class="panel">
  77. <view
  78. v-for="(item, index) in mockExamList"
  79. :key="index"
  80. class="exam-card"
  81. @click="goToMockDetail(item)"
  82. >
  83. <view class="card-top">
  84. <view class="card-top-left">
  85. <text class="tag tag-blue">{{ item.examTypeName }}</text>
  86. <text class="tag tag-orange" style="margin-left:8rpx;">模拟卷</text>
  87. </view>
  88. <text class="arrow-icon">›</text>
  89. </view>
  90. <text class="exam-name">{{ item.examName }}</text>
  91. <view class="info-grid col2">
  92. <view class="info-item-2">
  93. <span class="info-value" style="color:#8896a7;">最高成绩:
  94. <span class="info-value" :class="item.maxScore >= item.passScore ? 'text-green' : 'text-orange'">
  95. {{ item.maxScore > 0 ? item.maxScore + ' 分' : '未参加' }}
  96. </span></span>
  97. </view>
  98. <view class="info-item-2">
  99. <text class="info-value" style="text-align:right;color:#8896a7;">及格线:{{ item.passScore }} 分</text>
  100. </view>
  101. </view>
  102. </view>
  103. <view v-if="!mockExamList[0] && !mockExamLoading" class="empty-tip">暂无模拟考试</view>
  104. <view v-if="mockExamLoading" class="loading-tip">加载中...</view>
  105. </view>
  106. <!-- 我的成绩 -->
  107. <view v-if="activeTab === 2" class="panel">
  108. <!-- 筛选区 -->
  109. <view class="filter-row">
  110. <picker :range="resultOptions" :range-key="'label'" @change="onResultChange">
  111. <view class="picker-input" style="width:100rpx;">{{ resultOptions[resultIndex].label }} ▾</view>
  112. </picker>
  113. <picker mode="date" :value="startDate" @change="onStartDateChange">
  114. <view class="picker-input top-picker-input-box" :style="!startDate?'color:#999;font-size:22rpx;':''">{{ startDate?startDate:'请选择' }}</view>
  115. </picker>
  116. <text class="filter-sep">至</text>
  117. <picker mode="date" :value="endDate" @change="onEndDateChange" >
  118. <view class="picker-input top-picker-input-box" :style="!endDate?'color:#999;font-size:22rpx;':''">{{ endDate?endDate:'请选择' }}</view>
  119. </picker>
  120. <view class="top-right-box-button" @click="resetScoreList()">重置</view>
  121. </view>
  122. <!-- 考试类型标签 -->
  123. <scroll-view scroll-x class="tag-scroll">
  124. <view class="tag-scroll-inner">
  125. <text
  126. v-for="(tag, index) in examTypeTags"
  127. :key="index"
  128. class="filter-tag"
  129. :class="{ 'filter-tag-active': activeTagIndex === index }"
  130. @click="onTagChange(index)"
  131. >{{ tag.label }}</text>
  132. </view>
  133. </scroll-view>
  134. <!-- 成绩卡片 -->
  135. <view
  136. v-for="(item, index) in scoreList"
  137. :key="index"
  138. class="score-card"
  139. @click="goToScoreDetail(item)"
  140. >
  141. <view class="card-top">
  142. <text class="tag tag-blue">{{ item.examTypeName }}</text>
  143. <text class="score-result" :class="item.resultStatus == 2 ? 'result-pass' : 'result-fail'">
  144. {{ item.resultStatus == 2 ? '通过' : '未通过' }}
  145. </text>
  146. </view>
  147. <text class="score-exam-name">{{ item.examName }}</text>
  148. <view class="score-info-grid">
  149. <view><text class="text-gray">考试时间:</text>{{ item.startTime }}</view>
  150. <view><text class="text-gray">时长:</text>{{ item.answerDuration }} 分钟</view>
  151. <view><text class="text-gray">满分/及格:</text>{{ item.totalScore }} / {{ item.passScore }}</view>
  152. <view><text class="text-gray">考次:</text>{{ item.attemptCount }} / {{ item.attemptLimit === 0 ? '不限' : item.attemptLimit }}</view>
  153. <view><text class="text-gray">用时:</text>{{ item.myDuration || '-' }}</view>
  154. <view>
  155. <text class="text-gray">最高成绩:</text>
  156. <text :class="item.bestScore >= item.passScore ? 'text-green' : 'text-red'">{{ item.bestScore }} 分</text>
  157. </view>
  158. <view>
  159. <text class="text-gray">获得积分:</text>
  160. <text :class="item.myScore > 0 ? 'text-orange' : 'text-gray'">
  161. {{ item.myScore > 0 ? '+' + item.myScore + '分' : '无' }}
  162. </text>
  163. </view>
  164. <view>
  165. <text class="text-gray">考试结果:</text>
  166. <text :class="item.resultStatus === 1 ? 'text-green' : 'text-red'">
  167. {{ item.resultStatus === 1 ? '通过' : '未通过' }}
  168. </text>
  169. </view>
  170. </view>
  171. </view>
  172. <view v-if="!scoreList[0] && !scoreLoading" class="empty-tip">暂无成绩记录</view>
  173. <view v-if="scoreLoading" class="loading-tip">加载中...</view>
  174. <view v-if="scoreNoMore && scoreList[0]" class="loading-tip">没有更多了</view>
  175. </view>
  176. </scroll-view>
  177. </view>
  178. </template>
  179. <script>
  180. import {
  181. parseTime
  182. } from '@/component/public.js'
  183. import {
  184. examUserExamList,
  185. examUserExamMockList,
  186. examElExamUserResultMyScoreListPage,
  187. examUserExamTypeSelectExamType,
  188. } from '@/pages_safetyEducationExamination/api/index.js'
  189. export default {
  190. data() {
  191. return {
  192. tabs: ['考试任务', '模拟考试', '我的成绩'],
  193. activeTab: 0,
  194. // 考试任务
  195. examTaskList: [],
  196. examTaskLoading: false,
  197. // 模拟考试
  198. mockExamList: [],
  199. mockExamLoading: false,
  200. // 我的成绩
  201. scoreList: [],
  202. scoreLoading: false,
  203. scorePage: 1,
  204. scorePageSize: 10,
  205. scoreTotal: 0,
  206. scoreNoMore: false,
  207. // 筛选
  208. resultOptions: [
  209. { label: '全部', value: null },
  210. { label: '通过', value: 1 },
  211. { label: '未通过', value: 2 },
  212. ],
  213. resultIndex: 0,
  214. startDate: '',
  215. endDate: '',
  216. examTypeTags: [{ label: '全部', value: null }],
  217. activeTagIndex: 0,
  218. }
  219. },
  220. created() {
  221. this.loadExamTypeTags();
  222. },
  223. onShow() {
  224. this.refreshCurrentTab();
  225. },
  226. methods: {
  227. switchTab(index) {
  228. this.activeTab = index;
  229. this.refreshCurrentTab();
  230. },
  231. refreshCurrentTab() {
  232. if (this.activeTab === 0) {
  233. this.loadExamTaskList();
  234. } else if (this.activeTab === 1) {
  235. this.loadMockExamList();
  236. } else if (this.activeTab === 2) {
  237. this.resetScoreList();
  238. }
  239. },
  240. // 考试类型标签(用于我的成绩筛选)
  241. async loadExamTypeTags() {
  242. try {
  243. const { data } = await examUserExamTypeSelectExamType({});
  244. if (data.code === 200 && data.data) {
  245. const tags = (data.data || []).map(item => ({
  246. label: item.examTypeName,
  247. value: item.examTypeId,
  248. }));
  249. this.$set(this, 'examTypeTags', [{ label: '全部', value: null }, ...tags]);
  250. }
  251. } catch (e) {
  252. console.error('获取考试类型失败', e);
  253. }
  254. },
  255. // 考试任务列表
  256. async loadExamTaskList() {
  257. if (this.examTaskLoading) return;
  258. this.examTaskLoading = true;
  259. try {
  260. const { data } = await examUserExamList({ page: 1, pageSize: 50 });
  261. if (data.code === 200) {
  262. this.$set(this, 'examTaskList', data.data.records || []);
  263. }
  264. } catch (e) {
  265. console.error('获取考试任务失败', e);
  266. } finally {
  267. this.examTaskLoading = false;
  268. }
  269. },
  270. // 模拟考试列表
  271. async loadMockExamList() {
  272. if (this.mockExamLoading) return;
  273. this.mockExamLoading = true;
  274. try {
  275. const { data } = await examUserExamMockList({ page: 1, pageSize: 50 });
  276. if (data.code === 200) {
  277. this.$set(this, 'mockExamList', data.data.records || []);
  278. }
  279. } catch (e) {
  280. console.error('获取模拟考试失败', e);
  281. } finally {
  282. this.mockExamLoading = false;
  283. }
  284. },
  285. // 重置成绩列表(筛选条件变更时调用)
  286. resetScoreList() {
  287. this.scorePage = 1;
  288. this.scoreNoMore = false;
  289. this.startTime = '';
  290. this.endTime = '';
  291. this.$set(this, 'scoreList', []);
  292. this.loadScoreList();
  293. },
  294. // 我的成绩列表(支持分页)
  295. async loadScoreList() {
  296. if (this.scoreLoading || this.scoreNoMore) return;
  297. this.scoreLoading = true;
  298. try {
  299. const tag = this.examTypeTags[this.activeTagIndex];
  300. const result = this.resultOptions[this.resultIndex];
  301. const obj = {
  302. page: this.scorePage,
  303. pageSize: this.scorePageSize,
  304. examTypeId: tag.value || '',
  305. resultStatus: result.value || '',
  306. startTime: this.startDate?this.startDate + 'T00:00:00':'',
  307. endTime: this.endDate?this.endDate + 'T23:59:59':'',
  308. examScene:null
  309. };
  310. const { data } = await examElExamUserResultMyScoreListPage(obj);
  311. if (data.code === 200) {
  312. for(let i=0;i<data.data.records.length;i++){
  313. data.data.records[i].startTime = parseTime(data.data.records[i].startTime, "{y}-{m}-{d}")
  314. }
  315. const records = data.data.records || [];
  316. const newList = this.scorePage === 1 ? records : [...this.scoreList, ...records];
  317. this.$set(this, 'scoreList', newList);
  318. this.scoreTotal = data.data.total;
  319. if (newList.length >= this.scoreTotal) {
  320. this.scoreNoMore = true;
  321. } else {
  322. this.scorePage++;
  323. }
  324. }
  325. } catch (e) {
  326. console.error('获取成绩列表失败', e);
  327. } finally {
  328. this.scoreLoading = false;
  329. }
  330. },
  331. // 滚动到底部加载更多(我的成绩)
  332. onScrollToLower() {
  333. if (this.activeTab === 2) {
  334. this.loadScoreList();
  335. }
  336. },
  337. onResultChange(e) {
  338. this.resultIndex = e.detail.value;
  339. this.resetScoreList();
  340. },
  341. onStartDateChange(e) {
  342. this.startDate = e.detail.value;
  343. this.resetScoreList();
  344. },
  345. onEndDateChange(e) {
  346. this.endDate = e.detail.value;
  347. this.resetScoreList();
  348. },
  349. onTagChange(index) {
  350. this.activeTagIndex = index;
  351. this.resetScoreList();
  352. },
  353. goToExamDetail(item) {
  354. // 跳转考试详情,传递 examId / attemptId
  355. },
  356. goToMockDetail(item) {
  357. // 跳转模拟考试详情,传递 mockExamId
  358. },
  359. goToScoreDetail(item) {
  360. // 跳转成绩详情,传递 examId
  361. },
  362. },
  363. }
  364. </script>
  365. <style lang="stylus" scoped>
  366. .safetyEducationExamination-exam
  367. height 100%
  368. display flex
  369. flex-direction column
  370. overflow hidden
  371. background-color #f0f4fb
  372. // 顶部 Tab
  373. .tab-bar
  374. background #fff
  375. border-bottom 1rpx solid #e8edf3
  376. display flex
  377. flex-shrink 0
  378. .tab-item
  379. flex 1
  380. text-align center
  381. padding 18rpx 8rpx 16rpx
  382. font-size 26rpx
  383. color #888
  384. border-bottom 4rpx solid transparent
  385. .tab-item.active
  386. color #1677ff
  387. font-weight 600
  388. border-bottom-color #1677ff
  389. // 滚动区
  390. .scroll-content
  391. flex 1
  392. overflow hidden
  393. .panel
  394. padding 24rpx
  395. // 考试卡片
  396. .exam-card
  397. background #fff
  398. border-radius 20rpx
  399. padding 24rpx 28rpx
  400. margin-bottom 20rpx
  401. box-shadow 0 2rpx 12rpx rgba(0,0,0,0.04)
  402. border 1rpx solid #dde3ed
  403. .card-top
  404. display flex
  405. justify-content space-between
  406. align-items center
  407. margin-bottom 16rpx
  408. .card-top-left
  409. display flex
  410. align-items center
  411. .arrow-icon
  412. font-size 32rpx
  413. color #bbb
  414. // 标签
  415. .tag
  416. font-size 20rpx
  417. padding 4rpx 12rpx
  418. border-radius 8rpx
  419. border 1rpx solid transparent
  420. .tag-blue
  421. background #e6f4ff
  422. color #1677ff
  423. border-color #91caff
  424. .tag-orange
  425. background #fff7e6
  426. color #d46b08
  427. border-color #ffd591
  428. // 状态点
  429. .status-dot
  430. width 12rpx
  431. height 12rpx
  432. border-radius 50%
  433. margin-left 24rpx
  434. margin-right:10rpx;
  435. .dot-orange
  436. background #fa8c16
  437. .dot-green
  438. background #999
  439. .status-text
  440. font-size 22rpx
  441. margin-left 6rpx
  442. .text-orange
  443. color #fa8c16
  444. .text-gray
  445. color #999
  446. .text-green
  447. color #52c41a
  448. .text-blue
  449. color #1677ff
  450. .text-red
  451. color #f5222d
  452. // 考试名称
  453. .exam-name
  454. display block
  455. font-size 28rpx
  456. font-weight 600
  457. color #222
  458. margin-bottom 16rpx
  459. // 信息格
  460. .info-grid
  461. display grid
  462. gap 12rpx
  463. .col3
  464. grid-template-columns 1fr 1fr 1fr
  465. .col2
  466. grid-template-columns 1fr 1fr
  467. .info-item
  468. background #f4f6fa
  469. border-radius 12rpx
  470. padding 12rpx 16rpx
  471. display flex
  472. flex-direction column
  473. .info-item-2
  474. display flex
  475. flex-direction column
  476. .info-label
  477. font-size 20rpx
  478. color #999
  479. margin-bottom 8rpx
  480. .info-value
  481. font-size 22rpx
  482. // 筛选
  483. .filter-row
  484. display flex
  485. align-items center
  486. gap 12rpx
  487. padding 16rpx 0 8rpx
  488. flex-wrap nowrap
  489. .top-picker-input-box{
  490. width:190rpx;
  491. height:54rpx;
  492. padding:0;
  493. text-align: center;
  494. line-height:54rpx;
  495. }
  496. .top-right-box-button{
  497. border: 1rpx solid #dde3ed;
  498. border-radius:10rpx
  499. background:#fff;
  500. font-size:24rpx;
  501. width:100rpx;
  502. height:54rpx;
  503. padding:0;
  504. text-align: center;
  505. line-height:54rpx;
  506. }
  507. .picker-input
  508. font-size 24rpx
  509. color #333
  510. background #fff
  511. border 1rpx solid #dde3ed
  512. border-radius 10rpx
  513. padding 10rpx 16rpx
  514. .filter-sep
  515. font-size 22rpx
  516. color #999
  517. flex-shrink 0
  518. // 类型标签滚动
  519. .tag-scroll
  520. white-space nowrap
  521. padding 8rpx 0 16rpx
  522. .tag-scroll-inner
  523. display inline-flex
  524. gap 12rpx
  525. .filter-tag
  526. display inline-block
  527. font-size 22rpx
  528. padding 8rpx 20rpx
  529. border-radius 30rpx
  530. background #eef1f6
  531. color #666
  532. border 1rpx solid transparent
  533. .filter-tag-active
  534. background #e6f0ff
  535. color #1677ff
  536. border-color #91caff
  537. // 成绩卡片
  538. .score-card
  539. background #fff
  540. border-radius 20rpx
  541. padding 24rpx 28rpx
  542. margin-bottom 20rpx
  543. box-shadow 0 2rpx 12rpx rgba(0,0,0,0.04)
  544. border 1rpx solid #dde3ed
  545. .score-result
  546. font-size 22rpx
  547. font-weight 600
  548. padding 4rpx 16rpx
  549. border-radius 20rpx
  550. .result-pass
  551. background #f6ffed
  552. color #52c41a
  553. border 1rpx solid #b7eb8f
  554. .result-fail
  555. background #fff2f0
  556. color #f5222d
  557. border 1rpx solid #ffccc7
  558. .score-exam-name
  559. display block
  560. font-size 30rpx
  561. font-weight 700
  562. color #222
  563. margin 16rpx 0
  564. .score-info-grid
  565. display grid
  566. grid-template-columns 1fr 1fr
  567. gap 12rpx
  568. margin-bottom 20rpx
  569. font-size 26rpx
  570. color #333
  571. .empty-tip
  572. text-align center
  573. line-height 80rpx
  574. color #aaa
  575. font-size 24rpx
  576. .loading-tip
  577. text-align center
  578. line-height 60rpx
  579. color #aaa
  580. font-size 22rpx
  581. </style>