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 { | 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> | <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> | ||||
| <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> | ||||
| </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> | ||||
| </view> | |||||
| </template> | </template> | ||||
| <script> | <script> | ||||
| import MixinList from '@/mixins/list.js' | import MixinList from '@/mixins/list.js' | ||||
| import BookList from '@/components/BookList.vue' | |||||
| export default { | 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> | </script> | ||||
| <style scoped lang="scss"> | <style scoped lang="scss"> | ||||
| .search-container { | .search-container { | ||||
| background: #fff; | |||||
| min-height: 100vh; | |||||
| background: #fff; | |||||
| min-height: 100vh; | |||||
| } | } | ||||
| .search-header { | .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 { | .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> | </style> | ||||