refactor: 优化音频控制逻辑和列表混入代码 fix: 修复H5环境下用户信息存储问题 style: 调整搜索页面样式和分类标签栏 perf: 优化开屏动画在H5环境下的显示逻辑 docs: 更新package.json项目描述 test: 添加视频弹窗组件和文章列表组件main
| @ -0,0 +1,164 @@ | |||
| <template> | |||
| <view class="book-list"> | |||
| <view v-for="(book, index) in list" :key="index" class="book-item" @click="goToDetail(book)"> | |||
| <view class="book-cover"> | |||
| <image :src="book.booksImg" mode="aspectFill"></image> | |||
| </view> | |||
| <view class="book-info"> | |||
| <view class="book-title">{{ book.booksName }}</view> | |||
| <view class="book-author">{{ book.booksAuthor }}</view> | |||
| <view class="book-meta"> | |||
| <view class="book-duration"> | |||
| <image src="/static/play-icon.png" mode="aspectFill" class="book-icon"></image> | |||
| <text>{{ book.duration }}</text> | |||
| </view> | |||
| <view class="book-membership" :class="classMap[book.vipInfo.title]"> | |||
| {{ book.vipInfo.title }} | |||
| </view> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| <uv-loading-icon text="加载中" textSize="30rpx" v-if="isLoading"></uv-loading-icon> | |||
| <uv-empty v-else-if="list.length === 0"></uv-empty> | |||
| </view> | |||
| </template> | |||
| <script> | |||
| export default { | |||
| name: 'BookList', | |||
| props: { | |||
| list: { | |||
| type: Array, | |||
| default: () => [] | |||
| }, | |||
| isLoading: { | |||
| type: Boolean, | |||
| default: false | |||
| } | |||
| }, | |||
| data() { | |||
| return { | |||
| // 类型映射表 | |||
| classMap: { | |||
| '蕾朵会员': 'book-membership-premium', | |||
| '盛放会员': 'book-membership-vip', | |||
| '萌芽会员': 'book-membership-basic', | |||
| } | |||
| } | |||
| }, | |||
| methods: { | |||
| goToDetail(book) { | |||
| uni.navigateTo({ | |||
| url: '/subPages/home/directory?id=' + book.id | |||
| }) | |||
| } | |||
| } | |||
| } | |||
| </script> | |||
| <style scoped lang="scss"> | |||
| .book-list { | |||
| display: flex; | |||
| flex-direction: column; | |||
| gap: 32rpx; | |||
| } | |||
| .book-item { | |||
| display: flex; | |||
| align-items: center; | |||
| background: #F8F8F8; | |||
| height: 212rpx; | |||
| gap: 16rpx; | |||
| border-radius: 16rpx; | |||
| padding: 0rpx 16rpx; | |||
| &:last-child { | |||
| border-bottom: none; | |||
| } | |||
| .book-cover { | |||
| width: 136rpx; | |||
| height: 180rpx; | |||
| border-radius: 16rpx; | |||
| overflow: hidden; | |||
| margin-right: 16rpx; | |||
| image { | |||
| width: 100%; | |||
| height: 100%; | |||
| } | |||
| } | |||
| .book-info { | |||
| flex: 1; | |||
| display: flex; | |||
| flex-direction: column; | |||
| justify-content: space-between; | |||
| } | |||
| .book-title { | |||
| font-size: 32rpx; | |||
| font-weight: 600; | |||
| color: $primary-text-color; | |||
| line-height: 48rpx; | |||
| letter-spacing: 0; | |||
| margin-bottom: 12rpx; | |||
| overflow: hidden; | |||
| text-overflow: ellipsis; | |||
| } | |||
| .book-author { | |||
| font-size: 24rpx; | |||
| color: $secondary-text-color; | |||
| margin-bottom: 16rpx; | |||
| overflow: hidden; | |||
| text-overflow: ellipsis; | |||
| white-space: nowrap; | |||
| } | |||
| .book-meta { | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: space-between; | |||
| } | |||
| .book-duration { | |||
| display: flex; | |||
| align-items: center; | |||
| font-size: 22rpx; | |||
| color: #999; | |||
| .book-icon { | |||
| width: 18rpx; | |||
| height: 18rpx; | |||
| } | |||
| text { | |||
| margin-left: 8rpx; | |||
| } | |||
| } | |||
| .book-membership { | |||
| padding: 8rpx 16rpx; | |||
| border-radius: 8rpx; | |||
| font-size: 24rpx; | |||
| color: #211508; | |||
| flex-shrink: 0; | |||
| } | |||
| } | |||
| .book-membership-premium { | |||
| background: #E9F1FF; | |||
| border: 2rpx solid #C4DAFF | |||
| } | |||
| .book-membership-vip { | |||
| background: #FFF4E9; | |||
| border: 2rpx solid #FFE2C4 | |||
| } | |||
| .book-membership-basic { | |||
| background: #FFE9E9; | |||
| border: 2rpx solid #FFDBC4 | |||
| } | |||
| </style> | |||
| @ -0,0 +1,251 @@ | |||
| <template> | |||
| <view class="article-section" v-if="articleList.length > 0"> | |||
| <view class="section-header"> | |||
| <text class="section-title">精选文章</text> | |||
| </view> | |||
| <view class="article-list"> | |||
| <view v-for="(article, index) in articleList" :key="article.id" class="article-item" | |||
| @click="goArticleDetail(article)"> | |||
| <view class="article-content"> | |||
| <view class="article-title">{{ article.title }}</view> | |||
| <view class="article-meta"> | |||
| <!-- <view class="article-tag">精选</view> --> | |||
| <text class="article-time">{{ formatTime(article.createTime) }}</text> | |||
| </view> | |||
| </view> | |||
| <view class="article-arrow"> | |||
| <uv-icon name="arrow-right" size="16" color="#ccc"></uv-icon> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| </template> | |||
| <script> | |||
| export default { | |||
| props: { | |||
| articleList: { | |||
| type: Array, | |||
| default: () => [] | |||
| }, | |||
| }, | |||
| methods: { | |||
| goArticleDetail(article) { | |||
| uni.navigateTo({ | |||
| url: '/subPages/home/articleDetail?id=' + article.id | |||
| }) | |||
| }, | |||
| formatTime(timeStr) { | |||
| if (!timeStr) return '' | |||
| // 处理iOS兼容性问题,将 "2025-10-23 16:36:43" 格式转换为 "2025/10/23 16:36:43" | |||
| let formattedTimeStr = timeStr.replace(/-/g, '/') | |||
| const date = new Date(formattedTimeStr) | |||
| // 检查日期是否有效 | |||
| if (isNaN(date.getTime())) { | |||
| console.warn('Invalid date format:', timeStr) | |||
| return timeStr // 返回原始字符串 | |||
| } | |||
| const year = date.getFullYear() | |||
| const month = String(date.getMonth() + 1).padStart(2, '0') | |||
| const day = String(date.getDate()).padStart(2, '0') | |||
| return `${year}-${month}-${day}` | |||
| }, | |||
| } | |||
| } | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| // 文章列表样式 | |||
| .article-section { | |||
| margin-top: 40rpx; | |||
| padding: 0 30rpx; | |||
| .article-scroll { | |||
| white-space: nowrap; | |||
| } | |||
| .article-list { | |||
| display: flex; | |||
| flex-direction: column; | |||
| padding: 0 20rpx; | |||
| gap: 24rpx; | |||
| .article-item { | |||
| flex-shrink: 0; | |||
| // width: 580rpx; | |||
| height: 120rpx; | |||
| background: #f8f9fa; | |||
| border-radius: 16rpx; | |||
| padding: 24rpx; | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: space-between; | |||
| transition: all 0.3s ease; | |||
| &:active { | |||
| transform: scale(0.98); | |||
| background: #f0f1f2; | |||
| } | |||
| .article-content { | |||
| flex: 1; | |||
| display: flex; | |||
| flex-direction: column; | |||
| gap: 12rpx; | |||
| .article-title { | |||
| font-size: 32rpx; | |||
| font-weight: 600; | |||
| color: $primary-text-color; | |||
| line-height: 1.4; | |||
| display: -webkit-box; | |||
| -webkit-box-orient: vertical; | |||
| -webkit-line-clamp: 2; | |||
| overflow: hidden; | |||
| word-break: break-word; | |||
| } | |||
| .article-meta { | |||
| display: flex; | |||
| align-items: center; | |||
| gap: 16rpx; | |||
| .article-tag { | |||
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |||
| color: #fff; | |||
| font-size: 20rpx; | |||
| padding: 4rpx 12rpx; | |||
| border-radius: 12rpx; | |||
| font-weight: 500; | |||
| } | |||
| .article-time { | |||
| font-size: 24rpx; | |||
| color: $secondary-text-color; | |||
| } | |||
| } | |||
| } | |||
| .article-arrow { | |||
| margin-left: 16rpx; | |||
| opacity: 0.6; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| // 书本列表样式(从搜索页面复制) | |||
| .book-list-container { | |||
| background: #fff; | |||
| min-height: 50vh; | |||
| } | |||
| .book-list-results { | |||
| padding: 32rpx; | |||
| display: flex; | |||
| flex-direction: column; | |||
| gap: 32rpx; | |||
| } | |||
| .book-list-item { | |||
| display: flex; | |||
| align-items: center; | |||
| background: #F8F8F8; | |||
| height: 212rpx; | |||
| gap: 16rpx; | |||
| border-radius: 16rpx; | |||
| padding: 0rpx 16rpx; | |||
| &:last-child { | |||
| border-bottom: none; | |||
| } | |||
| .book-list-cover { | |||
| width: 136rpx; | |||
| height: 180rpx; | |||
| border-radius: 16rpx; | |||
| overflow: hidden; | |||
| margin-right: 16rpx; | |||
| image { | |||
| width: 100%; | |||
| height: 100%; | |||
| } | |||
| } | |||
| .book-list-info { | |||
| flex: 1; | |||
| display: flex; | |||
| flex-direction: column; | |||
| justify-content: space-between; | |||
| } | |||
| .book-list-title { | |||
| font-size: 32rpx; | |||
| font-weight: 600; | |||
| color: $primary-text-color; | |||
| line-height: 48rpx; | |||
| letter-spacing: 0; | |||
| margin-bottom: 12rpx; | |||
| overflow: hidden; | |||
| text-overflow: ellipsis; | |||
| } | |||
| .book-list-author { | |||
| font-size: 24rpx; | |||
| color: $secondary-text-color; | |||
| margin-bottom: 16rpx; | |||
| overflow: hidden; | |||
| text-overflow: ellipsis; | |||
| white-space: nowrap; | |||
| } | |||
| .book-list-meta { | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: space-between; | |||
| } | |||
| .book-list-duration { | |||
| display: flex; | |||
| align-items: center; | |||
| font-size: 22rpx; | |||
| color: #999; | |||
| .book-list-icon { | |||
| width: 18rpx; | |||
| height: 18rpx; | |||
| } | |||
| text { | |||
| margin-left: 8rpx; | |||
| } | |||
| } | |||
| .book-list-membership { | |||
| padding: 8rpx 16rpx; | |||
| border-radius: 8rpx; | |||
| font-size: 24rpx; | |||
| color: #211508; | |||
| } | |||
| } | |||
| .book-membership-premium { | |||
| background: #E9F1FF; | |||
| border: 2rpx solid #C4DAFF; | |||
| } | |||
| .book-membership-vip { | |||
| background: #FFF4E9; | |||
| border: 2rpx solid #FFE2C4; | |||
| } | |||
| .book-membership-basic { | |||
| background: #FFE9E9; | |||
| border: 2rpx solid #FFDBC4; | |||
| } | |||
| </style> | |||
| @ -1,138 +1,138 @@ | |||
| // 简化版列表的混入 | |||
| export default { | |||
| data() { | |||
| return { | |||
| list: [], | |||
| pageNo : 1, | |||
| pageSize : 8, | |||
| mixinListApi: '', | |||
| isLoading: false, | |||
| hasMore: true, | |||
| // 额外返回出去的数据 | |||
| extraData: null, | |||
| // 每次更新数据后执行的函数 可以进行数据处理 | |||
| afterUpdateDataFn: function() {}, | |||
| // 每次更新数据前执行的函数, | |||
| beforeUpdateDataFn: function() {}, | |||
| // 混入配置 | |||
| mixinListConfig: { | |||
| // 数据返回的直接路径 | |||
| responsePath: 'result.records', | |||
| // 列表是否需要下拉刷新 | |||
| isPullDownRefresh: true, | |||
| // 列表是否需要上拉加载 | |||
| isReachBottomLoad: true, | |||
| // 额外返回出去的数据的路径 | |||
| extraDataPath: '' , | |||
| // 自定义onShow | |||
| customOnShow: false, | |||
| } | |||
| } | |||
| }, | |||
| computed: { | |||
| // 自定义onShow前会执行的函数 | |||
| mixinFnBeforePageShow() { | |||
| return function() {} | |||
| } | |||
| }, | |||
| methods: { | |||
| // 获取文件的自定义传参 -- 可以在页面中重写 | |||
| mixinSetParams() { | |||
| return {} | |||
| }, | |||
| // 解析分路径获取嵌套值 | |||
| resolvePath(obj, path) { | |||
| if (path){ | |||
| return path.split('.').reduce((acc, key) => (acc && acc[key] !== undefined ? acc[key] : null), obj) | |||
| }else { | |||
| return obj | |||
| } | |||
| data() { | |||
| return { | |||
| list: [], | |||
| pageNo: 1, | |||
| pageSize: 8, | |||
| mixinListApi: '', | |||
| isLoading: false, | |||
| hasMore: true, | |||
| // 额外返回出去的数据 | |||
| extraData: null, | |||
| // 每次更新数据后执行的函数 可以进行数据处理 | |||
| afterUpdateDataFn: function () { }, | |||
| // 每次更新数据前执行的函数, | |||
| beforeUpdateDataFn: function () { }, | |||
| // 混入配置 | |||
| mixinListConfig: { | |||
| // 数据返回的直接路径 | |||
| responsePath: 'result.records', | |||
| // 列表是否需要下拉刷新 | |||
| isPullDownRefresh: true, | |||
| // 列表是否需要上拉加载 | |||
| isReachBottomLoad: true, | |||
| // 额外返回出去的数据的路径 | |||
| extraDataPath: '', | |||
| // 自定义onShow | |||
| customOnShow: false, | |||
| } | |||
| } | |||
| }, | |||
| // 初始化分页 | |||
| initPage(){ | |||
| this.pageNo = 1, | |||
| this.hasMore = true | |||
| computed: { | |||
| // 自定义onShow前会执行的函数 | |||
| mixinFnBeforePageShow() { | |||
| return function () { } | |||
| } | |||
| }, | |||
| // 获取列表 | |||
| async getList(isRefresh = false) { | |||
| // console.log('本次请求的pageNo和pageSize', this.pageNo, this.pageSize) | |||
| methods: { | |||
| // 获取文件的自定义传参 -- 可以在页面中重写 | |||
| mixinSetParams() { | |||
| return {} | |||
| }, | |||
| // 解析分路径获取嵌套值 | |||
| resolvePath(obj, path) { | |||
| if (path) { | |||
| return path.split('.').reduce((acc, key) => (acc && acc[key] !== undefined ? acc[key] : null), obj) | |||
| } else { | |||
| return obj | |||
| } | |||
| }, | |||
| // 初始化分页 | |||
| initPage() { | |||
| this.pageNo = 1 | |||
| this.hasMore = true | |||
| }, | |||
| // 获取列表 | |||
| async getList(isRefresh = false) { | |||
| // console.log('本次请求的pageNo和pageSize', this.pageNo, this.pageSize) | |||
| if (!this.hasMore) { | |||
| return | |||
| } | |||
| this.isLoading = true | |||
| const apiMethod = this.resolvePath(this.$api, this.mixinListApi) | |||
| if (typeof apiMethod !== 'function') { | |||
| console.log('mixinApi不存在', this.mixinListApi); | |||
| return | |||
| } | |||
| // 每次更新数据前执行的函数 | |||
| if (this.beforeUpdateDataFn) { | |||
| this.beforeUpdateDataFn(this.list) | |||
| } | |||
| const res = await apiMethod({ | |||
| pageNo: this.pageNo, | |||
| pageSize: this.pageSize, | |||
| ...this.mixinSetParams() | |||
| }) | |||
| const resData = this.resolvePath(res, this.mixinListConfig.responsePath) || [] | |||
| if (res.code === 200) { | |||
| // 如果没有值了 | |||
| if (!resData.length) { | |||
| this.hasMore = false | |||
| // uni.showToast({ | |||
| // title: '暂无更多数据', | |||
| // icon: 'none' | |||
| // }) | |||
| }else { | |||
| this.pageNo++ | |||
| } | |||
| if (!this.hasMore) { | |||
| return | |||
| } | |||
| this.isLoading = true | |||
| const apiMethod = this.resolvePath(this.$api, this.mixinListApi) | |||
| if (typeof apiMethod !== 'function') { | |||
| console.log('mixinApi不存在', this.mixinListApi); | |||
| return | |||
| } | |||
| // 每次更新数据前执行的函数 | |||
| if (this.beforeUpdateDataFn) { | |||
| this.beforeUpdateDataFn(this.list) | |||
| } | |||
| if (isRefresh ) { | |||
| // 如果是刷新,直接覆盖 | |||
| this.list = resData | |||
| } else { | |||
| this.list = [...this.list, ...resData] | |||
| } | |||
| const res = await apiMethod({ | |||
| pageNo: this.pageNo, | |||
| pageSize: this.pageSize, | |||
| ...this.mixinSetParams() | |||
| }) | |||
| const resData = this.resolvePath(res, this.mixinListConfig.responsePath) || [] | |||
| if (res.code === 200) { | |||
| // 如果没有值了 | |||
| if (!resData.length) { | |||
| this.hasMore = false | |||
| // uni.showToast({ | |||
| // title: '暂无更多数据', | |||
| // icon: 'none' | |||
| // }) | |||
| } else { | |||
| this.pageNo++ | |||
| } | |||
| if (isRefresh) { | |||
| // 如果是刷新,直接覆盖 | |||
| this.list = resData | |||
| // 如果有额外数据的路径,刷新后,需要将额外数据也刷新 | |||
| if (this.mixinListConfig.extraDataPath !== '') { | |||
| this.extraData = this.resolvePath(res, this.mixinListConfig.extraDataPath) | |||
| } else { | |||
| this.list = [...this.list, ...resData] | |||
| } | |||
| // 如果有额外数据的路径,刷新后,需要将额外数据也刷新 | |||
| if (this.mixinListConfig.extraDataPath !== '') { | |||
| this.extraData = this.resolvePath(res, this.mixinListConfig.extraDataPath) | |||
| } | |||
| } | |||
| // 每次更新数据后执行的函数 | |||
| if (this.afterUpdateDataFn) { | |||
| this.afterUpdateDataFn(this.list) | |||
| } | |||
| // 如果有在加载中 | |||
| if (this.isLoading) { | |||
| this.isLoading = false | |||
| } | |||
| // 有过有在下拉加载 | |||
| uni.stopPullDownRefresh() | |||
| }, | |||
| }, | |||
| async onShow() { | |||
| if (!this.mixinListConfig.customOnShow) { | |||
| if (this.mixinFnBeforePageShow) this.mixinFnBeforePageShow() | |||
| this.initPage() | |||
| await this.getList(true) | |||
| } | |||
| } | |||
| // 每次更新数据后执行的函数 | |||
| if (this.afterUpdateDataFn) { | |||
| this.afterUpdateDataFn(this.list) | |||
| } | |||
| // 如果有在加载中 | |||
| if (this.isLoading) { | |||
| this.isLoading = false | |||
| } | |||
| // 有过有在下拉加载 | |||
| uni.stopPullDownRefresh() | |||
| }, | |||
| }, | |||
| async onShow() { | |||
| if (!this.mixinListConfig.customOnShow) { | |||
| if (this.mixinFnBeforePageShow) this.mixinFnBeforePageShow() | |||
| this.initPage() | |||
| await this.getList(true) | |||
| } | |||
| }, | |||
| async onPullDownRefresh() { | |||
| // 在下拉还没结束前 不做任何操作 | |||
| if (this.isLoading) { | |||
| return | |||
| } | |||
| this.initPage() | |||
| await this.getList(true) | |||
| }, | |||
| async onReachBottom() { | |||
| if (this.isLoading) { | |||
| return | |||
| async onPullDownRefresh() { | |||
| // 在下拉还没结束前 不做任何操作 | |||
| if (this.isLoading) { | |||
| return | |||
| } | |||
| this.initPage() | |||
| await this.getList(true) | |||
| }, | |||
| async onReachBottom() { | |||
| if (this.isLoading) { | |||
| return | |||
| } | |||
| await this.getList() | |||
| } | |||
| await this.getList() | |||
| } | |||
| } | |||
| @ -0,0 +1,25 @@ | |||
| { | |||
| "name": "englishread-front", | |||
| "version": "1.0.0", | |||
| "description": "四零语境", | |||
| "main": "main.js", | |||
| "scripts": { | |||
| "dev:h5": "uni build --platform h5 --watch", | |||
| "build:h5": "uni build --platform h5", | |||
| "serve": "uni serve" | |||
| }, | |||
| "dependencies": { | |||
| "@dcloudio/uni-app": "^2.0.0", | |||
| "@dcloudio/uni-h5": "^2.0.0", | |||
| "@dcloudio/uni-helper-json": "*" | |||
| }, | |||
| "devDependencies": { | |||
| "@dcloudio/uni-cli-shared": "*", | |||
| "@dcloudio/webpack-uni-mp-loader": "*", | |||
| "@dcloudio/webpack-uni-pages-loader": "*" | |||
| }, | |||
| "browserslist": [ | |||
| "Android >= 4.4", | |||
| "ios >= 9" | |||
| ] | |||
| } | |||
| @ -0,0 +1,113 @@ | |||
| <template> | |||
| <uv-popup | |||
| ref="videoModal" | |||
| title="视频播放" | |||
| :show-cancel-button="false" | |||
| :show-confirm-button="false" | |||
| :close-on-click-overlay="true" | |||
| :safeAreaInsetBottom="false" | |||
| @close="handleClose" | |||
| > | |||
| <template #default> | |||
| <view class="video-container"> | |||
| <video | |||
| v-if="currentVideo" | |||
| :src="currentVideo" | |||
| controls | |||
| autoplay | |||
| :show-fullscreen-btn="true" | |||
| :show-play-btn="true" | |||
| :show-center-play-btn="true" | |||
| style="width: 100%; height: 400rpx; border-radius: 8rpx;" | |||
| @error="onVideoError" | |||
| @play="onVideoPlay" | |||
| @pause="onVideoPause" | |||
| ></video> | |||
| <view v-else class="video-loading"> | |||
| <text>视频加载中...</text> | |||
| </view> | |||
| </view> | |||
| </template> | |||
| </uv-popup> | |||
| </template> | |||
| <script> | |||
| export default { | |||
| name: 'VideoPopup', | |||
| data() { | |||
| return { | |||
| currentVideo: '' | |||
| } | |||
| }, | |||
| methods: { | |||
| // 打开视频弹窗 | |||
| open(videoUrl) { | |||
| if (videoUrl) { | |||
| this.currentVideo = videoUrl | |||
| this.$refs.videoModal.open() | |||
| } else { | |||
| uni.showToast({ | |||
| title: '视频地址无效', | |||
| icon: 'error' | |||
| }) | |||
| } | |||
| }, | |||
| // 关闭视频弹窗 | |||
| close() { | |||
| this.$refs.videoModal.close() | |||
| this.currentVideo = '' | |||
| }, | |||
| // 处理弹窗关闭事件 | |||
| handleClose() { | |||
| this.currentVideo = '' | |||
| this.$emit('close') | |||
| }, | |||
| // 视频错误处理 | |||
| onVideoError(e) { | |||
| console.error('视频播放错误:', e) | |||
| uni.showToast({ | |||
| title: '视频播放失败', | |||
| icon: 'error' | |||
| }) | |||
| this.close() | |||
| this.$emit('error', e) | |||
| }, | |||
| // 视频开始播放 | |||
| onVideoPlay() { | |||
| console.log('视频开始播放') | |||
| this.$emit('play') | |||
| }, | |||
| // 视频暂停 | |||
| onVideoPause() { | |||
| console.log('视频暂停播放') | |||
| this.$emit('pause') | |||
| } | |||
| } | |||
| } | |||
| </script> | |||
| <style scoped lang="scss"> | |||
| .video-container { | |||
| position: relative; | |||
| width: 90vw; | |||
| .video-loading { | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| height: 400rpx; | |||
| background: #f5f5f5; | |||
| border-radius: 8rpx; | |||
| text { | |||
| font-size: 28rpx; | |||
| color: #999; | |||
| } | |||
| } | |||
| } | |||
| </style> | |||
| @ -0,0 +1,385 @@ | |||
| <template> | |||
| <view class="article-detail"> | |||
| <!-- 导航栏 --> | |||
| <!-- #ifndef H5 --> | |||
| <uv-navbar :placeholder="true" left-icon="arrow-left" :title="articleData.title || '文章详情'" | |||
| @leftClick="goBack"></uv-navbar> | |||
| <!-- #endif --> | |||
| <!-- 文章内容 --> | |||
| <view class="article-container" v-if="articleData.id"> | |||
| <!-- 文章标题 --> | |||
| <view class="article-title">{{ articleData.title }}</view> | |||
| <!-- 文章信息 --> | |||
| <view class="article-info"> | |||
| <text class="article-time">{{ formatTime(articleData.createTime) }}</text> | |||
| <!-- <text class="article-author">{{ articleData.createBy }}</text> --> | |||
| </view> | |||
| <!-- 文章内容 --> | |||
| <view class="article-content"> | |||
| <uv-parse :content="articleData.content"/> | |||
| </view> | |||
| </view> | |||
| <!-- 加载状态 --> | |||
| <view class="loading-container" v-else-if="loading"> | |||
| <uv-loading-icon mode="circle"></uv-loading-icon> | |||
| <text class="loading-text">加载中...</text> | |||
| </view> | |||
| <!-- 音频播放悬浮按钮 --> | |||
| <view class="audio-float-button" v-if="hasAudio && articleData.id" @click="toggleAudio"> | |||
| <uv-icon :name="isPlaying ? 'pause-circle-fill' : 'play-circle-fill'" size="50" | |||
| :color="isPlaying ? '#ff6b6b' : '#4CAF50'"></uv-icon> | |||
| </view> | |||
| </view> | |||
| </template> | |||
| <script> | |||
| import audioManager from '@/utils/audioManager.js' | |||
| export default { | |||
| data() { | |||
| return { | |||
| articleId: '', | |||
| articleData: {}, | |||
| loading: true, | |||
| isPlaying: false, | |||
| audioUrl: '', | |||
| hasAudio: false | |||
| } | |||
| }, | |||
| onLoad(options) { | |||
| if (options.id) { | |||
| this.articleId = options.id | |||
| this.getArticleDetail() | |||
| } | |||
| // 绑定音频管理器事件监听 | |||
| this.bindAudioEvents() | |||
| }, | |||
| onUnload() { | |||
| // 页面卸载时清理音频资源和事件监听 | |||
| this.stopAudio() | |||
| this.unbindAudioEvents() | |||
| }, | |||
| methods: { | |||
| // 绑定音频管理器事件监听 | |||
| bindAudioEvents() { | |||
| audioManager.on('play', this.onAudioPlay) | |||
| audioManager.on('pause', this.onAudioPause) | |||
| audioManager.on('ended', this.onAudioEnded) | |||
| audioManager.on('error', this.onAudioError) | |||
| }, | |||
| // 解绑音频管理器事件监听 | |||
| unbindAudioEvents() { | |||
| audioManager.off('play', this.onAudioPlay) | |||
| audioManager.off('pause', this.onAudioPause) | |||
| audioManager.off('ended', this.onAudioEnded) | |||
| audioManager.off('error', this.onAudioError) | |||
| }, | |||
| // 音频播放开始事件 | |||
| onAudioPlay(data) { | |||
| if (data.audioType === 'article') { | |||
| this.isPlaying = true | |||
| console.log('文章音频开始播放') | |||
| } | |||
| }, | |||
| // 音频暂停事件 | |||
| onAudioPause(data) { | |||
| if (data.audioType === 'article') { | |||
| this.isPlaying = false | |||
| console.log('文章音频暂停播放') | |||
| } | |||
| }, | |||
| // 音频播放结束事件 | |||
| onAudioEnded(data) { | |||
| if (data.audioType === 'article') { | |||
| this.isPlaying = false | |||
| console.log('文章音频播放结束') | |||
| } | |||
| }, | |||
| // 音频播放错误事件 | |||
| onAudioError(data) { | |||
| if (data.audioType === 'article') { | |||
| this.isPlaying = false | |||
| console.error('文章音频播放错误:', data.error) | |||
| uni.showToast({ | |||
| title: '音频播放失败', | |||
| icon: 'none' | |||
| }) | |||
| } | |||
| }, | |||
| // 返回上一页 | |||
| goBack() { | |||
| uni.navigateBack() | |||
| }, | |||
| // 获取文章详情 | |||
| async getArticleDetail() { | |||
| try { | |||
| this.loading = true | |||
| const res = await this.$api.home.getArticleDetail({ id: this.articleId }) | |||
| if (res.code === 200) { | |||
| this.articleData = res.result | |||
| // #ifdef H5 | |||
| window.document.title = this.articleData.title || '文章详情' | |||
| // #endif | |||
| // 处理音频数据 | |||
| if (res.result.audios && Object.keys(res.result.audios).length > 0) { | |||
| // 获取第一个音频URL | |||
| const audioKeys = Object.keys(res.result.audios) | |||
| this.audioUrl = res.result.audios[audioKeys[0]] | |||
| this.hasAudio = true | |||
| } | |||
| } else { | |||
| uni.showToast({ | |||
| title: res.message || '获取文章详情失败', | |||
| icon: 'none' | |||
| }) | |||
| } | |||
| } catch (error) { | |||
| console.error('获取文章详情失败:', error) | |||
| uni.showToast({ | |||
| title: '网络错误,请重试', | |||
| icon: 'none' | |||
| }) | |||
| } finally { | |||
| this.loading = false | |||
| } | |||
| }, | |||
| // 切换音频播放状态 | |||
| toggleAudio() { | |||
| if (!this.audioUrl) { | |||
| console.error('音频URL为空') | |||
| return | |||
| } | |||
| try { | |||
| if (this.isPlaying) { | |||
| // 暂停音频 | |||
| this.pauseAudio() | |||
| } else { | |||
| // 播放音频 | |||
| this.playAudio() | |||
| } | |||
| } catch (error) { | |||
| console.error('音频播放切换异常:', error) | |||
| this.isPlaying = false | |||
| } | |||
| }, | |||
| // 播放音频 | |||
| async playAudio() { | |||
| if (!this.audioUrl) { | |||
| console.error('音频URL为空') | |||
| return | |||
| } | |||
| try { | |||
| // 使用audioManager播放音频,指定音频类型为'article' | |||
| await audioManager.playAudio(this.audioUrl, 'article') | |||
| console.log('开始播放文章音频:', this.audioUrl) | |||
| } catch (error) { | |||
| console.error('音频播放异常:', error) | |||
| this.isPlaying = false | |||
| uni.showToast({ | |||
| title: '音频播放失败', | |||
| icon: 'none' | |||
| }) | |||
| } | |||
| }, | |||
| // 暂停音频 | |||
| pauseAudio() { | |||
| try { | |||
| audioManager.pause() | |||
| console.log('暂停文章音频') | |||
| } catch (error) { | |||
| console.error('暂停音频失败:', error) | |||
| } | |||
| }, | |||
| // 停止音频 | |||
| stopAudio() { | |||
| try { | |||
| audioManager.stopCurrentAudio() | |||
| this.isPlaying = false | |||
| console.log('停止文章音频') | |||
| } catch (error) { | |||
| console.error('停止音频失败:', error) | |||
| } | |||
| }, | |||
| // 格式化时间 | |||
| formatTime(timeStr) { | |||
| if (!timeStr) return '' | |||
| // 处理iOS兼容性问题,将 "2025-10-23 16:36:43" 格式转换为 "2025/10/23 16:36:43" | |||
| let formattedTimeStr = timeStr.replace(/-/g, '/') | |||
| const date = new Date(formattedTimeStr) | |||
| // 检查日期是否有效 | |||
| if (isNaN(date.getTime())) { | |||
| console.warn('Invalid date format:', timeStr) | |||
| return timeStr // 返回原始字符串 | |||
| } | |||
| const year = date.getFullYear() | |||
| const month = String(date.getMonth() + 1).padStart(2, '0') | |||
| const day = String(date.getDate()).padStart(2, '0') | |||
| return `${year}-${month}-${day}` | |||
| } | |||
| } | |||
| } | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| .article-detail { | |||
| min-height: 100vh; | |||
| background: #fff; | |||
| } | |||
| .article-container { | |||
| padding: 30rpx; | |||
| } | |||
| .article-title { | |||
| font-size: 40rpx; | |||
| font-weight: 600; | |||
| color: #333; | |||
| line-height: 1.4; | |||
| margin-bottom: 30rpx; | |||
| } | |||
| .article-info { | |||
| display: flex; | |||
| align-items: center; | |||
| gap: 30rpx; | |||
| margin-bottom: 40rpx; | |||
| padding-bottom: 30rpx; | |||
| border-bottom: 1px solid #f0f0f0; | |||
| .article-time { | |||
| font-size: 26rpx; | |||
| color: #999; | |||
| } | |||
| .article-author { | |||
| font-size: 26rpx; | |||
| color: #666; | |||
| &::before { | |||
| content: '作者:'; | |||
| } | |||
| } | |||
| } | |||
| .article-content { | |||
| font-size: 32rpx; | |||
| line-height: 1.8; | |||
| color: #333; | |||
| // 富文本内容样式 | |||
| :deep(.rich-text) { | |||
| p { | |||
| margin-bottom: 20rpx; | |||
| line-height: 1.8; | |||
| } | |||
| img { | |||
| max-width: 100%; | |||
| height: auto; | |||
| border-radius: 8rpx; | |||
| margin: 20rpx 0; | |||
| } | |||
| section { | |||
| margin: 20rpx 0; | |||
| } | |||
| } | |||
| } | |||
| .loading-container { | |||
| display: flex; | |||
| flex-direction: column; | |||
| align-items: center; | |||
| justify-content: center; | |||
| height: 400rpx; | |||
| .loading-text { | |||
| margin-top: 20rpx; | |||
| font-size: 28rpx; | |||
| color: #999; | |||
| } | |||
| } | |||
| // 音频播放悬浮按钮 | |||
| .audio-float-button { | |||
| position: fixed; | |||
| right: 30rpx; | |||
| bottom: 100rpx; | |||
| width: 100rpx; | |||
| height: 100rpx; | |||
| border-radius: 50%; | |||
| background: rgba(255, 255, 255, 0.9); | |||
| box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1); | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| z-index: 999; | |||
| transition: all 0.3s ease; | |||
| &:active { | |||
| transform: scale(0.95); | |||
| } | |||
| // 添加呼吸动画效果 | |||
| &::before { | |||
| content: ''; | |||
| position: absolute; | |||
| top: -10rpx; | |||
| left: -10rpx; | |||
| right: -10rpx; | |||
| bottom: -10rpx; | |||
| border-radius: 50%; | |||
| background: rgba(76, 175, 80, 0.2); | |||
| animation: pulse 2s infinite; | |||
| z-index: -1; | |||
| } | |||
| } | |||
| @keyframes pulse { | |||
| 0% { | |||
| transform: scale(1); | |||
| opacity: 1; | |||
| } | |||
| 50% { | |||
| transform: scale(1.1); | |||
| opacity: 0.7; | |||
| } | |||
| 100% { | |||
| transform: scale(1); | |||
| opacity: 1; | |||
| } | |||
| } | |||
| </style> | |||
| @ -0,0 +1,147 @@ | |||
| <template> | |||
| <!-- 悬浮按钮 - 只在最后一页显示 --> | |||
| <div v-if="isLastPage" class="floating-buttons"> | |||
| <button | |||
| v-if="hasNextCourse" | |||
| class="floating-button next-lesson" | |||
| @click="handleNextCourse" | |||
| > | |||
| <span class="floating-button-text">下一课</span> | |||
| </button> | |||
| <button | |||
| class="floating-button back-to-start" | |||
| @click="handleBackToStart" | |||
| > | |||
| <span class="floating-button-text">回到开始</span> | |||
| </button> | |||
| </div> | |||
| </template> | |||
| <script> | |||
| export default { | |||
| name: 'FloatingButtons', | |||
| props: { | |||
| // 是否为最后一页 | |||
| isLastPage: { | |||
| type: Boolean, | |||
| default: false | |||
| }, | |||
| // 是否有下一课 | |||
| hasNextCourse: { | |||
| type: Boolean, | |||
| default: false | |||
| } | |||
| }, | |||
| methods: { | |||
| // 跳转到下一课 | |||
| handleNextCourse() { | |||
| this.$emit('next-course'); | |||
| }, | |||
| // 回到开始 | |||
| handleBackToStart() { | |||
| this.$emit('back-to-start'); | |||
| } | |||
| } | |||
| } | |||
| </script> | |||
| <style scoped> | |||
| /* 悬浮按钮样式 */ | |||
| .floating-buttons { | |||
| position: fixed; | |||
| top: 50%; | |||
| right: 30rpx; | |||
| transform: translateY(-50%); | |||
| z-index: 1000; | |||
| display: flex; | |||
| flex-direction: column; | |||
| gap: 16rpx; | |||
| } | |||
| .floating-button { | |||
| width: 180rpx; | |||
| height: 88rpx; | |||
| background: linear-gradient(135deg, #06DADC 0%, #04B8BA 100%); | |||
| border-radius: 44rpx; | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| box-shadow: 0 6rpx 24rpx rgba(6, 218, 220, 0.25); | |||
| transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |||
| border: none; | |||
| outline: none; | |||
| position: relative; | |||
| overflow: hidden; | |||
| } | |||
| .floating-button::before { | |||
| content: ''; | |||
| position: absolute; | |||
| top: 0; | |||
| left: 0; | |||
| right: 0; | |||
| bottom: 0; | |||
| background: linear-gradient(135deg, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0.05) 100%); | |||
| border-radius: 44rpx; | |||
| opacity: 0; | |||
| transition: opacity 0.3s ease; | |||
| } | |||
| .floating-button:hover::before { | |||
| opacity: 1; | |||
| } | |||
| .floating-button:active { | |||
| transform: scale(0.96); | |||
| box-shadow: 0 3rpx 12rpx rgba(6, 218, 220, 0.35); | |||
| } | |||
| .floating-button-text { | |||
| font-family: PingFang SC, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; | |||
| font-weight: 600; | |||
| font-size: 26rpx; | |||
| color: #FFFFFF; | |||
| text-align: center; | |||
| line-height: 1; | |||
| letter-spacing: 0.5rpx; | |||
| position: relative; | |||
| z-index: 1; | |||
| } | |||
| .floating-button.next-lesson { | |||
| background: linear-gradient(135deg, #06DADC 0%, #04B8BA 100%); | |||
| box-shadow: 0 6rpx 24rpx rgba(6, 218, 220, 0.25); | |||
| } | |||
| .floating-button.next-lesson:active { | |||
| box-shadow: 0 3rpx 12rpx rgba(6, 218, 220, 0.35); | |||
| } | |||
| .floating-button.back-to-start { | |||
| background: linear-gradient(135deg, #FF7B7B 0%, #FF6B6B 100%); | |||
| box-shadow: 0 6rpx 24rpx rgba(255, 107, 107, 0.25); | |||
| } | |||
| .floating-button.back-to-start:active { | |||
| box-shadow: 0 3rpx 12rpx rgba(255, 107, 107, 0.35); | |||
| } | |||
| /* 响应式调整 */ | |||
| @media (max-width: 750px) { | |||
| .floating-buttons { | |||
| top: 50%; | |||
| right: 24rpx; | |||
| transform: translateY(-50%); | |||
| } | |||
| .floating-button { | |||
| width: 160rpx; | |||
| height: 80rpx; | |||
| border-radius: 40rpx; | |||
| } | |||
| .floating-button-text { | |||
| font-size: 24rpx; | |||
| } | |||
| } | |||
| </style> | |||
| @ -1,321 +1,140 @@ | |||
| <template> | |||
| <view class="search-container"> | |||
| <!-- 顶部搜索栏 --> | |||
| <view class="search-header"> | |||
| <uv-search | |||
| v-model="searchKeyword" | |||
| placeholder="请输入内容" | |||
| :show-action="true" | |||
| action-text="搜索" | |||
| :action-style="{ | |||
| color: '#fff', | |||
| backgroundColor: '#06DADC', | |||
| borderRadius: '198rpx', | |||
| width: '100rpx', | |||
| height: '64rpx', | |||
| textAlign: 'center', | |||
| fontSize: '26rpx', | |||
| lineHeight: '64rpx', | |||
| }" | |||
| @search="handleSearch" | |||
| @custom="handleSearch" | |||
| @clear="handleSearch" | |||
| ></uv-search> | |||
| <!-- 分类标签栏 --> | |||
| <view class="category-tabs"> | |||
| <scroll-view scroll-x class="tab-scroll"> | |||
| <view class="tab-list"> | |||
| <view | |||
| v-for="(tab, index) in categoryTabs" | |||
| :key="index" | |||
| class="tab-item" | |||
| :class="{ active: currentTab === index }" | |||
| @click="switchTab(index)" | |||
| > | |||
| {{ tab.title }} | |||
| </view> | |||
| </view> | |||
| </scroll-view> | |||
| </view> | |||
| </view> | |||
| <!-- 搜索结果列表 --> | |||
| <view class="search-results"> | |||
| <view | |||
| v-for="(book, index) in list" | |||
| :key="index" | |||
| class="book-item" | |||
| @click="goToDetail(book)" | |||
| > | |||
| <view class="book-cover"> | |||
| <image :src="book.booksImg" mode="aspectFill"></image> | |||
| <view class="search-container"> | |||
| <!-- 顶部搜索栏 --> | |||
| <view class="search-header"> | |||
| <uv-search v-model="searchKeyword" placeholder="请输入内容" :show-action="true" action-text="搜索" :action-style="{ | |||
| color: '#fff', | |||
| backgroundColor: '#06DADC', | |||
| borderRadius: '198rpx', | |||
| width: '100rpx', | |||
| height: '64rpx', | |||
| textAlign: 'center', | |||
| fontSize: '26rpx', | |||
| lineHeight: '64rpx', | |||
| }" @search="handleSearch" @custom="handleSearch" @clear="handleSearch"></uv-search> | |||
| <!-- 分类标签栏 --> | |||
| <uv-tabs | |||
| :list="categoryTabs" | |||
| :current="currentTab" | |||
| keyName="title" | |||
| @change="switchTab" | |||
| :scrollable="true" | |||
| :lineColor="'#000'" | |||
| :activeStyle="{ color: '#000', fontWeight: '900' }" | |||
| :inactiveStyle="{ color: '#606266' }" | |||
| ></uv-tabs> | |||
| </view> | |||
| <view class="book-info"> | |||
| <view class="book-title">{{ book.booksName }}</view> | |||
| <view class="book-author">{{ book.booksAuthor }}</view> | |||
| <view class="book-meta"> | |||
| <view class="book-duration"> | |||
| <image src="/static/play-icon.png" mode="aspectFill" class="book-icon"></image> | |||
| <text>{{ book.duration }}</text> | |||
| </view> | |||
| <view class="book-membership" :class="classMap[book.vipInfo.title]"> | |||
| {{ book.vipInfo.title }} | |||
| </view> | |||
| </view> | |||
| <!-- 搜索结果列表 --> | |||
| <view class="search-results"> | |||
| <BookList :list="list" :isLoading="isLoading" /> | |||
| </view> | |||
| </view> | |||
| <uv-loading-icon text="加载中" textSize="30rpx" v-if="isLoading" ></uv-loading-icon> | |||
| <uv-empty v-else-if="list.length === 0" ></uv-empty> | |||
| </view> | |||
| </view> | |||
| </template> | |||
| <script> | |||
| import MixinList from '@/mixins/list.js' | |||
| import BookList from '@/components/BookList.vue' | |||
| export default { | |||
| mixins: [MixinList], | |||
| data() { | |||
| return { | |||
| mixinListApi: 'book.list', | |||
| // 自定义onShow | |||
| mixinListConfig: { | |||
| customOnShow: true, | |||
| }, | |||
| searchKeyword: '', | |||
| label : '', | |||
| currentTab: 0, | |||
| categoryTabs: [ ], | |||
| // 类型引射表 | |||
| classMap: { | |||
| '蕾朵会员': 'book-membership-premium', | |||
| '盛放会员': 'book-membership-vip', | |||
| '萌芽会员': 'book-membership-basic', | |||
| }, | |||
| bookList: [ | |||
| ] | |||
| } | |||
| }, | |||
| methods: { | |||
| mixinSetParams(){ | |||
| const params = { | |||
| category: this.categoryTabs[this.currentTab].id, | |||
| } | |||
| if(this.label){ | |||
| params.label = this.label | |||
| } | |||
| if(this.searchKeyword){ | |||
| params.title = this.searchKeyword | |||
| } | |||
| return params | |||
| }, | |||
| handleSearch() { | |||
| // console.log('搜索:', this.searchKeyword) | |||
| this.list = [] | |||
| this.initPage() | |||
| this.getList(true) | |||
| // 这里添加搜索逻辑 | |||
| mixins: [MixinList], | |||
| components: { | |||
| BookList | |||
| }, | |||
| data() { | |||
| return { | |||
| mixinListApi: 'book.list', | |||
| // 自定义onShow | |||
| mixinListConfig: { | |||
| customOnShow: true, | |||
| }, | |||
| searchKeyword: '', | |||
| label: '', | |||
| currentTab: 0, | |||
| categoryTabs: [], | |||
| bookList: [ | |||
| switchTab(index) { | |||
| this.currentTab = index | |||
| // console.log('切换分类:', this.categoryTabs[index]) | |||
| this.list = [] | |||
| this.initPage() | |||
| this.getList(true) | |||
| // 这里添加分类切换逻辑 | |||
| ] | |||
| } | |||
| }, | |||
| goToDetail(book) { | |||
| uni.navigateTo({ | |||
| url: '/subPages/home/directory?id=' + book.id | |||
| }) | |||
| methods: { | |||
| mixinSetParams() { | |||
| const params = {} | |||
| // 只有当不是"全部"选项时才添加category参数 | |||
| if (this.categoryTabs[this.currentTab] && this.categoryTabs[this.currentTab].id) { | |||
| params.category = this.categoryTabs[this.currentTab].id | |||
| } | |||
| if (this.label) { | |||
| params.label = this.label | |||
| } | |||
| if (this.searchKeyword) { | |||
| params.title = this.searchKeyword | |||
| } | |||
| return params | |||
| }, | |||
| handleSearch() { | |||
| // console.log('搜索:', this.searchKeyword) | |||
| this.list = [] | |||
| this.initPage() | |||
| this.getList(true) | |||
| // 这里添加搜索逻辑 | |||
| }, | |||
| switchTab(e) { | |||
| this.currentTab = e.index | |||
| this.initPage() | |||
| this.getList(true) | |||
| }, | |||
| // 获取书籍分类 | |||
| async getCategory() { | |||
| const categoryRes = await this.$api.book.category() | |||
| if (categoryRes.code === 200) { | |||
| this.categoryTabs = categoryRes.result.map(item => ({ | |||
| title: item.title, | |||
| id: item.id | |||
| })) | |||
| // 在数组开头添加"全部"选项 | |||
| this.categoryTabs.unshift({ | |||
| title: '全部', | |||
| id: null | |||
| }) | |||
| } | |||
| }, | |||
| }, | |||
| // 获取书籍分类 | |||
| async getCategory() { | |||
| const categoryRes = await this.$api.book.category() | |||
| if (categoryRes.code === 200){ | |||
| this.categoryTabs = categoryRes.result.map(item => ({ | |||
| title:item.title, | |||
| id: item.id | |||
| })) | |||
| } | |||
| onLoad(options) { | |||
| if (options.label) { | |||
| this.label = options.label | |||
| } | |||
| }, | |||
| }, | |||
| onLoad(options) { | |||
| if (options.label){ | |||
| this.label = options.label | |||
| async onShow() { | |||
| await this.getCategory() | |||
| this.getList() | |||
| } | |||
| }, | |||
| async onShow() { | |||
| await this.getCategory() | |||
| this.getList() | |||
| } | |||
| } | |||
| </script> | |||
| <style scoped lang="scss"> | |||
| .search-container { | |||
| background: #fff; | |||
| min-height: 100vh; | |||
| background: #fff; | |||
| min-height: 100vh; | |||
| } | |||
| .search-header { | |||
| padding: 10rpx 32rpx 6rpx; | |||
| background: #fff; | |||
| position: sticky; | |||
| top: 0; | |||
| left: 0; | |||
| right: 0; | |||
| } | |||
| .category-tabs { | |||
| background: #fff; | |||
| // border-bottom: 1rpx solid #f0f0f0; | |||
| .tab-scroll { | |||
| white-space: nowrap; | |||
| } | |||
| .tab-list { | |||
| display: flex; | |||
| padding: 0 32rpx; | |||
| } | |||
| .tab-item { | |||
| flex-shrink: 0; | |||
| padding: 24rpx 22rpx; | |||
| font-size: 32rpx; | |||
| color: $secondary-text-color; | |||
| position: relative; | |||
| &.active { | |||
| color: $primary-text-color; | |||
| font-weight: 600; | |||
| &::after { | |||
| content: ''; | |||
| position: absolute; | |||
| bottom: 0; | |||
| left: 50%; | |||
| transform: translateX(-50%); | |||
| width: 22rpx; | |||
| height: 4rpx; | |||
| background: $primary-text-color; | |||
| border-radius: 2rpx; | |||
| } | |||
| } | |||
| } | |||
| padding: 10rpx 32rpx 6rpx; | |||
| background: #fff; | |||
| position: sticky; | |||
| top: 0; | |||
| left: 0; | |||
| right: 0; | |||
| background-color: #fff; | |||
| z-index: 9; | |||
| } | |||
| .search-results { | |||
| padding: 32rpx; | |||
| display: flex; | |||
| flex-direction: column; | |||
| gap: 32rpx; | |||
| } | |||
| .book-item { | |||
| display: flex; | |||
| align-items: center; | |||
| // border-bottom: 1rpx solid #f5f5f5; | |||
| background: #F8F8F8; | |||
| // width: 686rpx; | |||
| height: 212rpx; | |||
| gap: 16rpx; | |||
| border-radius: 16rpx; | |||
| padding: 0rpx 16rpx; | |||
| &:last-child { | |||
| border-bottom: none; | |||
| } | |||
| .book-cover { | |||
| width: 136rpx; | |||
| height: 180rpx; | |||
| border-radius: 16rpx; | |||
| overflow: hidden; | |||
| margin-right: 16rpx; | |||
| image { | |||
| width: 100%; | |||
| height: 100%; | |||
| } | |||
| } | |||
| .book-info { | |||
| flex: 1; | |||
| display: flex; | |||
| flex-direction: column; | |||
| justify-content: space-between; | |||
| } | |||
| .book-title { | |||
| font-size: 32rpx; | |||
| font-weight: 600; | |||
| color: $primary-text-color; | |||
| line-height: 48rpx; | |||
| letter-spacing: 0; | |||
| margin-bottom: 12rpx; | |||
| overflow: hidden; | |||
| text-overflow: ellipsis; | |||
| } | |||
| .book-author { | |||
| font-size: 24rpx; | |||
| color: $secondary-text-color; | |||
| margin-bottom: 16rpx; | |||
| overflow: hidden; | |||
| text-overflow: ellipsis; | |||
| white-space: nowrap; | |||
| } | |||
| .book-meta { | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: space-between; | |||
| } | |||
| .book-duration { | |||
| display: flex; | |||
| align-items: center; | |||
| font-size: 22rpx; | |||
| color: #999; | |||
| .book-icon{ | |||
| width: 18rpx; | |||
| height: 18rpx; | |||
| } | |||
| text { | |||
| margin-left: 8rpx; | |||
| } | |||
| } | |||
| .book-membership { | |||
| padding: 8rpx 16rpx; | |||
| border-radius: 8rpx; | |||
| font-size: 24rpx; | |||
| color: #211508; | |||
| } | |||
| } | |||
| .book-membership-premium { | |||
| background: #E9F1FF; | |||
| border: 2rpx solid #C4DAFF | |||
| } | |||
| .book-membership-vip { | |||
| background: #FFF4E9; | |||
| border: 2rpx solid #FFE2C4 | |||
| padding: 32rpx; | |||
| } | |||
| .book-membership-basic { | |||
| background: #FFE9E9; | |||
| border: 2rpx solid #FFDBC4 | |||
| } | |||
| </style> | |||