Browse Source

feat(文章): 新增文章详情页及相关功能

- 添加文章详情页路由配置及页面组件
- 实现文章列表获取及展示功能
- 增加音频播放悬浮按钮及播放逻辑
- 优化H5环境下开屏动画显示逻辑
- 调整首页布局,增加文章列表展示区域
hfll
hflllll 1 month ago
parent
commit
3c31771305
7 changed files with 1269 additions and 13 deletions
  1. +17
    -0
      api/modules/home.js
  2. +7
    -0
      pages.json
  3. +16
    -0
      pages/components/SplashScreen.vue
  4. +429
    -11
      pages/index/home.vue
  5. +490
    -0
      subPages/home/article.vue
  6. +308
    -0
      subPages/home/richtext.vue
  7. +2
    -2
      subPages/home/search.vue

+ 17
- 0
api/modules/home.js View File

@ -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
})
}
}

+ 7
- 0
pages.json View File

@ -197,6 +197,13 @@
// #endif
"navigationBarTitleText": "分享"
}
},
{
"path": "home/article",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "文章详情"
}
}
]
}


+ 16
- 0
pages/components/SplashScreen.vue View File

@ -85,6 +85,16 @@ export default {
// store
await this.$nextTick()
// #ifdef H5
// H5sessionStorage
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
// H5sessionStorage
sessionStorage.setItem('splash_shown', 'true')
// #endif
this.startCountdown()
} else {
console.log('沒有開屏圖片,跳過開屏動畫')


+ 429
- 11
pages/index/home.vue View File

@ -61,12 +61,77 @@
@click="onBannerClick"
></uv-swiper>
</view>
<!-- 书本列表当选择非"全部"tab时显示 -->
<view class="book-list-container" v-if="showBookList">
<view class="book-list-results">
<view
v-for="(book, index) in bookList"
:key="index"
class="book-list-item"
@click="goToBookDetail(book)"
>
<view class="book-list-cover">
<image :src="book.booksImg" mode="aspectFill"></image>
</view>
<view class="book-list-info">
<view class="book-list-title">{{ book.booksName }}</view>
<view class="book-list-author">{{ book.booksAuthor }}</view>
<view class="book-list-meta">
<view class="book-list-duration">
<image src="/static/play-icon.png" mode="aspectFill" class="book-list-icon"></image>
<text>{{ book.duration }}</text>
</view>
<view class="book-list-membership" :class="classMap[book.vipInfo.title]">
{{ book.vipInfo.title }}
</view>
</view>
</view>
</view>
<uv-loading-icon text="加载中" textSize="30rpx" v-if="isLoadingBooks"></uv-loading-icon>
<uv-empty v-else-if="bookList.length === 0"></uv-empty>
</view>
</view>
<!-- 文章列表 -->
<view class="article-section" v-if="articleList.length > 0 && !showBookList">
<view class="section-header">
<text class="section-title">精选文章 </text>
</view>
<scroll-view
show-scrollbar="false"
class="article-scroll"
scroll-x="true"
>
<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>
</scroll-view>
</view>
<!-- 根据labelBooksData动态渲染书籍区块 -->
<view
v-for="(labelData, labelIndex) in labelBooksData"
:key="labelIndex"
class="section"
v-if="!showBookList"
>
<view class="section-header" @click="goLabel(labelData.labelInfo)">
<text class="section-title">{{ labelData.labelInfo.title }}</text>
@ -77,6 +142,7 @@
</view>
<!-- 第一个label今日更新样式 -->
<!--
<scroll-view
v-if="labelIndex === 0"
show-scrollbar="false"
@ -103,11 +169,12 @@
</view>
</view>
</view>
</scroll-view>
</scroll-view> -->
<!-- 第二个label推荐书籍样式 -->
<scroll-view
v-else-if="labelIndex === 1"
v-if="labelIndex === 0"
show-scrollbar="false"
class="content-scroll"
scroll-x="true"
@ -157,7 +224,7 @@
</view>
<!-- 推荐内容列表 -->
<view class="section">
<view class="section" v-if="!showBookList">
<view class="recommend-list">
<view
@click="goPlan(item.id, item.type)"
@ -230,6 +297,17 @@ export default {
// label
labelBooksData: [],
// tab
bookList: [],
isLoadingBooks: false,
showBookList: false, //
//
classMap: {
'蕾朵会员': 'book-membership-premium',
'盛放会员': 'book-membership-vip',
'萌芽会员': 'book-membership-basic',
},
//
@ -237,6 +315,8 @@ export default {
],
//
articleList: [],
currentVideo: '',
@ -273,9 +353,11 @@ export default {
methods: {
//
detectDevice() {
let screenWidth = 0
// #ifdef H5
const userAgent = navigator.userAgent
const screenWidth = window.innerWidth || document.documentElement.clientWidth
screenWidth = window.innerWidth || document.documentElement.clientWidth
const screenHeight = window.innerHeight || document.documentElement.clientHeight
//
@ -294,7 +376,7 @@ export default {
// #ifndef H5
// H5
const systemInfo = uni.getSystemInfoSync()
const screenWidth = systemInfo.screenWidth
screenWidth = systemInfo.screenWidth
this.isTablet = screenWidth >= 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;
}
</style>

+ 490
- 0
subPages/home/article.vue View File

@ -0,0 +1,490 @@
<template>
<view class="article-detail">
<!-- 导航栏 -->
<uv-navbar
:placeholder="true"
left-icon="arrow-left"
:title="articleData.title || '文章详情'"
@leftClick="goBack"
></uv-navbar>
<!-- 文章内容 -->
<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">
<rich-text :nodes="articleData.content"></rich-text>
</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>
export default {
data() {
return {
articleId: '',
articleData: {},
loading: true,
isPlaying: false,
audioUrl: '',
hasAudio: false,
audioContext: null
}
},
onLoad(options) {
if (options.id) {
this.articleId = options.id
this.getArticleDetail()
}
},
onUnload() {
//
this.stopAudio();
},
methods: {
// HTML5 Audiouni-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;
},
//
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
//
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;
}
},
//
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) {
try {
this.audioContext.pause();
this.isPlaying = false;
} catch (error) {
console.error('暂停音频失败:', error);
}
}
},
//
stopAudio() {
if (this.audioContext) {
try {
this.audioContext.stop();
this.audioContext.destroy();
this.isPlaying = false;
this.audioContext = null;
} 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>

+ 308
- 0
subPages/home/richtext.vue View File

@ -24,6 +24,15 @@
</view>
</view>
</view>
<!-- 音频播放悬浮按钮 -->
<view v-if="audioUrl" :class="['audio-float-btn', { 'playing': isPlaying }]" @click="toggleAudio">
<uv-icon
:name="isPlaying ? 'pause-circle-fill' : 'play-circle-fill'"
size="60"
:color="isPlaying ? '#ff6b6b' : '#4CAF50'"
></uv-icon>
</view>
</view>
</template>
@ -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 Audiouni-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()
}
}
</script>
@ -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);
}
}
</style>

+ 2
- 2
subPages/home/search.vue View File

@ -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%;


Loading…
Cancel
Save