remind.vue 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. <template>
  2. <view class="container">
  3. <view class="content">
  4. <!-- 装饰背景 -->
  5. <view class="decor-bg"></view>
  6. <!-- 加载状态 -->
  7. <view class="loading-tip" v-if="loading">
  8. <text>加载中...</text>
  9. </view>
  10. <view class="info-card" v-else>
  11. <view class="info-list">
  12. <!-- 测评岗位 -->
  13. <view class="info-item">
  14. <view class="label-wrap">
  15. <view class="dot"></view>
  16. <text class="label">测评岗位</text>
  17. </view>
  18. <text class="value">{{ examInfo.position || '—' }}</text>
  19. </view>
  20. <!-- 考题题型(从abilityConfigs汇总) -->
  21. <view class="info-item" v-if="examInfo.questionTypes">
  22. <view class="label-wrap">
  23. <view class="dot"></view>
  24. <text class="label">考题题型</text>
  25. </view>
  26. <text class="value">{{ examInfo.questionTypes }}</text>
  27. </view>
  28. <!-- 考试时间 -->
  29. <view class="info-item" v-if="examInfo.totalTime">
  30. <view class="label-wrap">
  31. <view class="dot"></view>
  32. <text class="label">考试时间</text>
  33. </view>
  34. <text class="value">{{ examInfo.totalTime }}分钟</text>
  35. </view>
  36. <!-- 合格分数(遍历每个能力配置) -->
  37. <view class="info-item border-none" v-if="examInfo.passMark">
  38. <view class="label-wrap">
  39. <view class="dot"></view>
  40. <text class="label">合格标准</text>
  41. </view>
  42. <view class="value-group">
  43. <text class="value">满分{{ examInfo.totalScore }}分,{{ examInfo.passMark }}分及格</text>
  44. <!-- 各能力及格线(当多于1个能力时展示) -->
  45. <text class="sub-value" v-if="examInfo.abilityPassDesc">{{ examInfo.abilityPassDesc }}</text>
  46. </view>
  47. </view>
  48. </view>
  49. </view>
  50. <!-- 按钮区域 -->
  51. <view class="action-area">
  52. <button class="start-btn" @click="startQuiz" :disabled="loading">确认开始测评</button>
  53. <text class="bottom-tip">请确保在安静、网络稳定的环境下进行</text>
  54. </view>
  55. </view>
  56. </view>
  57. </template>
  58. <script setup lang="js">
  59. import { onLoad } from '@dcloudio/uni-app';
  60. import { ref } from 'vue';
  61. import { getAssessmentDetail, getAssessmentList, createExamApply } from '../../api/assessment.js';
  62. const source = ref('');
  63. const assessmentId = ref('');
  64. const loading = ref(true);
  65. // 考试汇总信息
  66. const examInfo = ref({
  67. position: '', // 测评岗位
  68. questionTypes: '', // 题型描述
  69. totalTime: 0, // 总时长(分钟)
  70. totalScore: 0, // 总分
  71. passMark: 0, // 合格分
  72. abilityPassDesc: '', // 各能力子合格描述
  73. });
  74. // 保存第一个有链接的静态考试链接(兜底用)
  75. const firstExamLink = ref('');
  76. onLoad(async (options) => {
  77. if (options.source) source.value = options.source;
  78. if (options.id) assessmentId.value = options.id;
  79. // 加载测评详情
  80. await loadExamInfo();
  81. });
  82. /**
  83. * 从后端加载测评详情并解析考试配置
  84. */
  85. const loadExamInfo = async () => {
  86. loading.value = true;
  87. try {
  88. let data = null;
  89. if (assessmentId.value) {
  90. // 优先用传入的测评ID查详情
  91. const res = await getAssessmentDetail(assessmentId.value);
  92. if (res.code === 200 && res.data) {
  93. data = res.data;
  94. }
  95. }
  96. if (!data) {
  97. // 无ID时兜底:取第一个测评
  98. const listRes = await getAssessmentList({ pageNum: 1, pageSize: 1 });
  99. if (listRes.code === 200 && Array.isArray(listRes.rows) && listRes.rows.length > 0) {
  100. const detailRes = await getAssessmentDetail(listRes.rows[0].id);
  101. if (detailRes.code === 200 && detailRes.data) {
  102. data = detailRes.data;
  103. // 如果原来没有 assessmentId,使用兜底的ID
  104. if (!assessmentId.value) {
  105. assessmentId.value = listRes.rows[0].id;
  106. console.log('使用兜底测评ID:', assessmentId.value);
  107. }
  108. }
  109. }
  110. }
  111. if (data) {
  112. // 解析岗位信息
  113. examInfo.value.position = data.position || data.evaluationName || '—';
  114. // 解析能力配置列表
  115. const abilityConfigs = Array.isArray(data.abilityConfigs) ? data.abilityConfigs : [];
  116. if (abilityConfigs.length > 0) {
  117. // 计算总时长(各能力时长之和)
  118. const totalTime = abilityConfigs.reduce((sum, c) => sum + (c.thirdExamTime || 0), 0);
  119. examInfo.value.totalTime = totalTime;
  120. // 计算总分
  121. const totalScore = abilityConfigs.reduce((sum, c) => sum + (c.thirdExamTotalScore || 0), 0);
  122. examInfo.value.totalScore = totalScore;
  123. // 及格分(汇总所有能力的及格分)
  124. const passMark = abilityConfigs.reduce((sum, c) => sum + (c.thirdExamPassMark || 0), 0);
  125. examInfo.value.passMark = passMark;
  126. // 多能力时:展示各能力及格线描述
  127. if (abilityConfigs.length > 1) {
  128. const descs = abilityConfigs.map(c => `${c.abilityName || c.thirdExamName || '能力'}及格${c.thirdExamPassMark || 0}分`);
  129. examInfo.value.abilityPassDesc = descs.join(',');
  130. }
  131. // 固定题型描述
  132. examInfo.value.questionTypes = '单选/多选/问答';
  133. // 缓存第一个有效静态链接(兜底用)
  134. const firstConfig = abilityConfigs.find(c => c && c.thirdExamLink);
  135. if (firstConfig) {
  136. firstExamLink.value = firstConfig.thirdExamLink;
  137. }
  138. }
  139. }
  140. } catch (err) {
  141. console.error('加载测评信息失败:', err);
  142. } finally {
  143. loading.value = false;
  144. }
  145. };
  146. /**
  147. * 点击确认开始测评
  148. */
  149. const startQuiz = async () => {
  150. try {
  151. if (!firstExamLink.value && !assessmentId.value) {
  152. uni.showToast({ title: '未配置考试链接', icon: 'none' });
  153. return;
  154. }
  155. // 获取当前登录用户信息
  156. const userInfo = uni.getStorageSync('userInfo') || {};
  157. const studentId = userInfo.studentId;
  158. if (!studentId) {
  159. uni.showToast({ title: '请先登录', icon: 'none' });
  160. return;
  161. }
  162. // 如果有 assessmentId,先创建测评申请记录
  163. if (assessmentId.value) {
  164. try {
  165. uni.showLoading({ title: '创建测评申请...' });
  166. console.log('准备创建测评申请,evaluationId:', assessmentId.value, 'studentId:', studentId);
  167. // 创建测评申请记录
  168. const applyRes = await createExamApply(assessmentId.value, studentId);
  169. console.log('测评申请创建结果:', applyRes);
  170. uni.hideLoading();
  171. if (applyRes.code !== 200) {
  172. uni.showToast({ title: applyRes.msg || '创建测评申请失败', icon: 'none' });
  173. return;
  174. }
  175. console.log('测评申请创建成功,申请ID:', applyRes.data.id);
  176. } catch (apiError) {
  177. uni.hideLoading();
  178. console.error('创建测评申请失败:', apiError);
  179. // 创建申请失败不阻止进入考试,只是记录日志
  180. console.warn('测评申请创建失败,但仍允许进入考试');
  181. }
  182. }
  183. // 保持原有的跳转逻辑
  184. uni.navigateTo({
  185. url: `/pages/common/webview?mode=kaoshixing&assessmentId=${encodeURIComponent(assessmentId.value || '')}&fallbackUrl=${encodeURIComponent(firstExamLink.value || '')}`
  186. });
  187. } catch (error) {
  188. console.error('打开考试链接失败:', error);
  189. uni.showToast({ title: '打开考试链接失败', icon: 'none' });
  190. }
  191. };
  192. </script>
  193. <style lang="scss" scoped>
  194. .container {
  195. min-height: 100vh;
  196. background-color: #F8F9FB;
  197. display: flex;
  198. flex-direction: column;
  199. }
  200. .content {
  201. flex: 1;
  202. padding: 30rpx 40rpx;
  203. position: relative;
  204. overflow: hidden;
  205. }
  206. .loading-tip {
  207. display: flex; justify-content: center; align-items: center;
  208. padding: 80rpx 0; font-size: 28rpx; color: #999;
  209. }
  210. .decor-bg {
  211. position: absolute; top: -100rpx; right: -100rpx; width: 400rpx; height: 400rpx;
  212. background: radial-gradient(circle, rgba(31,108,255,0.05) 0%, transparent 70%);
  213. z-index: 0;
  214. }
  215. .info-card {
  216. background: #FFF;
  217. border-radius: 32rpx;
  218. padding: 40rpx;
  219. box-shadow: 0 8rpx 30rpx rgba(0,0,0,0.02);
  220. position: relative;
  221. z-index: 1;
  222. }
  223. .info-list {
  224. display: flex;
  225. flex-direction: column;
  226. }
  227. .info-item {
  228. display: flex;
  229. align-items: center;
  230. padding: 36rpx 0;
  231. border-bottom: 2rpx solid #F5F7FA;
  232. &.border-none { border-bottom: none; }
  233. .label-wrap {
  234. display: flex;
  235. align-items: center;
  236. width: 200rpx;
  237. flex-shrink: 0;
  238. .dot { width: 8rpx; height: 8rpx; background: #1F6CFF; border-radius: 50%; margin-right: 16rpx; }
  239. .label { font-size: 30rpx; color: #666; font-weight: 500; }
  240. }
  241. .value {
  242. font-size: 30rpx;
  243. color: #1A1A1A;
  244. font-weight: bold;
  245. flex: 1;
  246. }
  247. .value-group {
  248. display: flex;
  249. flex-direction: column;
  250. gap: 8rpx;
  251. .sub-value { font-size: 24rpx; color: #999; font-weight: normal; }
  252. }
  253. }
  254. .action-area {
  255. margin-top: 80rpx;
  256. display: flex;
  257. flex-direction: column;
  258. align-items: center;
  259. position: relative;
  260. z-index: 1;
  261. .start-btn {
  262. width: 100%;
  263. height: 100rpx;
  264. background: linear-gradient(135deg, #1F6CFF 0%, #0056FF 100%);
  265. color: #FFFFFF;
  266. border-radius: 50rpx;
  267. display: flex;
  268. align-items: center;
  269. justify-content: center;
  270. font-size: 32rpx;
  271. font-weight: bold;
  272. box-shadow: 0 12rpx 24rpx rgba(31, 108, 255, 0.2);
  273. margin-bottom: 30rpx;
  274. &::after { border: none; }
  275. &:active { transform: scale(0.98); opacity: 0.9; }
  276. }
  277. .bottom-tip { font-size: 24rpx; color: #999; }
  278. }
  279. </style>