assessment-records.vue 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. <template>
  2. <view class="container">
  3. <scroll-view class="records-scroll" scroll-y>
  4. <view v-if="loading" class="state-box">
  5. <text class="state-text">加载中...</text>
  6. </view>
  7. <view v-else-if="records.length === 0" class="state-box">
  8. <text class="state-text">暂无测评记录</text>
  9. </view>
  10. <view v-else class="records-list">
  11. <view v-for="(record, index) in records" :key="index" class="record-card">
  12. <view class="card-header">
  13. <view class="header-item">
  14. <text class="score">{{ record.score }}</text>
  15. <text class="label">得分</text>
  16. </view>
  17. <view class="header-item">
  18. <text class="time">{{ record.duration }}</text>
  19. <text class="label">答题用时</text>
  20. </view>
  21. <view class="detail-link" @click="viewDetail(record)">
  22. <text>测评情况</text>
  23. <image src="/static/icons/chevron-right-blue.svg" class="arrow"></image>
  24. </view>
  25. </view>
  26. <view class="card-footer">
  27. <view class="type-box">
  28. <text class="type-name">{{ record.typeName }}</text>
  29. <text :class="['status-tag', record.status]">{{ record.statusText }}</text>
  30. </view>
  31. <text class="date">{{ record.date }}</text>
  32. </view>
  33. </view>
  34. </view>
  35. <view v-if="!loading && records.length > 0" class="no-more">—— 已到底啦~ ——</view>
  36. </scroll-view>
  37. </view>
  38. </template>
  39. <script setup>
  40. import { ref } from 'vue';
  41. import { onShow, onPullDownRefresh } from '@dcloudio/uni-app';
  42. import { getAssessmentRecordList } from '../../api/assessment.js';
  43. const loading = ref(false);
  44. const records = ref([]);
  45. const getStatusMeta = (item) => {
  46. if (item.statusType && item.statusText) {
  47. return {
  48. status: item.statusType,
  49. statusText: item.statusText
  50. };
  51. }
  52. if (item.finalResult === '1') {
  53. return { status: 'pass', statusText: '通过' };
  54. }
  55. if (item.finalResult === '2') {
  56. return { status: 'fail', statusText: '未通过' };
  57. }
  58. if (item.applyStatus === '2') {
  59. return { status: 'pending', statusText: '待评分' };
  60. }
  61. if (item.applyStatus === '1') {
  62. return { status: 'pending', statusText: '测评中' };
  63. }
  64. return { status: 'pending', statusText: '待测评' };
  65. };
  66. const formatDuration = (value) => {
  67. if (value === null || value === undefined || value === '') {
  68. return '--';
  69. }
  70. const duration = String(value).trim();
  71. if (duration.includes(':')) {
  72. return duration;
  73. }
  74. const totalMinutes = Number(duration);
  75. if (Number.isNaN(totalMinutes)) {
  76. return duration;
  77. }
  78. const hours = Math.floor(totalMinutes / 60);
  79. const minutes = totalMinutes % 60;
  80. if (hours > 0) {
  81. return `${hours}小时${minutes}分钟`;
  82. }
  83. return `${minutes}分钟`;
  84. };
  85. const pickRecordDate = (item) => {
  86. return item.finishedTime || item.deadlineTime || item.scheduleStartTime || item.createTime || '--';
  87. };
  88. const calcDuration = (item) => {
  89. const start = item.scheduleStartTime;
  90. const end = item.finishedTime;
  91. if (!start || !end) return '--';
  92. const ms = new Date(end) - new Date(start);
  93. if (isNaN(ms) || ms < 0) return '--';
  94. const totalMinutes = Math.floor(ms / 60000);
  95. const hours = Math.floor(totalMinutes / 60);
  96. const minutes = totalMinutes % 60;
  97. if (hours > 0) {
  98. return `${hours}小时${minutes}分钟`;
  99. }
  100. return `${minutes}分钟`;
  101. };
  102. const normalizeRecord = (item) => {
  103. const statusMeta = getStatusMeta(item);
  104. return {
  105. ...item,
  106. score: item.finalResult === '1' ? '通过' : item.finalResult === '2' ? '未通过' : '--',
  107. duration: calcDuration(item),
  108. typeName: item.evaluationName || '未命名测评',
  109. status: statusMeta.status,
  110. statusText: statusMeta.statusText,
  111. date: pickRecordDate(item)
  112. };
  113. };
  114. const loadRecords = async () => {
  115. const userInfo = uni.getStorageSync('userInfo') || {};
  116. const userId = userInfo.studentId || userInfo.id;
  117. if (!userId) {
  118. records.value = [];
  119. uni.showToast({ title: '用户信息失效,请重新登录', icon: 'none' });
  120. return;
  121. }
  122. loading.value = true;
  123. try {
  124. const res = await getAssessmentRecordList(userId);
  125. const rows = Array.isArray(res?.data) ? res.data : [];
  126. records.value = rows.map(normalizeRecord);
  127. } catch (error) {
  128. console.error('加载测评记录失败:', error);
  129. records.value = [];
  130. } finally {
  131. loading.value = false;
  132. }
  133. };
  134. onShow(() => {
  135. loadRecords();
  136. });
  137. onPullDownRefresh(async () => {
  138. await loadRecords();
  139. uni.stopPullDownRefresh();
  140. });
  141. const viewDetail = (record) => {
  142. if (!record.evaluationId) {
  143. uni.showToast({ title: '数据异常', icon: 'none' });
  144. return;
  145. }
  146. // 跳转到图形化报告页,传入测评ID
  147. uni.navigateTo({
  148. url: `/pages/assessment/report?id=${record.evaluationId}`
  149. });
  150. };
  151. </script>
  152. <style lang="scss" scoped>
  153. .container {
  154. min-height: 100vh;
  155. background-color: #F8F9FB;
  156. }
  157. .state-box {
  158. display: flex;
  159. justify-content: center;
  160. align-items: center;
  161. min-height: 320rpx;
  162. }
  163. .state-text {
  164. font-size: 28rpx;
  165. color: #999;
  166. }
  167. .records-list {
  168. padding: 30rpx;
  169. }
  170. .record-card {
  171. background: #FFF;
  172. border-radius: 20rpx;
  173. padding: 30rpx;
  174. margin-bottom: 30rpx;
  175. box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.02);
  176. .card-header {
  177. display: flex;
  178. align-items: center;
  179. padding-bottom: 24rpx;
  180. border-bottom: 1rpx solid #F0F2F5;
  181. margin-bottom: 24rpx;
  182. .header-item {
  183. flex: 1;
  184. display: flex;
  185. flex-direction: column;
  186. .score, .time {
  187. font-size: 36rpx;
  188. font-weight: bold;
  189. color: #1A1A1A;
  190. margin-bottom: 4rpx;
  191. }
  192. .label {
  193. font-size: 24rpx;
  194. color: #999;
  195. }
  196. }
  197. .detail-link {
  198. display: flex;
  199. align-items: center;
  200. font-size: 26rpx;
  201. color: #1F6CFF;
  202. .arrow { width: 24rpx; height: 24rpx; margin-left: 4rpx; }
  203. }
  204. }
  205. .card-footer {
  206. display: flex;
  207. justify-content: space-between;
  208. align-items: center;
  209. .type-box {
  210. display: flex;
  211. align-items: center;
  212. .type-name { font-size: 28rpx; color: #333; font-weight: 500; margin-right: 16rpx; }
  213. .status-tag {
  214. font-size: 22rpx;
  215. padding: 4rpx 16rpx;
  216. border-radius: 8rpx;
  217. &.pending { background: #FFF7E6; color: #FAAD14; }
  218. &.pass { background: #F6FFED; color: #52C41A; }
  219. &.fail { background: #FFF1F0; color: #FF4D4F; }
  220. }
  221. }
  222. .date { font-size: 24rpx; color: #CCC; }
  223. }
  224. }
  225. .no-more {
  226. text-align: center;
  227. font-size: 24rpx;
  228. color: #CCC;
  229. padding: 40rpx 0 80rpx;
  230. }
  231. </style>