index.vue 12 KB

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