index.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487
  1. <!-- 知识学习列表 -->
  2. <template>
  3. <view class="safetyEducationExamination-study">
  4. <!-- 知识点类型标签 -->
  5. <scroll-view scroll-x class="tag-scroll tag-scroll-border" 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. <!-- 视频/文章二级 Tab -->
  17. <view class="sub-tab-bar">
  18. <view
  19. v-for="(sub, index) in subTabs"
  20. :key="index"
  21. class="sub-tab-item"
  22. :class="{ active: activeSubTab === index }"
  23. @click="switchSubTab(index)"
  24. >{{ sub }}</view>
  25. </view>
  26. <!-- 内容区 -->
  27. <scroll-view scroll-y class="scroll-content" @scrolltolower="onScrollToLower">
  28. <!-- 视频资源 -->
  29. <view v-if="activeSubTab === 0" class="panel">
  30. <view class="video-grid">
  31. <view
  32. v-for="(item, index) in videoList"
  33. :key="index"
  34. class="video-card"
  35. @click="goKnowledgeDetail(item)"
  36. >
  37. <view class="video-thumb" :style="'background:' + item.coverGradient">
  38. <image v-if="item.exampleImg" :src="baseUrl + item.exampleImg" class="thumb-img" mode="aspectFill" />
  39. <text v-else class="play-icon">▶</text>
  40. </view>
  41. <view class="video-info">
  42. <text class="video-title">{{ item.coursewareName }}</text>
  43. <text class="video-meta">{{ formatDuration(item.minLearningDuration) }}分钟 | {{ item.creditHours }}学时 | {{ item.points }}积分</text>
  44. <view class="mt8">
  45. <text class="status-tag" :class="item.statusClass">{{ item.statusName }}</text>
  46. </view>
  47. </view>
  48. </view>
  49. </view>
  50. <view v-if="!videoList[0] && !videoLoading" class="empty-tip">暂无视频资源</view>
  51. <view v-if="videoLoading" class="loading-tip">加载中...</view>
  52. <view v-if="videoNoMore && videoList[0]" class="loading-tip">没有更多了</view>
  53. </view>
  54. <!-- 文章资料 -->
  55. <view v-if="activeSubTab === 1" class="panel">
  56. <view
  57. v-for="(item, index) in articleList"
  58. :key="index"
  59. class="course-card"
  60. @click="goKnowledgeDetail(item)"
  61. >
  62. <view class="course-cover" :style="'background:' + item.coverGradient">
  63. <text class="cover-icon">{{ item.coverIcon || '📄' }}</text>
  64. </view>
  65. <view class="course-info">
  66. <view class="course-info-top">
  67. <text class="course-name">{{ item.coursewareName }}</text>
  68. <text class="status-tag" :class="item.statusClass">{{ item.statusName }}</text>
  69. </view>
  70. <text class="course-meta">{{ formatDuration(item.minLearningDuration) }}分钟 | {{ item.creditHours }}学时 | {{ item.points }}积分</text>
  71. </view>
  72. </view>
  73. <view v-if="!articleList[0] && !articleLoading" class="empty-tip">暂无文章资料</view>
  74. <view v-if="articleLoading" class="loading-tip">加载中...</view>
  75. <view v-if="articleNoMore && articleList[0]" class="loading-tip">没有更多了</view>
  76. </view>
  77. </scroll-view>
  78. </view>
  79. </template>
  80. <script>
  81. import {
  82. config
  83. } from '@/api/request/config.js'
  84. import {
  85. examUserCoursewareLearningList,
  86. examElKnowledgePointTreeList,
  87. } from '@/pages_safetyEducationExamination/api/index.js'
  88. // 封面渐变色池
  89. const COVER_GRADIENTS = [
  90. 'linear-gradient(135deg,#ffd666,#fa8c16)',
  91. 'linear-gradient(135deg,#b37feb,#722ed1)',
  92. 'linear-gradient(135deg,#ff9c6e,#f5222d)',
  93. 'linear-gradient(135deg,#5cdbd3,#13c2c2)',
  94. 'linear-gradient(135deg,#1857d4,#5b8af0)',
  95. 'linear-gradient(135deg,#10a37f,#34d399)',
  96. 'linear-gradient(135deg,#6c63ff,#a78bfa)',
  97. 'linear-gradient(135deg,#95de64,#237804)',
  98. ];
  99. // 学习状态映射
  100. function getStatusInfo(status) {
  101. const map = {
  102. 0: { name: '未开始', cls: 'tag-gray' },
  103. 1: { name: '学习中', cls: 'tag-orange' },
  104. 2: { name: '已完成', cls: 'tag-green' },
  105. };
  106. return map[status] || map[0];
  107. }
  108. export default {
  109. data() {
  110. return {
  111. baseUrl: config.base_url,
  112. subTabs: ['视频资源', '文章资料'],
  113. activeSubTab: 0,
  114. // 类型标签
  115. typeTags: [{ label: '全部', value: null }],
  116. activeTagIndex: 0,
  117. // 视频资源
  118. videoList: [],
  119. videoLoading: false,
  120. videoPage: 1,
  121. videoPageSize: 10,
  122. videoTotal: 0,
  123. videoNoMore: false,
  124. // 文章资料
  125. articleList: [],
  126. articleLoading: false,
  127. articlePage: 1,
  128. articlePageSize: 10,
  129. articleTotal: 0,
  130. articleNoMore: false,
  131. }
  132. },
  133. onLoad(options) {
  134. // 接收跳转参数,pageType=2 时直接展示文章资料
  135. if (options.pageType == 2) {
  136. this.activeSubTab = 1;
  137. }
  138. },
  139. created() {
  140. this.loadTypeTags();
  141. },
  142. onShow() {
  143. this.refreshCurrentTab();
  144. },
  145. methods: {
  146. // 加载知识点类型标签(dataScope=1)
  147. async loadTypeTags() {
  148. try {
  149. const { data } = await examElKnowledgePointTreeList({ dataScope: 1 });
  150. if (data.code === 200 && data.data) {
  151. const tags = (data.data || []).map(item => ({
  152. label: item.knowledgePointName,
  153. value: item.id,
  154. }));
  155. this.$set(this, 'typeTags', [{ label: '全部', value: null }, ...tags]);
  156. }
  157. } catch (e) {
  158. console.error('获取知识点类型失败', e);
  159. }
  160. },
  161. switchSubTab(index) {
  162. this.activeSubTab = index;
  163. this.refreshCurrentTab();
  164. },
  165. onTagChange(index) {
  166. this.activeTagIndex = index;
  167. this.refreshCurrentTab();
  168. },
  169. refreshCurrentTab() {
  170. if (this.activeSubTab === 0) {
  171. this.resetVideoList();
  172. } else {
  173. this.resetArticleList();
  174. }
  175. },
  176. // 秒数转分钟
  177. formatDuration(seconds) {
  178. return seconds ? Math.ceil(seconds / 60) : 0;
  179. },
  180. // 重置视频列表
  181. resetVideoList() {
  182. this.videoPage = 1;
  183. this.videoNoMore = false;
  184. this.$set(this, 'videoList', []);
  185. this.loadVideoList();
  186. },
  187. // 重置文章列表
  188. resetArticleList() {
  189. this.articlePage = 1;
  190. this.articleNoMore = false;
  191. this.$set(this, 'articleList', []);
  192. this.loadArticleList();
  193. },
  194. // 知识学习-视频(materialType=1)
  195. async loadVideoList() {
  196. if (this.videoLoading || this.videoNoMore) return;
  197. this.videoLoading = true;
  198. try {
  199. const tag = this.typeTags[this.activeTagIndex];
  200. const { data } = await examUserCoursewareLearningList({
  201. page: this.videoPage,
  202. pageSize: this.videoPageSize,
  203. materialType: 1,
  204. knowledgePointId: tag.value || undefined,
  205. });
  206. if (data.code === 200) {
  207. const records = (data.data.records || []).map((item, i) => {
  208. const s = getStatusInfo(item.learnStatus);
  209. const colorIndex = (this.videoList.length + i) % COVER_GRADIENTS.length;
  210. return {
  211. ...item,
  212. coverGradient: COVER_GRADIENTS[colorIndex],
  213. statusName: s.name,
  214. statusClass: s.cls,
  215. };
  216. });
  217. const newList = this.videoPage === 1 ? records : [...this.videoList, ...records];
  218. this.$set(this, 'videoList', newList);
  219. this.videoTotal = data.data.total;
  220. if (newList.length >= this.videoTotal) {
  221. this.videoNoMore = true;
  222. } else {
  223. this.videoPage++;
  224. }
  225. }
  226. } catch (e) {
  227. console.error('获取视频资源失败', e);
  228. } finally {
  229. this.videoLoading = false;
  230. }
  231. },
  232. // 知识学习-文章(materialType=2)
  233. async loadArticleList() {
  234. if (this.articleLoading || this.articleNoMore) return;
  235. this.articleLoading = true;
  236. try {
  237. const tag = this.typeTags[this.activeTagIndex];
  238. const { data } = await examUserCoursewareLearningList({
  239. page: this.articlePage,
  240. pageSize: this.articlePageSize,
  241. materialType: 2,
  242. knowledgePointId: tag.value || undefined,
  243. });
  244. if (data.code === 200) {
  245. const records = (data.data.records || []).map((item, i) => {
  246. const s = getStatusInfo(item.learnStatus);
  247. const colorIndex = (this.articleList.length + i) % COVER_GRADIENTS.length;
  248. return {
  249. ...item,
  250. coverGradient: COVER_GRADIENTS[colorIndex],
  251. statusName: s.name,
  252. statusClass: s.cls,
  253. };
  254. });
  255. const newList = this.articlePage === 1 ? records : [...this.articleList, ...records];
  256. this.$set(this, 'articleList', newList);
  257. this.articleTotal = data.data.total;
  258. if (newList.length >= this.articleTotal) {
  259. this.articleNoMore = true;
  260. } else {
  261. this.articlePage++;
  262. }
  263. }
  264. } catch (e) {
  265. console.error('获取文章资料失败', e);
  266. } finally {
  267. this.articleLoading = false;
  268. }
  269. },
  270. // 滚动到底部
  271. onScrollToLower() {
  272. if (this.activeSubTab === 0) {
  273. this.loadVideoList();
  274. } else {
  275. this.loadArticleList();
  276. }
  277. },
  278. goKnowledgeDetail(item) {
  279. // 跳转知识学习详情,传递 coursewareId
  280. },
  281. },
  282. }
  283. </script>
  284. <style lang="stylus" scoped>
  285. .safetyEducationExamination-study
  286. height 100%
  287. display flex
  288. flex-direction column
  289. overflow hidden
  290. background-color #f0f4fb
  291. // 类型标签横滚
  292. .tag-scroll
  293. background #fff
  294. white-space nowrap
  295. flex-shrink 0
  296. .tag-scroll-border
  297. border-bottom 1rpx solid #e8edf3
  298. .tag-scroll-inner
  299. display inline-flex
  300. gap 16rpx
  301. padding 16rpx 24rpx
  302. .filter-tag
  303. display inline-block
  304. font-size 22rpx
  305. padding 8rpx 20rpx
  306. border-radius 30rpx
  307. background #eef1f6
  308. color #666
  309. border 1rpx solid transparent
  310. white-space nowrap
  311. .filter-tag-active
  312. background #e6f0ff
  313. color #1677ff
  314. border-color #91caff
  315. font-weight 600
  316. // 二级 Tab(视频/文章)
  317. .sub-tab-bar
  318. background #fff
  319. border-bottom 1rpx solid #e8edf3
  320. display flex
  321. padding 0 24rpx
  322. flex-shrink 0
  323. .sub-tab-item
  324. flex 1
  325. text-align center
  326. padding 16rpx 8rpx 14rpx
  327. font-size 26rpx
  328. color #888
  329. border-bottom 4rpx solid transparent
  330. .sub-tab-item.active
  331. color #1677ff
  332. font-weight 600
  333. border-bottom-color #1677ff
  334. // 滚动区
  335. .scroll-content
  336. flex 1
  337. overflow hidden
  338. .panel
  339. padding 24rpx
  340. // 视频宫格
  341. .video-grid
  342. display grid
  343. grid-template-columns 1fr 1fr
  344. gap 20rpx
  345. .video-card
  346. background #fff
  347. border-radius 16rpx
  348. overflow hidden
  349. box-shadow 0 2rpx 10rpx rgba(0,0,0,0.05)
  350. border 1rpx solid #dde3ed
  351. .video-thumb
  352. width 100%
  353. height 160rpx
  354. display flex
  355. align-items center
  356. justify-content center
  357. position relative
  358. .thumb-img
  359. width 100%
  360. height 100%
  361. .play-icon
  362. font-size 48rpx
  363. color rgba(255,255,255,0.9)
  364. .video-info
  365. padding 16rpx
  366. .video-title
  367. display block
  368. font-size 24rpx
  369. font-weight 600
  370. color #222
  371. margin-bottom 8rpx
  372. line-height 36rpx
  373. overflow hidden
  374. text-overflow ellipsis
  375. display -webkit-box
  376. -webkit-line-clamp 2
  377. -webkit-box-orient vertical
  378. .video-meta
  379. display block
  380. font-size 20rpx
  381. color #999
  382. .mt8
  383. margin-top 8rpx
  384. // 课程卡片(文章用)
  385. .course-card
  386. background #fff
  387. border-radius 20rpx
  388. margin-bottom 20rpx
  389. overflow hidden
  390. box-shadow 0 2rpx 12rpx rgba(0,0,0,0.04)
  391. border 1rpx solid #dde3ed
  392. .course-cover
  393. width 100%
  394. height 180rpx
  395. display flex
  396. align-items center
  397. justify-content center
  398. .cover-icon
  399. font-size 64rpx
  400. .course-info
  401. padding 20rpx 24rpx
  402. .course-info-top
  403. display flex
  404. justify-content space-between
  405. align-items center
  406. margin-bottom 8rpx
  407. .course-name
  408. flex 1
  409. font-size 28rpx
  410. font-weight 600
  411. color #222
  412. margin-right 12rpx
  413. .course-meta
  414. font-size 22rpx
  415. color #999
  416. // 状态标签
  417. .status-tag
  418. font-size 20rpx
  419. padding 4rpx 12rpx
  420. border-radius 8rpx
  421. border 1rpx solid transparent
  422. flex-shrink 0
  423. .tag-gray
  424. background #f5f5f5
  425. color #999
  426. border-color #e8e8e8
  427. .tag-orange
  428. background #fff7e6
  429. color #d46b08
  430. border-color #ffd591
  431. .tag-green
  432. background #f6ffed
  433. color #389e0d
  434. border-color #b7eb8f
  435. // 提示
  436. .empty-tip
  437. text-align center
  438. line-height 80rpx
  439. color #aaa
  440. font-size 24rpx
  441. .loading-tip
  442. text-align center
  443. line-height 60rpx
  444. color #aaa
  445. font-size 22rpx
  446. </style>