courseSearch.vue 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840
  1. <template>
  2. <view class="container">
  3. <!-- 顶部导航 Tab -->
  4. <view class="top-nav">
  5. <view class="search-cont">
  6. <view class="inner">
  7. <image class="icon-search" src="https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/class/search.png" mode=""></image>
  8. <input type="text" v-model="keyword" @input="clearInput" placeholder="请输入关键字搜索内容" placeholder-style="font-size:28rpx;color:#BBBBBB;"/>
  9. <view class="close-circle" v-if="showClearIcon" @click="clearIcon">
  10. <u-icon name="close-circle" color="#999" size="22" ></u-icon>
  11. </view>
  12. </view>
  13. <view class="sousuo" @click="goSearch">搜索</view>
  14. </view>
  15. <!-- <view class="nav-row">
  16. <view class="nav-all" :class="{ active: activeId === 'all'}" @tap.stop="onSelectAll" @click.stop="onSelectAll">
  17. <text>全部</text>
  18. </view>
  19. <scroll-view scroll-x class="nav-scroll" :show-scrollbar="false">
  20. <view class="nav-inner">
  21. <view
  22. v-for="(item, index) in navList"
  23. :key="index"
  24. :class="['nav-item', { active: item.id === activeId }]"
  25. @tap.stop="onSelectByIndex(index)"
  26. @click.stop="onSelectByIndex(index)"
  27. >
  28. <text>{{ item.categoryName}}</text>
  29. </view>
  30. </view>
  31. </scroll-view>
  32. </view> -->
  33. </view>
  34. <!-- 最近学习 内容 -->
  35. <scroll-view
  36. scroll-y
  37. class="scroll-wrap"
  38. :show-scrollbar="false"
  39. lower-threshold="120"
  40. @scrolltolower="onCourseScrollToLower"
  41. >
  42. <!-- 搜索结果 -->
  43. <template v-if="isSearch">
  44. <view class="course-skeleton" v-if="courseSectionLoading && courseList.length === 0">
  45. <view class="sk-course-card" v-for="i in 3" :key="'sk-search-' + i">
  46. <view class="sk-thumb"></view>
  47. <view class="sk-info">
  48. <view class="sk-line sk-line-lg"></view>
  49. <view class="sk-line sk-line-md"></view>
  50. <view class="sk-foot">
  51. <view class="sk-line sk-line-sm"></view>
  52. <view class="sk-btn"></view>
  53. </view>
  54. </view>
  55. </view>
  56. </view>
  57. <view class="course-grid-wrap" v-else-if="courseList.length > 0">
  58. <view class="course-grid" :class="{ 'list-dimmed': courseRefreshing }">
  59. <view
  60. v-for="(course, idx) in courseList"
  61. :key="course.courseId"
  62. class="course-card"
  63. @click="onCourseClick(course)"
  64. >
  65. <view class="card-cover">
  66. <image v-if="course.cover" :src="course.cover" mode="aspectFill" class="cover-img"></image>
  67. </view>
  68. <view class="course-info">
  69. <text class="card-title">{{ course.title }}</text>
  70. <view class="x-end" style="justify-content: space-between;">
  71. <view class="course-meta">
  72. <image src="https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/class/renshu.png"></image>
  73. <text class="meta-count">{{ courseViewsDisplay(course.views)}}</text>
  74. </view>
  75. <view class="btn-watch" @click.stop="onCourseClick(course)">立即观看</view>
  76. </view>
  77. </view>
  78. </view>
  79. </view>
  80. <view class="refresh-mask" v-if="courseRefreshing">
  81. <view class="refresh-spinner"></view>
  82. </view>
  83. </view>
  84. <view v-if="courseList.length > 0" class="load-tip">
  85. <text v-if="courseLoading && !courseRefreshing">加载中...</text>
  86. <text v-else-if="!courseHasMore">没有更多了</text>
  87. </view>
  88. </template>
  89. <!-- 大家都在学习 -->
  90. <template v-else>
  91. <view class="recommend-skeleton-wrap" v-if="recommendLoading && recommendList.length === 0">
  92. <text class="recommend-title">大家都在学习</text>
  93. <view class="course-skeleton" style="padding: 0;">
  94. <view class="sk-course-card" v-for="i in 3" :key="'sk-rec-' + i">
  95. <view class="sk-thumb"></view>
  96. <view class="sk-info">
  97. <view class="sk-line sk-line-lg"></view>
  98. <view class="sk-line sk-line-md"></view>
  99. <view class="sk-foot">
  100. <view class="sk-line sk-line-sm"></view>
  101. <view class="sk-btn"></view>
  102. </view>
  103. </view>
  104. </view>
  105. </view>
  106. </view>
  107. <view class="recommend-section" v-else>
  108. <text class="recommend-title">大家都在学习</text>
  109. <view class="course-grid" style="padding: 0;">
  110. <view
  111. v-for="(course, idx) in recommendList"
  112. :key="course.courseId"
  113. class="course-card"
  114. @click="onCourseClick(course)"
  115. >
  116. <view class="card-cover">
  117. <image v-if="course.cover" :src="course.cover" mode="aspectFill" class="cover-img"></image>
  118. </view>
  119. <view class="card-footer">
  120. <text class="card-title">{{ course.title }}</text>
  121. <view class="x-end" style="justify-content: space-between;">
  122. <view class="course-meta">
  123. <image src="https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/class/renshu.png"></image>
  124. <text class="meta-count">{{ courseViewsDisplay(course.views) }}</text>
  125. </view>
  126. <view class="btn-watch" @click.stop="onCourseClick(course)">立即观看</view>
  127. </view>
  128. </view>
  129. </view>
  130. </view>
  131. </view>
  132. </template>
  133. <template v-if="isSearch && keyword && courseList.length === 0 && !courseSectionLoading && !courseLoading">
  134. <view class="empty-state">
  135. <!-- <view class="empty-icon"></view> -->
  136. <image class="empty-icon" src="https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/class/nodata.png"></image>
  137. <text class="empty-text">没有搜索任何内容,换个词试试</text>
  138. </view>
  139. </template>
  140. <view class="bottom-placeholder"></view>
  141. </scroll-view>
  142. </view>
  143. </template>
  144. <script>
  145. import { listPublicCourse } from '@/api/home.js'
  146. export default {
  147. data() {
  148. return {
  149. statusBarHeight: uni.getSystemInfoSync().statusBarHeight + 'px',
  150. keyword: '',
  151. tabIndex: 0,
  152. activeId:'all',
  153. categoryExpand: true,
  154. isSearch:false,
  155. showClearIcon: false,
  156. categoryIndex: 0,
  157. categories: ['全部', '歌唱艺术', '太极养生', '防骗指南', '手机摄影', '棋牌益智', '用药指导', '膳食营养', '慢病管理'],
  158. courseList: [],
  159. coursePageNum: 1,
  160. coursePageSize: 10,
  161. courseLoading: false,
  162. courseRefreshing: false,
  163. courseSectionLoading: false,
  164. courseHasMore: true,
  165. courseSearchKeyword: '',
  166. courseRequestId: 0,
  167. recommendLoading: true,
  168. recentList: [
  169. // {
  170. // date: '2026-02-08',
  171. // list: [
  172. // { title: '歌唱家刘金的《0基础金曲演唱速练课》', progress: 87, cover: '' },
  173. // { title: '资深编辑邹方斌讲《毛笔书法修心课》', progress: 56, cover: '' }
  174. // ]
  175. // },
  176. // {
  177. // date: '2026-02-01',
  178. // list: [
  179. // { title: '张斌《元气八段锦》系列课', progress: 56, cover: '' }
  180. // ]
  181. // }
  182. ],
  183. navList:[
  184. // {id: 37,categoryName: "健康食品"},
  185. // {id: 35,categoryName: "绿色有机"}
  186. ],
  187. recommendList: [
  188. // { title: '刘金的《0基础金曲演唱速练课》', views: '9239', cover: '' },
  189. // { title: '邹方斌讲《毛笔书法修心课》', views: '10.8w', cover: '' },
  190. // { title: '张斌《元气八段锦》系列课', views: '2.5w', cover: '' },
  191. // { title: '翔哥精讲摄影课-手机微距拍摄技巧...', views: '100w+', cover: '' }
  192. ]
  193. }
  194. },
  195. onLoad(options) {
  196. // this.keyword = (options && options.keyword) || ''
  197. this.getCourseList({})
  198. this.getNewCourseList()
  199. },
  200. methods: {
  201. courseViewsDisplay(views) {
  202. const raw = views;
  203. const n = Number(raw);
  204. if (!Number.isFinite(n) || n < 0) {
  205. return "0";
  206. }
  207. if (n < 100000) {
  208. return String(Math.floor(n));
  209. }
  210. if (n < 1000000) {
  211. return (n / 10000).toFixed(1) + "w";
  212. }
  213. return "100w+";
  214. },
  215. onSelectAll() {
  216. this.$emit('select', { id: 'all', categoryName: '全部' });
  217. },
  218. clearInput: function(event) {
  219. this.keyword = event.detail.value;
  220. if (event.detail.value.length > 0) {
  221. this.showClearIcon = true;
  222. } else {
  223. this.showClearIcon = false;
  224. }
  225. },
  226. clearIcon: function() {
  227. this.keyword = '';
  228. this.showClearIcon = false;
  229. this.isSearch=false;
  230. },
  231. async getCourseList(options = {}) {
  232. const loadMore = !!options.loadMore
  233. if (this.courseLoading && loadMore) return
  234. if (loadMore && !this.courseHasMore) return
  235. const requestId = ++this.courseRequestId
  236. if (!loadMore) {
  237. this.coursePageNum = 1
  238. this.courseHasMore = true
  239. if (options.keyword !== undefined) {
  240. this.courseSearchKeyword = (options.keyword || '').trim()
  241. }
  242. if (this.isSearch) {
  243. if (this.courseList.length > 0) {
  244. this.courseRefreshing = true
  245. } else {
  246. this.courseSectionLoading = true
  247. }
  248. }
  249. }
  250. const currentPageNum = this.coursePageNum
  251. const params = {
  252. pageNum: currentPageNum,
  253. pageSize: this.coursePageSize
  254. }
  255. if (this.courseSearchKeyword) {
  256. params.keyword = this.courseSearchKeyword
  257. }
  258. this.courseLoading = true
  259. try {
  260. const res = await listPublicCourse(params)
  261. if (requestId !== this.courseRequestId) return
  262. const list = (res && res.data && res.data.list) || []
  263. const mapped = list.map(item => ({
  264. courseId: item.courseId,
  265. title: item.courseTitle || item.courseName || '',
  266. views: item.watchUserCount || 0,
  267. cover: item.imgUrl || ''
  268. }))
  269. this.courseList = loadMore ? this.courseList.concat(mapped) : mapped
  270. const total = Number(res.data && res.data.total)
  271. if (Number.isFinite(total) && total >= 0) {
  272. this.courseHasMore = this.courseList.length < total
  273. } else {
  274. this.courseHasMore = mapped.length >= this.coursePageSize
  275. }
  276. if (mapped.length > 0) {
  277. this.coursePageNum = currentPageNum + 1
  278. } else {
  279. this.courseHasMore = false
  280. }
  281. } catch (e) {
  282. if (requestId !== this.courseRequestId) return
  283. if (!loadMore) {
  284. this.courseList = []
  285. }
  286. this.courseHasMore = false
  287. } finally {
  288. if (requestId !== this.courseRequestId) return
  289. this.courseLoading = false
  290. this.courseRefreshing = false
  291. if (!loadMore && this.isSearch) this.courseSectionLoading = false
  292. }
  293. },
  294. onCourseScrollToLower() {
  295. if (!this.isSearch) return
  296. this.getCourseList({ loadMore: true })
  297. },
  298. async getNewCourseList() {
  299. if (!this.recommendList.length) this.recommendLoading = true
  300. try {
  301. const params = { pageNum: 1, pageSize: 6 }
  302. const res = await listPublicCourse(params)
  303. const list = (res && res.data && res.data.list) || []
  304. this.recommendList = list.map(item => ({
  305. courseId: item.courseId,
  306. title: item.courseTitle || item.courseName || '',
  307. views: item.watchUserCount || 0,
  308. cover: item.imgUrl || ''
  309. }))
  310. } catch (e) {
  311. this.recommendList = []
  312. } finally {
  313. this.recommendLoading = false
  314. }
  315. },
  316. goSearch() {
  317. if (!this.keyword.trim()) {
  318. uni.showToast({
  319. icon: 'none',
  320. title: '请输入搜索内容'
  321. })
  322. return
  323. }
  324. if (this.courseRefreshing || this.courseSectionLoading) return
  325. this.isSearch = true
  326. this.getCourseList({ keyword: this.keyword })
  327. },
  328. onCourseClick(course) {
  329. if (course && course.courseId) {
  330. uni.navigateTo({ url: '/pages_index/courseDetail?courseId=' + course.courseId + '&type=1' })
  331. return
  332. }
  333. }
  334. }
  335. }
  336. </script>
  337. <style lang="scss" scoped>
  338. /* 图片箭头旋转类(展开时向上) */
  339. .rotate-arrow {
  340. transform: rotate(180deg);
  341. }
  342. .container {
  343. min-height: 100vh;
  344. background: #F5F5F5;
  345. display: flex;
  346. flex-direction: column;
  347. }
  348. /* 顶部 Tab */
  349. .top-nav {
  350. display: flex;
  351. flex-direction: column;
  352. background: #fff;
  353. /* border-bottom: 1rpx solid #f0f0f0; */
  354. }
  355. .search-cont{
  356. padding: 24rpx 24rpx 20rpx;
  357. background-color: #FFFFFF;
  358. display:flex;
  359. align-items: center;
  360. justify-content: space-between;
  361. .inner{
  362. box-sizing: border-box;
  363. flex:1;
  364. // width: 100%;
  365. height: 76rpx;
  366. background: #F7F7F7;
  367. border-radius: 38rpx;
  368. display: flex;
  369. align-items: center;
  370. padding-left:20rpx;
  371. margin-right: 24rpx;
  372. .icon-search{
  373. width: 44rpx;
  374. height: 44rpx;
  375. margin-right: 12rpx;
  376. }
  377. input{
  378. height: 76rpx;
  379. line-height: 76rpx;
  380. flex: 1;
  381. font-family: PingFangSC, PingFang SC;
  382. font-weight: 400;
  383. font-size: 32rpx;
  384. }
  385. .close-circle{
  386. position: relative;
  387. z-index:999;
  388. padding:0 20rpx;
  389. }
  390. }
  391. .sousuo{
  392. font-family: PingFangSC, PingFang SC;
  393. font-weight: 400;
  394. font-size: 36rpx;
  395. color: #222222;
  396. line-height: 50rpx;
  397. }
  398. }
  399. .nav-row {
  400. display: flex;
  401. align-items: center;
  402. padding: 0 24rpx 24rpx;
  403. gap: 30rpx;
  404. }
  405. .nav-all {
  406. flex-shrink: 0;
  407. font-family: PingFangSC, PingFang SC;
  408. font-weight: 400;
  409. font-size: 40rpx;
  410. color: rgba(0,0,0,0.85);
  411. line-height: 56rpx;
  412. }
  413. .nav-all.active {
  414. color: #FF233C;
  415. font-weight: 600;
  416. }
  417. .nav-scroll {
  418. flex: 1;
  419. }
  420. .nav-inner {
  421. display: inline-flex;
  422. padding:0;
  423. gap: 30rpx;
  424. }
  425. .nav-item {
  426. flex-shrink: 0;
  427. font-family: PingFangSC, PingFang SC;
  428. font-weight: 400;
  429. font-size: 40rpx;
  430. color: rgba(0,0,0,0.85);
  431. line-height: 56rpx;
  432. }
  433. .nav-item.active {
  434. color: #FF233C;
  435. font-weight: 600;
  436. }
  437. /* 滚动区 */
  438. .scroll-wrap {
  439. flex: 1;
  440. height: 0;
  441. }
  442. /* 分类标签 */
  443. .category-wrap {
  444. padding:24rpx 0;
  445. border-radius: 0rpx 0rpx 20rpx 20rpx;
  446. background: #fff;
  447. display: flex;
  448. align-items: center;
  449. flex-direction: column;
  450. }
  451. .category-tags {
  452. display: flex;
  453. flex-wrap: wrap;
  454. gap:24rpx;
  455. flex-direction: row;
  456. justify-content: center;
  457. }
  458. .category-tags.collapsed {
  459. max-height: 88rpx;
  460. overflow: hidden;
  461. }
  462. .tag-item {
  463. width: 30%;
  464. padding: 20rpx 0;
  465. text-align: center;
  466. border-radius: 20rpx;
  467. background: #f0f0f0;
  468. }
  469. .tag-item text {
  470. font-family: PingFangSC, PingFang SC;
  471. font-weight: 400;
  472. font-size: 40rpx;
  473. color: rgba(0,0,0,0.85);
  474. }
  475. .tag-item.active {
  476. background: linear-gradient( 135deg, #FF5267 0%, #FF233C 100%);
  477. }
  478. .tag-item.active text {
  479. color: #fff;
  480. }
  481. .expand-btn {
  482. display: flex;
  483. align-items: center;
  484. justify-content: center;
  485. margin-top: 20rpx;
  486. width: 166rpx;
  487. height: 64rpx;
  488. border-radius: 32rpx;
  489. border: 2rpx solid #FF233C;
  490. }
  491. .expand-btn text {
  492. font-family: PingFangSC, PingFang SC;
  493. font-weight: 400;
  494. font-size: 32rpx;
  495. color: #FF233C;
  496. }
  497. .expand-btn .arrow {
  498. margin-left: 6rpx;
  499. font-size: 22rpx;
  500. }
  501. .expand-btn image{
  502. margin-left:10rpx ;
  503. width: 32rpx;
  504. height: 32rpx;
  505. }
  506. /* 课程网格 */
  507. .course-grid {
  508. display: flex;
  509. flex-wrap: wrap;
  510. padding:24rpx;
  511. gap: 24rpx 20rpx;
  512. flex-direction: column;
  513. }
  514. .course-card {
  515. display: flex;
  516. background: #fff;
  517. border-radius: 20rpx;
  518. overflow: hidden;
  519. padding: 20rpx;
  520. }
  521. .course-tag {
  522. position: absolute;
  523. left: 0;
  524. bottom: 0;
  525. right: 0;
  526. padding: 8rpx 12rpx;
  527. background: linear-gradient(transparent, rgba(0,0,0,0.6));
  528. font-size: 22rpx;
  529. color: #fff;
  530. }
  531. .course-info {
  532. flex: 1;
  533. padding-left: 24rpx;
  534. /* padding: 20rpx 24rpx; */
  535. display: flex;
  536. flex-direction: column;
  537. justify-content: space-between;
  538. min-width: 0;
  539. }
  540. .course-meta {
  541. display: flex;
  542. align-items: center;
  543. image{
  544. width: 30rpx;
  545. height: 30rpx;
  546. }
  547. }
  548. .meta-icon {
  549. font-size: 24rpx;
  550. margin-right: 6rpx;
  551. color: #999;
  552. }
  553. .meta-count {
  554. margin-left: 10rpx;
  555. font-family: PingFangSC, PingFang SC;
  556. font-weight: 400;
  557. font-size: 32rpx;
  558. color: #666666;
  559. }
  560. .card-cover {
  561. position: relative;
  562. width: 296rpx;
  563. height: 222rpx;
  564. border-radius: 20rpx;
  565. overflow: hidden;
  566. flex-shrink: 0;
  567. }
  568. .cover-img {
  569. width: 100%;
  570. height: 100%;
  571. background: #BB6D6D;
  572. }
  573. .card-title {
  574. font-family: PingFangSC, PingFang SC;
  575. font-weight: 600;
  576. font-size: 36rpx;
  577. color: #222222;
  578. line-height: 50rpx;
  579. text-align: justify;
  580. overflow: hidden;
  581. text-overflow: ellipsis;
  582. display: -webkit-box;
  583. -webkit-line-clamp: 2;
  584. -webkit-box-orient: vertical;
  585. }
  586. .card-footer {
  587. flex: 1;
  588. padding-left: 24rpx;
  589. /* padding: 20rpx 24rpx; */
  590. display: flex;
  591. flex-direction: column;
  592. justify-content: space-between;
  593. min-width: 0;
  594. }
  595. .card-views {
  596. font-family: PingFangSC, PingFang SC;
  597. font-weight: 400;
  598. font-size: 32rpx;
  599. color: #666666;
  600. line-height: 44rpx;
  601. }
  602. .btn-watch {
  603. width: 168rpx;
  604. height: 64rpx;
  605. line-height: 64rpx;
  606. text-align: center;
  607. background: linear-gradient( 135deg, #FF5267 0%, #FF233C 100%);
  608. border-radius: 32rpx;
  609. font-family: PingFangSC, PingFang SC;
  610. font-weight: 600;
  611. font-size: 32rpx;
  612. color: #FFFFFF;
  613. }
  614. /* 最近学习 - 按日期分组 */
  615. .recent-group {
  616. padding: 24rpx 24rpx 0;
  617. }
  618. .group-date {
  619. display: flex;
  620. align-items: center;
  621. margin-bottom: 24rpx;
  622. }
  623. .date-icon {
  624. width: 32rpx;
  625. height: 32rpx;
  626. margin-right: 8rpx;
  627. }
  628. .date-text {
  629. font-family: PingFangSC, PingFang SC;
  630. font-weight: 600;
  631. font-size: 40rpx;
  632. line-height: 56rpx;
  633. color: #222222;
  634. }
  635. .recent-card {
  636. display: flex;
  637. background: #fff;
  638. border-radius: 20rpx;
  639. overflow: hidden;
  640. padding: 20rpx;
  641. margin-bottom: 24rpx;
  642. }
  643. .recent-card:last-child{
  644. margin-bottom: 0;
  645. }
  646. .recent-thumb {
  647. position: relative;
  648. width: 296rpx;
  649. height: 222rpx;
  650. border-radius: 20rpx;
  651. overflow: hidden;
  652. flex-shrink: 0;
  653. }
  654. .thumb-img {
  655. width: 100%;
  656. height: 100%;
  657. background: #BB6D6D;
  658. }
  659. .recent-info {
  660. flex: 1;
  661. padding-left: 24rpx;
  662. /* padding: 20rpx 24rpx; */
  663. display: flex;
  664. flex-direction: column;
  665. justify-content: space-between;
  666. min-width: 0;
  667. }
  668. .recent-title {
  669. font-family: PingFangSC, PingFang SC;
  670. font-weight: 600;
  671. font-size: 36rpx;
  672. color: #222222;
  673. line-height: 50rpx;
  674. text-align: justify;
  675. overflow: hidden;
  676. text-overflow: ellipsis;
  677. display: -webkit-box;
  678. -webkit-line-clamp: 2;
  679. -webkit-box-orient: vertical;
  680. }
  681. .recent-progress {
  682. font-family: PingFangSC, PingFang SC;
  683. font-weight: 400;
  684. font-size: 32rpx;
  685. color: rgba(0,0,0,0.65);
  686. line-height: 44rpx;
  687. }
  688. /* 空态 */
  689. .empty-state {
  690. display: flex;
  691. flex-direction: column;
  692. align-items: center;
  693. justify-content: center;
  694. padding: 80rpx 0 48rpx;
  695. /* background: linear-gradient(180deg, #fff5f5 0%, #fff 100%); */
  696. }
  697. .empty-icon {
  698. width: 310rpx;
  699. height: 260rpx;
  700. margin-bottom: 30rpx;
  701. }
  702. .empty-text {
  703. font-family: PingFangSC, PingFang SC;
  704. font-weight: 400;
  705. font-size: 36rpx;
  706. color: rgba(0,0,0,0.25);
  707. line-height: 50rpx;
  708. }
  709. /* 为您精选 */
  710. .recommend-section {
  711. margin-top: 300rpx;
  712. padding: 0 24rpx 32rpx;
  713. }
  714. .recommend-title {
  715. display: block;
  716. font-family: PingFangSC, PingFang SC;
  717. font-weight: 600;
  718. font-size: 40rpx;
  719. color: #222222;
  720. line-height: 56rpx;
  721. padding-bottom: 24rpx;
  722. }
  723. .bottom-placeholder {
  724. height: 120rpx;
  725. }
  726. .load-tip {
  727. padding: 24rpx 0 8rpx;
  728. text-align: center;
  729. font-size: 28rpx;
  730. color: #999;
  731. }
  732. @mixin sk-shimmer {
  733. background: linear-gradient(90deg, #eee 25%, #f5f5f5 50%, #eee 75%);
  734. background-size: 200% 100%;
  735. animation: skeleton-shimmer 1.2s ease-in-out infinite;
  736. }
  737. .sk-line {
  738. height: 28rpx;
  739. border-radius: 6rpx;
  740. @include sk-shimmer;
  741. }
  742. .sk-line-lg { width: 90%; }
  743. .sk-line-md { width: 70%; margin-top: 16rpx; }
  744. .sk-line-sm { width: 40%; }
  745. @keyframes skeleton-shimmer {
  746. 0% { background-position: 100% 0; }
  747. 100% { background-position: -100% 0; }
  748. }
  749. .course-skeleton {
  750. padding: 24rpx;
  751. display: flex;
  752. flex-direction: column;
  753. gap: 24rpx;
  754. }
  755. .sk-course-card {
  756. display: flex;
  757. background: #fff;
  758. border-radius: 20rpx;
  759. padding: 20rpx;
  760. }
  761. .sk-thumb {
  762. width: 296rpx;
  763. height: 222rpx;
  764. border-radius: 20rpx;
  765. flex-shrink: 0;
  766. @include sk-shimmer;
  767. }
  768. .sk-info {
  769. flex: 1;
  770. padding-left: 24rpx;
  771. min-width: 0;
  772. }
  773. .sk-foot {
  774. display: flex;
  775. align-items: center;
  776. justify-content: space-between;
  777. margin-top: 40rpx;
  778. }
  779. .sk-btn {
  780. width: 168rpx;
  781. height: 64rpx;
  782. border-radius: 32rpx;
  783. @include sk-shimmer;
  784. }
  785. .recommend-skeleton-wrap {
  786. margin-top: 300rpx;
  787. padding: 0 24rpx 32rpx;
  788. }
  789. .course-grid-wrap {
  790. position: relative;
  791. }
  792. .course-grid.list-dimmed {
  793. opacity: 0.55;
  794. transition: opacity 0.2s ease;
  795. }
  796. .refresh-mask {
  797. position: absolute;
  798. left: 0;
  799. right: 0;
  800. top: 0;
  801. bottom: 0;
  802. display: flex;
  803. align-items: center;
  804. justify-content: center;
  805. pointer-events: none;
  806. }
  807. .refresh-spinner {
  808. width: 48rpx;
  809. height: 48rpx;
  810. border: 4rpx solid #e8e8e8;
  811. border-top-color: #FF233C;
  812. border-radius: 50%;
  813. animation: spin 0.8s linear infinite;
  814. }
  815. @keyframes spin {
  816. to { transform: rotate(360deg); }
  817. }
  818. </style>