diff --git a/api/modules/home.js b/api/modules/home.js index 0ceec34..4f9d4b1 100644 --- a/api/modules/home.js +++ b/api/modules/home.js @@ -35,5 +35,22 @@ export default{ url: '/index/banner', method: 'GET', }) + }, + + // 查询文章列表 + async getArticle() { + return http({ + url: '/index/articleList', + method: 'GET', + }) + }, + + // 查询文章详情 + async getArticleDetail(data) { + return http({ + url: '/index/articleDetail', + method: 'GET', + data + }) } } \ No newline at end of file diff --git a/pages.json b/pages.json index 24f6b74..0d71e28 100644 --- a/pages.json +++ b/pages.json @@ -197,6 +197,13 @@ // #endif "navigationBarTitleText": "分享" } + }, + { + "path": "home/article", + "style": { + "navigationStyle": "custom", + "navigationBarTitleText": "文章详情" + } } ] } diff --git a/pages/components/SplashScreen.vue b/pages/components/SplashScreen.vue index a5b6c78..6483944 100644 --- a/pages/components/SplashScreen.vue +++ b/pages/components/SplashScreen.vue @@ -85,6 +85,16 @@ export default { // 等待一小段時間確保 store 數據加載完成 await this.$nextTick() + // #ifdef H5 + // H5环境下检查sessionStorage,判断是否在当前会话中已经显示过开屏动画 + const hasShownSplash = sessionStorage.getItem('splash_shown') + if (hasShownSplash) { + console.log('当前会话已显示过开屏动画,跳过') + this.$emit('close') + return + } + // #endif + // 獲取圖片URL,優先使用配置,然後使用本地存儲 let imageUrl = '' @@ -105,6 +115,12 @@ export default { this.splashContent = imageUrl this.countdown = this.duration this.showSplash = true + + // #ifdef H5 + // H5环境下设置sessionStorage标记,表示当前会话已显示过开屏动画 + sessionStorage.setItem('splash_shown', 'true') + // #endif + this.startCountdown() } else { console.log('沒有開屏圖片,跳過開屏動畫') diff --git a/pages/index/home.vue b/pages/index/home.vue index 6f40f53..07202fb 100644 --- a/pages/index/home.vue +++ b/pages/index/home.vue @@ -61,12 +61,77 @@ @click="onBannerClick" > - + + + + + + + + + + {{ book.booksName }} + {{ book.booksAuthor }} + + + + {{ book.duration }} + + + {{ book.vipInfo.title }} + + + + + + + + + + + + + 精选文章 + + + + + + + {{ article.title }} + + + + + + + + + + {{ labelData.labelInfo.title }} @@ -77,6 +142,7 @@ + + - + = 768 // #endif }, @@ -307,7 +389,15 @@ export default { // 切换Tab async switchTab(index) { this.activeTab = index - await this.getBooksByLabels() + + if (index === 0) { + // 第一个tab(全部)显示原有内容 + this.showBookList = false + await this.getBooksByLabels() + } else { + // 其他tab显示书本列表 + await this.getBookList() + } }, // 轮播图点击事件 @@ -382,14 +472,32 @@ export default { })) } }, + + // 获取文章列表 + async getArticleList() { + try { + const articleRes = await this.$api.home.getArticle() + if (articleRes.code === 200) { + this.articleList = articleRes.result || [] + console.log('文章列表数据:', this.articleList) + } + } catch (error) { + console.error('获取文章列表失败:', error) + this.articleList = [] + } + }, // 获取书籍分类 async getCategory() { const categoryRes = await this.$api.book.category() if (categoryRes.code === 200){ - this.tabs = categoryRes.result.map(item => ({ - title:item.title, + // 硬编码第一个tab为"全部" + this.tabs = [ + { title: '全部', id: null }, + ...categoryRes.result.map(item => ({ + title: item.title, id: item.id - })) + })) + ] } }, // 获取书籍标签 @@ -453,6 +561,104 @@ export default { }) }, + // 获取书本列表(用于tab切换) + async getBookList() { + if (this.activeTab === 0) { + // 第一个tab(全部)不显示书本列表,显示原有内容 + this.showBookList = false + return + } + + this.isLoadingBooks = true + this.showBookList = true + + try { + const params = { + category: this.tabs[this.activeTab].id, + pageNo: 1, + pageSize: 20 + } + + const res = await this.$api.book.list(params) + if (res.code === 200) { + this.bookList = res.result.records || [] + } else { + this.bookList = [] + console.error('获取书本列表失败:', res) + } + } catch (error) { + console.error('获取书本列表出错:', error) + this.bookList = [] + } finally { + this.isLoadingBooks = false + } + }, + + // 跳转到书本详情(从搜索页面复制) + goToBookDetail(book) { + uni.navigateTo({ + url: '/subPages/home/directory?id=' + book.id + }) + }, + + // 跳转文章详情 + goArticleDetail(article) { + console.log('点击文章:', article) + uni.navigateTo({ + url: `/subPages/home/article?id=${article.id}` + }) + }, + + // 跳转更多文章页面 + goMoreArticles() { + uni.navigateTo({ + url: '/subPages/home/articleList' + }) + }, + + // 格式化时间 + formatTime(timeStr) { + if (!timeStr) return '' + + try { + // 处理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 '' + } + const now = new Date() + const diff = now - date + + // 小于1分钟 + if (diff < 60000) { + return '刚刚' + } + // 小于1小时 + if (diff < 3600000) { + return Math.floor(diff / 60000) + '分钟前' + } + // 小于1天 + if (diff < 86400000) { + return Math.floor(diff / 3600000) + '小时前' + } + // 小于7天 + if (diff < 604800000) { + return Math.floor(diff / 86400000) + '天前' + } + + // 超过7天显示具体日期 + const month = date.getMonth() + 1 + const day = date.getDate() + return `${month}月${day}日` + } catch (error) { + return '' + } + }, + // 关闭视频弹窗 closeVideoModal() { this.$refs.videoModal.close() @@ -485,7 +691,7 @@ export default { this.detectDevice() // 先获取基础数据 - await Promise.all([this.getBanner(), this.getSignup(), this.getCategory(), this.getLabel()]) + await Promise.all([this.getBanner(), this.getSignup(), this.getCategory(), this.getLabel(), this.getArticleList()]) // 根据label数据获取对应的书籍 await this.getBooksByLabels() @@ -614,7 +820,31 @@ export default { } } +.section-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 30rpx ; + margin-bottom: 24rpx; + .section-title { + font-size: 36rpx; + // font-weight: 600; + color: $primary-text-color; + } + + .section-more { + display: flex; + align-items: center; + gap: 4rpx; + + text { + font-size: 24rpx; + color: $secondary-text-color; + } + } +} // 内容区块 + .section { margin-top: 40rpx; @@ -933,4 +1163,192 @@ export default { } } } + +// 文章列表样式 +.article-section { + margin-top: 40rpx; + + .article-scroll { + white-space: nowrap; + } + + .article-list { + display: flex; + padding: 0 30rpx; + 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; +} diff --git a/subPages/home/article.vue b/subPages/home/article.vue new file mode 100644 index 0000000..2857476 --- /dev/null +++ b/subPages/home/article.vue @@ -0,0 +1,490 @@ + + + + + \ No newline at end of file diff --git a/subPages/home/richtext.vue b/subPages/home/richtext.vue index 5fc82b1..f43344a 100644 --- a/subPages/home/richtext.vue +++ b/subPages/home/richtext.vue @@ -24,6 +24,15 @@ + + + + + @@ -32,6 +41,10 @@ export default { data() { return { htmlContent: '', + articleDetail: null, // 文章详情数据 + audioUrl: '', // 音频URL + isPlaying: false, // 音频播放状态 + audioContext: null, // 音频上下文 // 富文本样式配置 tagStyle: { p: 'margin: 16rpx 0; line-height: 1.6; color: #333;', @@ -55,6 +68,9 @@ export default { // 获取传递的富文本内容 if (options.content) { this.htmlContent = decodeURIComponent(options.content) + } else if(options.articleId) { + // 从数据库获取文章内容 + this.getArticleContent(options.articleId) } else { uni.showToast({ title: '内容加载失败', @@ -72,7 +88,259 @@ export default { uni.navigateBack({ delta: 1 }) + }, + + // 获取文章详情 + async getArticleContent(articleId) { + try { + const res = await this.$api.home.getArticleDetail({ id: articleId }) + if (res.code === 200 && res.result) { + this.articleDetail = res.result + this.htmlContent = res.result.content + + // 获取第一个音频URL + if (res.result.audios) { + const audioKeys = Object.keys(res.result.audios) + if (audioKeys.length > 0) { + this.audioUrl = res.result.audios[audioKeys[0]] + } + } + } else { + uni.showToast({ + title: '文章加载失败', + icon: 'error' + }) + setTimeout(() => { + this.goBack() + }, 1500) + } + } catch (error) { + console.error('获取文章详情失败:', error) + uni.showToast({ + title: '网络错误', + icon: 'error' + }) + setTimeout(() => { + this.goBack() + }, 1500) + } + }, + + // 创建HTML5 Audio实例并包装为uni-app兼容接口 + createHTML5Audio() { + const audio = new Audio(); + + // 包装为uni-app兼容的接口 + const wrappedAudio = { + // 原生HTML5 Audio实例 + _nativeAudio: audio, + + // 基本属性 + get src() { return audio.src; }, + set src(value) { audio.src = value; }, + + get duration() { return audio.duration || 0; }, + get currentTime() { return audio.currentTime || 0; }, + + get paused() { return audio.paused; }, + + // 支持倍速的关键属性 + get playbackRate() { return audio.playbackRate; }, + set playbackRate(value) { + try { + audio.playbackRate = value; + } catch (error) { + console.error('HTML5 Audio倍速设置失败:', error); + } + }, + + // 基本方法 + play() { + return audio.play().catch(error => { + console.error('HTML5 Audio播放失败:', error); + }); + }, + + pause() { + audio.pause(); + }, + + stop() { + audio.pause(); + audio.currentTime = 0; + }, + + seek(time) { + audio.currentTime = time; + }, + + destroy() { + audio.pause(); + audio.src = ''; + audio.load(); + }, + + // 事件绑定方法 + onCanplay(callback) { + audio.addEventListener('canplay', callback); + }, + + onPlay(callback) { + audio.addEventListener('play', callback); + }, + + onPause(callback) { + audio.addEventListener('pause', callback); + }, + + onEnded(callback) { + audio.addEventListener('ended', callback); + }, + + onTimeUpdate(callback) { + audio.addEventListener('timeupdate', callback); + }, + + onError(callback) { + // 包装错误事件,过滤掉非关键错误 + const wrappedCallback = (error) => { + // 只在有src且音频正在播放时才传递错误事件 + if (audio.src && audio.src.trim() !== '' && !audio.paused) { + callback(error); + } else { + console.log('HTML5 Audio错误(已忽略):', { + hasSrc: !!audio.src, + paused: audio.paused, + errorType: error.type || 'unknown' + }); + } + }; + audio.addEventListener('error', wrappedCallback); + }, + + // 移除事件监听 + offCanplay(callback) { + audio.removeEventListener('canplay', callback); + }, + + offPlay(callback) { + audio.removeEventListener('play', callback); + }, + + offPause(callback) { + audio.removeEventListener('pause', callback); + }, + + offEnded(callback) { + audio.removeEventListener('ended', callback); + }, + + offTimeUpdate(callback) { + audio.removeEventListener('timeupdate', callback); + }, + + offError(callback) { + audio.removeEventListener('error', callback); + } + }; + + return wrappedAudio; + }, + + // 切换音频播放状态 + toggleAudio() { + if (!this.audioUrl) { + uni.showToast({ + title: '暂无音频', + icon: 'none' + }) + return + } + + if (this.isPlaying) { + this.pauseAudio() + } else { + this.playAudio() + } + }, + + // 播放音频 + playAudio() { + if (!this.audioUrl) { + console.error('音频URL为空'); + return; + } + + try { + // 销毁旧的音频实例 + if (this.audioContext) { + this.audioContext.destroy(); + this.audioContext = null; + } + + // 平台判断:H5环境使用createHTML5Audio,其他环境使用uni.createInnerAudioContext + if (uni.getSystemInfoSync().platform === 'devtools' || process.env.NODE_ENV === 'development' || typeof window !== 'undefined') { + // H5环境:使用包装的HTML5 Audio + this.audioContext = this.createHTML5Audio(); + } else { + // 非H5环境(小程序等) + this.audioContext = uni.createInnerAudioContext(); + } + + // 设置音频源 + this.audioContext.src = this.audioUrl; + + // 绑定事件监听 + this.audioContext.onPlay(() => { + this.isPlaying = true; + }); + + this.audioContext.onPause(() => { + this.isPlaying = false; + }); + + this.audioContext.onEnded(() => { + this.isPlaying = false; + }); + + this.audioContext.onError((error) => { + console.error('音频播放错误:', error); + this.isPlaying = false; + }); + + // 开始播放 + this.audioContext.play(); + } catch (error) { + console.error('音频播放异常:', error); + this.isPlaying = false; + } + }, + + // 暂停音频 + pauseAudio() { + if (this.audioContext) { + this.audioContext.pause() + } + }, + + // 停止音频 + stopAudio() { + if (this.audioContext) { + try { + this.audioContext.stop(); + this.audioContext.destroy(); + this.isPlaying = false; + this.audioContext = null; + } catch (error) { + console.error('停止音频失败:', error); + } + } } + }, + + // 页面卸载时清理音频资源 + onUnload() { + this.stopAudio() } } @@ -179,4 +447,44 @@ export default { } } } + +// 音频播放悬浮按钮样式 +.audio-float-btn { + position: fixed; + right: 60rpx; + bottom: 120rpx; + width: 120rpx; + height: 120rpx; + background: rgba(255, 255, 255, 0.95); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.15); + backdrop-filter: blur(10rpx); + z-index: 999; + transition: all 0.3s ease; + + &:active { + transform: scale(0.95); + box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.2); + } + + // 添加呼吸动画效果 + &.playing { + animation: pulse 2s infinite; + } +} + +@keyframes pulse { + 0% { + box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.15), 0 0 0 0 rgba(76, 175, 80, 0.4); + } + 70% { + box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.15), 0 0 0 20rpx rgba(76, 175, 80, 0); + } + 100% { + box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.15), 0 0 0 0 rgba(76, 175, 80, 0); + } +} \ No newline at end of file diff --git a/subPages/home/search.vue b/subPages/home/search.vue index 3bd3916..2744d85 100644 --- a/subPages/home/search.vue +++ b/subPages/home/search.vue @@ -169,7 +169,7 @@ export default { top: 0; left: 0; right: 0; - + z-index: 999; } .category-tabs { background: #fff; @@ -239,7 +239,7 @@ export default { border-radius: 16rpx; overflow: hidden; margin-right: 16rpx; - + z-index: 1; image { width: 100%; height: 100%;