四零语境前端代码仓库
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

822 lines
19 KiB

1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
  1. <template>
  2. <view class="home-container">
  3. <!-- 開動頁面組件 -->
  4. <SplashScreen @close="onSplashClose" />
  5. <!-- 状态栏安全区域 -->
  6. <uv-status-bar></uv-status-bar>
  7. <!-- 顶部搜索栏 -->
  8. <view class="header">
  9. <view class="search-container" @click="goSearch">
  10. <uv-search
  11. placeholder="请输入要查询的内容"
  12. :show-action="false"
  13. shape="round"
  14. bg-color="#f5f5f5"
  15. color="#666"
  16. height="38"
  17. margin="0 200rpx 0 0"
  18. placeholderColor="#c6c6c6"
  19. ></uv-search>
  20. </view>
  21. </view>
  22. <!-- Tab栏 -->
  23. <view class="tab-container">
  24. <scroll-view show-scrollbar="false" class="tab-scroll" scroll-x="true" >
  25. <view class="tab-list">
  26. <view
  27. v-for="(tab, index) in tabs"
  28. :key="index"
  29. class="tab-item"
  30. :class="{ active: activeTab === index }"
  31. @click="switchTab(index)"
  32. >
  33. {{ tab.title }}
  34. </view>
  35. </view>
  36. </scroll-view>
  37. </view>
  38. <!-- 轮播图 -->
  39. <view class="swiper-container">
  40. <uv-swiper
  41. :list="bannerList"
  42. keyName="image"
  43. height="121"
  44. radius="12"
  45. indicator
  46. ndicatorInactiveColor="#fff"
  47. :loading="false"
  48. indicatorMode="dot"
  49. indicatorActiveColor="#F95A01"
  50. @click="onBannerClick"
  51. ></uv-swiper>
  52. </view>
  53. <!-- 根据labelBooksData动态渲染书籍区块 -->
  54. <view
  55. v-for="(labelData, labelIndex) in labelBooksData"
  56. :key="labelIndex"
  57. class="section"
  58. >
  59. <view class="section-header" @click="goLabel(labelData.labelInfo)">
  60. <text class="section-title">{{ labelData.labelInfo.title }}</text>
  61. <view class="section-more">
  62. <text>更多</text>
  63. <uv-icon name="arrow-right" size="14" color="#888"></uv-icon>
  64. </view>
  65. </view>
  66. <!-- 第一个label今日更新样式 -->
  67. <scroll-view
  68. v-if="labelIndex === 0"
  69. show-scrollbar="false"
  70. class="content-scroll"
  71. scroll-x="true"
  72. >
  73. <view class="content-list">
  74. <view
  75. v-for="(item, index) in labelData.books"
  76. :key="index"
  77. class="content-item"
  78. @click="goBook(item)"
  79. >
  80. <view class="item-cover">
  81. <image :src="item.booksImg || '/static/default-image.png'" mode="aspectFill"></image>
  82. </view>
  83. <view class="item-info">
  84. <text class="item-title">{{ item.booksName }}</text>
  85. <text class="item-author">{{ item.booksAuthor }}</text>
  86. <view class="item-duration">
  87. <image src="/static/play-icon.png" class="item-icon" />
  88. <text>{{ item.duration }}</text>
  89. </view>
  90. </view>
  91. </view>
  92. </view>
  93. </scroll-view>
  94. <!-- 第二个label推荐书籍样式 -->
  95. <scroll-view
  96. v-else-if="labelIndex === 1"
  97. show-scrollbar="false"
  98. class="content-scroll"
  99. scroll-x="true"
  100. >
  101. <view class="book-list">
  102. <view
  103. v-for="(book, index) in labelData.books"
  104. :key="index"
  105. class="book-item"
  106. @click="goBook(book)"
  107. >
  108. <view class="book-cover">
  109. <image :src="book.booksImg || '/static/default-image.png'" mode="aspectFill"></image>
  110. <view class="book-overlay">
  111. <view class="book-duration" v-if="book.duration">
  112. <image src="/static/alarm-icon.png" class="book-duration-icon" />
  113. <text class="book-duration-text">{{ book.duration }}</text>
  114. </view>
  115. <view class="book-title">{{ book.booksName }}</view>
  116. </view>
  117. </view>
  118. </view>
  119. </view>
  120. </scroll-view>
  121. <!-- 第三个及以后的label网格样式 -->
  122. <view v-else class="book-grid">
  123. <view
  124. v-for="(book, index) in labelData.books"
  125. :key="index"
  126. class="book-grid-item"
  127. @click="goBook(book)"
  128. >
  129. <view class="book-grid-cover">
  130. <image :src="book.booksImg || '/static/default-image.png'" mode="aspectFill"></image>
  131. </view>
  132. <!-- <view class="book-grid-info">
  133. <text class="book-grid-title">{{ book.booksName }}</text>
  134. <view class="book-grid-meta">
  135. <text class="book-grid-grade">{{ book.categoryName }}/</text>
  136. <image src="/static/play-icon.png" class="book-grid-duration-icon" />
  137. <text class="book-grid-duration">{{ book.duration }}</text>
  138. </view>
  139. </view> -->
  140. </view>
  141. </view>
  142. </view>
  143. <!-- 推荐内容列表 -->
  144. <view class="section">
  145. <view class="recommend-list">
  146. <view
  147. @click="goPlan(item.id, item.type)"
  148. v-for="(item, index) in recommendList"
  149. :key="index"
  150. class="recommend-item"
  151. >
  152. <image :src="item.img" mode="aspectFill" class="recommend-image"></image>
  153. </view>
  154. </view>
  155. </view>
  156. <!-- 视频播放弹窗 -->
  157. <uv-modal
  158. ref="videoModal"
  159. title="视频播放"
  160. :show-cancel-button="false"
  161. :show-confirm-button="false"
  162. width="90%"
  163. :close-on-click-overlay="true"
  164. @close="closeVideoModal"
  165. >
  166. <template #default>
  167. <view class="video-container">
  168. <video
  169. v-if="currentVideo"
  170. :src="currentVideo"
  171. controls
  172. autoplay
  173. :show-fullscreen-btn="true"
  174. :show-play-btn="true"
  175. :show-center-play-btn="true"
  176. style="width: 100%; height: 400rpx; border-radius: 8rpx;"
  177. @error="onVideoError"
  178. @play="onVideoPlay"
  179. @pause="onVideoPause"
  180. ></video>
  181. <view v-else class="video-loading">
  182. <text>视频加载中...</text>
  183. </view>
  184. </view>
  185. </template>
  186. </uv-modal>
  187. </view>
  188. </template>
  189. <script>
  190. import SplashScreen from '../components/SplashScreen.vue'
  191. export default {
  192. components: {
  193. SplashScreen
  194. },
  195. data() {
  196. return {
  197. // Tab数据
  198. tabs: [ ],
  199. activeTab: 0,
  200. // 轮播图数据
  201. bannerList: [
  202. ],
  203. // 书籍分类
  204. labels: [
  205. ],
  206. // 根据label获取的书籍数据(二维数组)
  207. labelBooksData: [],
  208. // 推荐列表数据
  209. recommendList: [
  210. ],
  211. currentVideo: ''
  212. }
  213. },
  214. methods: {
  215. // 開動頁面關閉處理
  216. onSplashClose() {
  217. console.log('開動頁面已關閉')
  218. // 可以在這裡添加其他邏輯,比如統計、初始化等
  219. },
  220. // 切换Tab
  221. async switchTab(index) {
  222. this.activeTab = index
  223. await this.getBooksByLabels()
  224. },
  225. // 轮播图点击事件
  226. onBannerClick(index) {
  227. console.log('点击轮播图:', index)
  228. const bannerItem = this.bannerList[index]
  229. if (!bannerItem) return
  230. // 根据 typ 字段判断跳转类型
  231. switch(bannerItem.typ) {
  232. case '1': // 课程详情
  233. if (bannerItem.bookId) {
  234. uni.navigateTo({
  235. url: '/subPages/home/directory?id=' + bannerItem.bookId
  236. })
  237. }
  238. break
  239. case '2': // 视频播放
  240. if (bannerItem.video) {
  241. this.$refs.videoModal.open()
  242. this.currentVideo = bannerItem.video
  243. }
  244. break
  245. case '0': // 富文本内容
  246. if (bannerItem.content) {
  247. uni.navigateTo({
  248. url: '/subPages/home/richtext?content=' + encodeURIComponent(bannerItem.content)
  249. })
  250. }
  251. break
  252. default:
  253. console.log('未知的轮播图类型:', bannerItem.typ)
  254. }
  255. },
  256. // 跳转计划定制
  257. goPlan(id, type) {
  258. uni.navigateTo({
  259. url: '/subPages/home/plan?id=' + id + '&type=' + type
  260. })
  261. },
  262. goSearch() {
  263. uni.navigateTo({
  264. url: '/subPages/home/search'
  265. })
  266. },
  267. goBook(book) {
  268. uni.navigateTo({
  269. url: '/subPages/home/directory?id=' + book.id
  270. })
  271. },
  272. async getBanner() {
  273. const bannerRes = await this.$api.home.getBanner()
  274. if (bannerRes.code === 200){
  275. this.bannerList = bannerRes.result.map(item => ({
  276. image: item.img,
  277. title: item.title,
  278. typ: item.typ,
  279. bookId: item.bookId,
  280. video: item.video,
  281. content: item.content,
  282. id: item.id
  283. }))
  284. }
  285. },
  286. async getSignup() {
  287. const signupRes = await this.$api.home.getLink()
  288. if (signupRes.code === 200){
  289. this.recommendList = signupRes.result.map(item => ({
  290. img: item.img,
  291. id: item.id,
  292. type: item.type
  293. }))
  294. }
  295. },
  296. // 获取书籍分类
  297. async getCategory() {
  298. const categoryRes = await this.$api.book.category()
  299. if (categoryRes.code === 200){
  300. this.tabs = categoryRes.result.map(item => ({
  301. title:item.title,
  302. id: item.id
  303. }))
  304. }
  305. },
  306. // 获取书籍标签
  307. async getLabel() {
  308. const labelRes = await this.$api.book.label()
  309. if (labelRes.code === 200){
  310. this.labels = labelRes.result.map(item => ({
  311. title:item.lable,
  312. id: item.id
  313. }))
  314. }
  315. },
  316. // 根据label数组获取书籍数据
  317. async getBooksByLabels() {
  318. if (!this.labels || this.labels.length === 0) {
  319. console.log('labels数据为空,无法获取书籍')
  320. return
  321. }
  322. try {
  323. // 创建请求数组,每个label发起一次请求
  324. const requests = this.labels.map(label =>
  325. this.$api.book.list({
  326. label: label.id,
  327. pageNo: 1,
  328. pageSize: 6,
  329. category: this.tabs[this.activeTab].id
  330. }, false)
  331. )
  332. // 并发执行所有请求
  333. const responses = await Promise.all(requests)
  334. // 创建二维数组存储结果
  335. this.labelBooksData = responses.map((response, index) => {
  336. if (response.code === 200) {
  337. return {
  338. labelInfo: this.labels[index],
  339. books: response.result.records || []
  340. }
  341. } else {
  342. console.error(`获取label ${this.labels[index].title} 的书籍失败:`, response)
  343. return {
  344. labelInfo: this.labels[index],
  345. books: []
  346. }
  347. }
  348. })
  349. console.log('根据label获取的书籍数据:', this.labelBooksData)
  350. } catch (error) {
  351. console.error('获取书籍数据失败:', error)
  352. this.labelBooksData = []
  353. }
  354. },
  355. goLabel(label){
  356. uni.navigateTo({
  357. url: '/subPages/home/search?label=' + label.id
  358. })
  359. },
  360. // 关闭视频弹窗
  361. closeVideoModal() {
  362. this.$refs.videoModal.close()
  363. this.currentVideo = ''
  364. },
  365. // 视频错误处理
  366. onVideoError(e) {
  367. console.error('视频播放错误:', e)
  368. uni.showToast({
  369. title: '视频播放失败',
  370. icon: 'error'
  371. })
  372. this.closeVideoModal()
  373. },
  374. // 视频开始播放
  375. onVideoPlay() {
  376. console.log('视频开始播放')
  377. },
  378. // 视频暂停
  379. onVideoPause() {
  380. console.log('视频暂停播放')
  381. }
  382. },
  383. async onShow() {
  384. // 先获取基础数据
  385. await Promise.all([this.getBanner(), this.getSignup(), this.getCategory(), this.getLabel()])
  386. // 根据label数据获取对应的书籍
  387. await this.getBooksByLabels()
  388. }
  389. }
  390. </script>
  391. <style lang="scss" scoped>
  392. .home-container {
  393. background: #fff;
  394. min-height: 100vh;
  395. padding-bottom: 80rpx;
  396. }
  397. // 顶部搜索栏
  398. .header {
  399. display: flex;
  400. align-items: center;
  401. padding: 6rpx 32rpx;
  402. background: #fff;
  403. .search-container {
  404. flex: 1;
  405. }
  406. }
  407. // Tab栏
  408. .tab-container {
  409. background: #fff;
  410. // border-bottom: 1px solid #f0f0f0;
  411. top: 0;
  412. left: 0;
  413. right: 0;
  414. z-index: 999;
  415. .tab-scroll {
  416. white-space: nowrap;
  417. .tab-list {
  418. display: flex;
  419. padding: 0 20rpx;
  420. .tab-item {
  421. flex-shrink: 0;
  422. padding: 20rpx 20rpx;
  423. font-size: 32rpx;
  424. color: #666;
  425. position: relative;
  426. &.active {
  427. color: $primary-text-color;
  428. font-weight: 700;
  429. &::after {
  430. content: '';
  431. position: absolute;
  432. bottom: 0;
  433. left: 50%;
  434. transform: translateX(-50%);
  435. width: 22rpx;
  436. height: 4rpx;
  437. background: $primary-text-color;
  438. border-radius: 2rpx;
  439. }
  440. }
  441. }
  442. }
  443. }
  444. }
  445. // 轮播图容器
  446. .swiper-container {
  447. margin: 20rpx;
  448. border-radius: 12rpx;
  449. overflow: hidden;
  450. }
  451. // 内容区块
  452. .section {
  453. margin-top: 40rpx;
  454. .section-header {
  455. display: flex;
  456. align-items: center;
  457. justify-content: space-between;
  458. padding: 0 30rpx ;
  459. margin-bottom: 24rpx;
  460. .section-title {
  461. font-size: 36rpx;
  462. // font-weight: 600;
  463. color: $primary-text-color;
  464. }
  465. .section-more {
  466. display: flex;
  467. align-items: center;
  468. gap: 4rpx;
  469. text {
  470. font-size: 24rpx;
  471. color: $secondary-text-color;
  472. }
  473. }
  474. }
  475. .content-scroll {
  476. white-space: nowrap;
  477. }
  478. }
  479. // 今日更新列表
  480. .content-list {
  481. display: flex;
  482. padding: 0 30rpx;
  483. gap: 32rpx;
  484. .content-item {
  485. flex-shrink: 0;
  486. width: 602rpx;
  487. height: 212rpx;
  488. display: flex;
  489. align-items: center;
  490. background: #F8F8F8;
  491. padding: 16rpx;
  492. border-radius: 16rpx;
  493. gap: 16rpx;
  494. .item-cover {
  495. width: 136rpx;
  496. height: 200rpx;
  497. border-radius: 16rpx;
  498. // overflow: hidden;
  499. image {
  500. width: 136rpx;
  501. height: 200rpx;
  502. }
  503. }
  504. .item-info {
  505. // padding-top: 20rpx;
  506. gap: 16rpx;
  507. display: flex;
  508. flex-direction: column;
  509. .item-title {
  510. font-size: 32rpx;
  511. font-weight: 700;
  512. color: $primary-text-color;
  513. letter-spacing: 0;
  514. line-height: 48rpx;
  515. // margin-bottom: 12rpx;
  516. overflow: hidden;
  517. text-overflow: ellipsis;
  518. white-space: nowrap;
  519. }
  520. .item-author {
  521. font-size: 24rpx;
  522. color: $secondary-text-color;
  523. // margin-bottom: 8rpx;
  524. letter-spacing: 0;
  525. overflow: hidden;
  526. text-overflow: ellipsis;
  527. white-space: nowrap;
  528. }
  529. .item-duration {
  530. gap: 12rpx;
  531. display: flex;
  532. align-items: center;
  533. font-size: 22rpx;
  534. letter-spacing: 0;
  535. color: $secondary-text-color;
  536. .item-icon{
  537. width: 22rpx;
  538. height: 25rpx;
  539. }
  540. }
  541. }
  542. }
  543. }
  544. // 推荐书籍列表
  545. .book-list {
  546. display: flex;
  547. padding: 0 30rpx;
  548. gap: 32rpx;
  549. .book-item {
  550. flex-shrink: 0;
  551. width: 270rpx;
  552. transition: transform 0.3s ease, box-shadow 0.3s ease;
  553. &:active {
  554. transform: scale(0.98);
  555. }
  556. .book-cover {
  557. width: 100%;
  558. height: 360rpx;
  559. border-radius: 16rpx;
  560. overflow: hidden;
  561. position: relative;
  562. box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.15);
  563. transition: box-shadow 0.3s ease, transform 0.3s ease;
  564. &:active {
  565. box-shadow: 0 8rpx 30rpx rgba(0, 0, 0, 0.25);
  566. transform: translateY(-2rpx);
  567. }
  568. image {
  569. width: 100%;
  570. height: 100%;
  571. transition: transform 0.3s ease;
  572. }
  573. .book-overlay {
  574. position: absolute;
  575. bottom: 0;
  576. left: 0;
  577. right: 0;
  578. width: 100%;
  579. height: 140rpx;
  580. padding: 20rpx 16rpx 12rpx;
  581. box-sizing: border-box;
  582. /* 优化的渐变遮罩效果 */
  583. background: linear-gradient(
  584. 180deg,
  585. rgba(0, 0, 0, 0) 0%,
  586. rgba(0, 0, 0, 0.3) 30%,
  587. rgba(0, 0, 0, 0.7) 70%,
  588. rgba(0, 0, 0, 0.85) 100%
  589. );
  590. /* 增强的毛玻璃效果 */
  591. backdrop-filter: blur(8px) saturate(1.2);
  592. -webkit-backdrop-filter: blur(8px) saturate(1.2);
  593. /* 添加微妙的边框 */
  594. border-top: 1px solid rgba(255, 255, 255, 0.1);
  595. /* 平滑过渡效果 */
  596. transition: all 0.3s ease;
  597. .book-duration{
  598. display: flex;
  599. align-items: center;
  600. gap: 6rpx;
  601. margin-bottom: 8rpx;
  602. &-icon{
  603. width: 22rpx;
  604. height: 22rpx;
  605. opacity: 0.9;
  606. }
  607. &-text{
  608. font-size: 20rpx;
  609. font-weight: 500;
  610. color: rgba(255, 255, 255, 0.9);
  611. text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
  612. }
  613. }
  614. .book-title {
  615. max-width: 220rpx;
  616. font-size: 24rpx;
  617. font-weight: 600;
  618. line-height: 1.3;
  619. color: #ffffff;
  620. text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
  621. /* 文本截断优化 */
  622. display: -webkit-box;
  623. -webkit-box-orient: vertical;
  624. -webkit-line-clamp: 2;
  625. overflow: hidden;
  626. word-break: break-word;
  627. white-space: normal;
  628. }
  629. }
  630. }
  631. }
  632. }
  633. // 书籍网格布局
  634. .book-grid {
  635. display: flex;
  636. flex-wrap: wrap;
  637. padding: 0 30rpx;
  638. gap: 32rpx;
  639. .book-grid-item {
  640. width: 208rpx;
  641. display: flex;
  642. flex-direction: column;
  643. // backdrop-filter: red;
  644. .book-grid-cover {
  645. box-shadow: 0px 4px 4px 0px #C0BCBA75;
  646. width: 100%;
  647. height: 278rpx;
  648. border-radius: 16rpx;
  649. overflow: hidden;
  650. margin-bottom: 16rpx;
  651. image {
  652. width: 100%;
  653. height: 100%;
  654. }
  655. }
  656. .book-grid-info {
  657. width: 208rpx;
  658. padding: 6rpx;
  659. overflow: hidden;
  660. text-overflow: ellipsis;
  661. white-space: nowrap;
  662. .book-grid-title {
  663. font-size: 28rpx;
  664. font-weight: 700;
  665. color: $primary-text-color;
  666. margin-bottom: 14rpx;
  667. }
  668. .book-grid-meta {
  669. display: flex;
  670. align-items: center;
  671. // gap: 16rpx;
  672. .book-grid-duration-icon {
  673. width: 24rpx;
  674. height: 24rpx;
  675. margin-right: 12rpx;
  676. }
  677. .book-grid-grade {
  678. font-size: 24rpx;
  679. color: $secondary-text-color;
  680. margin-right: 8rpx;
  681. }
  682. .book-grid-duration {
  683. font-size: 24rpx;
  684. color: $secondary-text-color;
  685. }
  686. }
  687. }
  688. }
  689. }
  690. // 推荐列表样式
  691. .recommend-list {
  692. padding: 0 30rpx;
  693. .recommend-item {
  694. width: 100%;
  695. height: 200rpx;
  696. margin-bottom: 48rpx;
  697. border-radius: 32rpx;
  698. overflow: hidden;
  699. &:last-child {
  700. margin-bottom: 0;
  701. }
  702. .recommend-image {
  703. width: 100%;
  704. height: 100%;
  705. }
  706. }
  707. }
  708. // 视频弹窗样式
  709. .video-container {
  710. position: relative;
  711. padding: 20rpx 0;
  712. .video-loading {
  713. display: flex;
  714. align-items: center;
  715. justify-content: center;
  716. height: 400rpx;
  717. background: #f5f5f5;
  718. border-radius: 8rpx;
  719. text {
  720. font-size: 28rpx;
  721. color: #999;
  722. }
  723. }
  724. }
  725. </style>