index.vue 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  1. <!-- 学时任务列表 -->
  2. <template>
  3. <view class="safetyEducationExamination-studyHourTask">
  4. <!-- 顶部类型标签 -->
  5. <scroll-view scroll-x class="tag-scroll" enable-flex>
  6. <view class="tag-scroll-inner">
  7. <text
  8. v-for="(tag, index) in typeTags"
  9. :key="index"
  10. class="filter-tag"
  11. :class="{ 'filter-tag-active': activeTagIndex === index }"
  12. @click="onTagChange(index)"
  13. >{{ tag.label }}</text>
  14. </view>
  15. </scroll-view>
  16. <!-- 课程列表 -->
  17. <scroll-view scroll-y class="scroll-content" @scrolltolower="onScrollToLower">
  18. <view class="panel">
  19. <view
  20. v-for="(item, index) in courseList"
  21. :key="index"
  22. class="course-card"
  23. @click="goCourseDetail(item)"
  24. >
  25. <view class="course-cover" :style="'background:' + item.coverGradient">
  26. <image v-if="item.materialType == 1 && item.exampleImg" :src="baseUrl + item.exampleImg" class="cover-image" mode="aspectFill" />
  27. <text v-else class="cover-icon">{{ item.coverIcon || '📖' }}</text>
  28. </view>
  29. <view class="course-info">
  30. <view class="course-info-top">
  31. <text class="course-name">{{ item.coursewareName }}</text>
  32. <text class="status-tag" :class="item.statusClass">{{ item.statusName }}</text>
  33. </view>
  34. <text class="course-meta">{{ formatDuration(item.minLearningDuration) }}分钟 | {{ item.creditHours }}学时 | {{ item.points }}积分</text>
  35. </view>
  36. </view>
  37. <view v-if="!courseList[0] && !loading" class="empty-tip">暂无学时任务</view>
  38. <view v-if="loading" class="loading-tip">加载中...</view>
  39. <view v-if="noMore && courseList[0]" class="loading-tip">没有更多了</view>
  40. </view>
  41. </scroll-view>
  42. </view>
  43. </template>
  44. <script>
  45. import {
  46. config
  47. } from '@/api/request/config.js'
  48. import {
  49. examUserCoursewareLearningList,
  50. examElKnowledgePointTreeList,
  51. } from '@/pages_safetyEducationExamination/api/index.js'
  52. // 封面渐变色池
  53. const COVER_GRADIENTS = [
  54. 'linear-gradient(135deg,#ffd666,#fa8c16)',
  55. 'linear-gradient(135deg,#b37feb,#722ed1)',
  56. 'linear-gradient(135deg,#ff9c6e,#f5222d)',
  57. 'linear-gradient(135deg,#5cdbd3,#13c2c2)',
  58. 'linear-gradient(135deg,#1857d4,#5b8af0)',
  59. 'linear-gradient(135deg,#10a37f,#34d399)',
  60. 'linear-gradient(135deg,#6c63ff,#a78bfa)',
  61. 'linear-gradient(135deg,#95de64,#237804)',
  62. ];
  63. // 学习状态映射
  64. function getStatusInfo(status) {
  65. const map = {
  66. 0: { name: '未开始', cls: 'tag-gray' },
  67. 1: { name: '学习中', cls: 'tag-orange' },
  68. 2: { name: '已完成', cls: 'tag-green' },
  69. };
  70. return map[status] || map[0];
  71. }
  72. export default {
  73. data() {
  74. return {
  75. baseUrl: config.base_url,
  76. typeTags: [],
  77. activeTagIndex: 0,
  78. courseList: [],
  79. loading: false,
  80. page: 1,
  81. pageSize: 10,
  82. total: 0,
  83. noMore: false,
  84. knowledgePointIds: null, // 从上个页面传来的知识点ID数组
  85. }
  86. },
  87. onLoad(options) {
  88. console.log('options',options);
  89. // 接收知识点ID参数
  90. if (options.knowledgePointId) {
  91. this.knowledgePointIds = options.knowledgePointId;
  92. }
  93. },
  94. created() {
  95. },
  96. async onShow() {
  97. await this.loadTypeTags();
  98. this.resetList();
  99. },
  100. methods: {
  101. // 递归扁平化树形结构,提取所有节点
  102. flattenTree(nodes, result = []) {
  103. if (!nodes || !Array.isArray(nodes)) return result;
  104. nodes.forEach(node => {
  105. result.push(node);
  106. if (node.children && node.children.length > 0) {
  107. this.flattenTree(node.children, result);
  108. }
  109. });
  110. return result;
  111. },
  112. // 加载知识点标签(根据传入的knowledgePointIds过滤)
  113. async loadTypeTags() {
  114. try {
  115. const { data } = await examElKnowledgePointTreeList({ dataScope: 1 });
  116. if (data.code === 200 && data.data) {
  117. // 扁平化树形结构,获取所有节点
  118. const allNodes = this.flattenTree(data.data);
  119. let tags = allNodes;
  120. // 如果有传入的知识点ID,则过滤标签
  121. if (this.knowledgePointIds) {
  122. const idsArray = this.knowledgePointIds.split(',').map(id => id.trim());
  123. tags = allNodes.filter(item => idsArray.includes(String(item.id)));
  124. }
  125. const typeTags = tags.map(item => ({
  126. label: item.knowledgePointName,
  127. value: item.id,
  128. }));
  129. this.$set(this, 'typeTags', typeTags);
  130. // 默认选中第一个
  131. if (typeTags.length > 0) {
  132. this.activeTagIndex = 0;
  133. }
  134. }
  135. } catch (e) {
  136. console.error('获取知识点类型失败', e);
  137. }
  138. },
  139. onTagChange(index) {
  140. if (this.activeTagIndex !== index) {
  141. this.activeTagIndex = index;
  142. this.resetList();
  143. }
  144. },
  145. resetList() {
  146. this.page = 1;
  147. this.noMore = false;
  148. this.courseList = [];
  149. this.loadCourseList();
  150. },
  151. // 学时任务列表(materialType为空,查询全部类型)
  152. async loadCourseList() {
  153. if (this.loading || this.noMore) return;
  154. this.loading = true;
  155. try {
  156. const tag = this.typeTags[this.activeTagIndex];
  157. const { data } = await examUserCoursewareLearningList({
  158. page: this.page,
  159. pageSize: this.pageSize,
  160. materialType: '', // 空字符串,查询全部类型
  161. knowledgePointId: tag ? tag.value : undefined,
  162. });
  163. if (data.code === 200) {
  164. const records = (data.data.records || []).map((item, i) => {
  165. const s = getStatusInfo(item.learnStatus);
  166. const colorIndex = (this.courseList.length + i) % COVER_GRADIENTS.length;
  167. return {
  168. ...item,
  169. coverGradient: COVER_GRADIENTS[colorIndex],
  170. statusName: s.name,
  171. statusClass: s.cls,
  172. };
  173. });
  174. const newList = this.page === 1 ? records : [...this.courseList, ...records];
  175. this.$set(this, 'courseList', newList);
  176. this.total = data.data.total;
  177. if (newList.length >= this.total) {
  178. this.noMore = true;
  179. } else {
  180. this.page++;
  181. }
  182. }
  183. } catch (e) {
  184. console.error('获取学时任务失败', e);
  185. } finally {
  186. this.loading = false;
  187. }
  188. },
  189. onScrollToLower() {
  190. this.loadCourseList();
  191. },
  192. // 秒数转分钟
  193. formatDuration(seconds) {
  194. return seconds ? Math.ceil(seconds / 60) : 0;
  195. },
  196. goCourseDetail(item) {
  197. // 跳转课程详情,传递 coursewareId
  198. },
  199. },
  200. }
  201. </script>
  202. <style lang="stylus" scoped>
  203. .safetyEducationExamination-studyHourTask
  204. height 100%
  205. display flex
  206. flex-direction column
  207. overflow hidden
  208. background-color #f0f4fb
  209. // 类型标签横滚
  210. .tag-scroll
  211. background #fff
  212. white-space nowrap
  213. padding 16rpx 24rpx
  214. border-bottom 1rpx solid #e8edf3
  215. flex-shrink 0
  216. .tag-scroll-inner
  217. display inline-flex
  218. gap 16rpx
  219. .filter-tag
  220. display inline-block
  221. font-size 22rpx
  222. padding 8rpx 20rpx
  223. border-radius 30rpx
  224. background #eef1f6
  225. color #666
  226. border 1rpx solid transparent
  227. white-space nowrap
  228. .filter-tag-active
  229. background #e6f0ff
  230. color #1677ff
  231. border-color #91caff
  232. font-weight 600
  233. // 滚动区
  234. .scroll-content
  235. flex 1
  236. overflow hidden
  237. .panel
  238. padding 24rpx
  239. // 课程卡片
  240. .course-card
  241. background #fff
  242. border-radius 20rpx
  243. margin-bottom 20rpx
  244. overflow hidden
  245. box-shadow 0 2rpx 12rpx rgba(0,0,0,0.04)
  246. border 1rpx solid #dde3ed
  247. .course-cover
  248. width 100%
  249. height 180rpx
  250. display flex
  251. align-items center
  252. justify-content center
  253. .cover-icon
  254. font-size 64rpx
  255. .cover-image
  256. width 100%
  257. height 100%
  258. .course-info
  259. padding 20rpx 24rpx
  260. .course-info-top
  261. display flex
  262. justify-content space-between
  263. align-items center
  264. margin-bottom 8rpx
  265. .course-name
  266. flex 1
  267. font-size 28rpx
  268. font-weight 600
  269. color #222
  270. margin-right 12rpx
  271. .course-meta
  272. font-size 22rpx
  273. color #999
  274. // 状态标签
  275. .status-tag
  276. font-size 20rpx
  277. padding 4rpx 12rpx
  278. border-radius 8rpx
  279. border 1rpx solid transparent
  280. flex-shrink 0
  281. .tag-gray
  282. background #f5f5f5
  283. color #999
  284. border-color #e8e8e8
  285. .tag-orange
  286. background #fff7e6
  287. color #d46b08
  288. border-color #ffd591
  289. .tag-green
  290. background #f6ffed
  291. color #389e0d
  292. border-color #b7eb8f
  293. // 提示
  294. .empty-tip
  295. text-align center
  296. line-height 80rpx
  297. color #aaa
  298. font-size 24rpx
  299. .loading-tip
  300. text-align center
  301. line-height 60rpx
  302. color #aaa
  303. font-size 22rpx
  304. </style>