chuangte_bike_newxcx/page_shanghu/gongzuotai/yuyin/index.vue

585 lines
14 KiB
Vue
Raw Permalink Normal View History

2026-02-06 15:28:01 +08:00
<template>
<view class="page">
<u-navbar title="语音管理" :border-bottom="false" :background="bgc" back-icon-color="#262B37" title-color='#262B37'
title-size='36' height='36' id="navbar">
</u-navbar>
<scroll-view class="list-container" scroll-y @scrolltolower="loadMore">
<view class="voice-list">
<view class="voice-item" v-for="(item, index) in voiceList" :key="index">
<view class="voice-info" @click="toDetail(item.id)">
<view class="voice-meta">
<text class="meta-item">编号: {{ item.no || '-' }}</text>
<text class="meta-item" v-if="item.remark">备注: {{ item.remark }}</text>
</view>
<!-- 音频播放器 -->
<view class="audio-player" v-if="item.fileUrl" @click.stop>
<view class="play-control" @click="togglePlay(item, index)">
<u-icon
:name="item.isPlaying ? 'pause-circle' : 'play-circle'"
:color="item.isPlaying ? '#64B6A7' : '#3996FD'"
size="48"
></u-icon>
</view>
<view class="progress-container">
<view class="progress-bar">
<view class="progress-bg">
<view
class="progress-fill"
:style="{ width: (item.progress || 0) + '%' }"
></view>
</view>
</view>
<view class="time-info">
<text class="current-time">{{ formatTime(item.currentTime || 0) }}</text>
<text class="total-time">/ {{ formatTime(item.duration || 0) }}</text>
</view>
</view>
</view>
</view>
<view class="voice-actions">
<view class="config-btn" @click.stop="toConfig(item)">
语音配置
</view>
</view>
</view>
</view>
<view class="empty-tip" v-if="voiceList.length === 0 && !loading">
暂无语音数据
</view>
<view class="loading-tip" v-if="loading">
加载中...
</view>
</scroll-view>
<!-- 底部占位 -->
<view class="footer-placeholder"></view>
<!-- 新增按钮与项目原有风格一致 -->
<view class="footer-container">
<view class="add-btn-wrap" @click="toAdd">
<view class="btn-shadow"></view>
<view class="btn-content">
<u-icon name="plus" color="#ffffff" size="36" style="margin-right: 12rpx;"></u-icon>
<text>新增</text>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
bgc: {
backgroundColor: "#fff",
},
voiceList: [],
loading: false,
pageNum: 1,
pageSize: 20,
total: 0,
finished: false,
areaId: '',
audioContexts: {} // 存储每个音频的上下文
}
},
onLoad() {
this.getUserAreaThenList()
},
onShow() {
// 从新增或详情页返回时刷新列表
this.pageNum = 1
this.voiceList = []
this.finished = false
this.getUserAreaThenList()
},
onUnload() {
// 页面卸载时销毁所有音频上下文
this.destroyAllAudio()
},
methods: {
// 先取运营区 id 再拉列表
getUserAreaThenList() {
if (uni.getStorageSync('adminAreaid')) {
this.areaId = uni.getStorageSync('adminAreaid')
this.getList()
return
}
this.$u.get('/getInfo').then(res => {
if (res.code == 200 && res.user && res.user.areaId) {
this.areaId = res.user.areaId
this.getList()
} else {
uni.showToast({ title: '未获取到运营区信息', icon: 'none' })
}
}).catch(() => {
uni.showToast({ title: '获取运营区信息失败', icon: 'none' })
})
},
// 获取语音列表(带运营区 id
getList() {
if (this.loading || this.finished) return
if (!this.areaId) return
this.loading = true
this.$u.get('/bst/voice/list?areaId=' + (this.areaId || '')).then((res) => {
if (res.code == 200) {
let list = []
if (Array.isArray(res.data)) {
list = res.data
this.total = res.data.length
} else if (res.rows) {
// 分页数据
this.total = res.total || 0
if (this.pageNum == 1) {
list = res.rows || []
} else {
list = this.voiceList.concat(res.rows || [])
}
if (!res.rows || res.rows.length < this.pageSize) {
this.finished = true
}
}
// 初始化音频播放状态
this.voiceList = list.map(item => ({
...item,
isPlaying: false,
progress: 0,
currentTime: 0,
duration: 0
}))
// 预加载音频时长
this.preloadAudioDurations()
} else {
uni.showToast({
title: res.msg || '获取列表失败',
icon: 'none',
duration: 2000
})
}
}).catch(err => {
uni.showToast({
title: '请求失败',
icon: 'none',
duration: 2000
})
}).finally(() => {
this.loading = false
})
},
// 加载更多
loadMore() {
if (!this.finished && !this.loading) {
this.pageNum++
this.getList()
}
},
// 跳转详情
toDetail(id) {
uni.navigateTo({
url: `/page_shanghu/gongzuotai/yuyin/detail?id=${id}`
})
},
// 跳转新增
toAdd() {
uni.navigateTo({
url: '/page_shanghu/gongzuotai/yuyin/add'
})
},
// 跳转语音配置(阻止冒泡)
toConfig(item) {
uni.navigateTo({
url: `/page_shanghu/gongzuotai/yuyin/config?voiceId=${item.id}&voiceName=${encodeURIComponent(item.name || '')}`
})
},
// 切换播放/暂停
togglePlay(item, index) {
if (!item.fileUrl) return
// 如果当前项正在播放,则暂停
if (item.isPlaying) {
this.pauseAudio(index)
return
}
// 暂停其他正在播放的音频
this.pauseAllAudio(index)
// 播放当前音频
this.playAudio(item, index)
},
// 播放音频
playAudio(item, index) {
if (!item.fileUrl) return
// 如果已有音频上下文,直接播放
if (this.audioContexts[index]) {
this.audioContexts[index].play()
this.voiceList[index].isPlaying = true
return
}
// 创建新的音频上下文
const audioContext = uni.createInnerAudioContext()
audioContext.src = item.fileUrl
audioContext.autoplay = false
// 监听播放进度
audioContext.onTimeUpdate(() => {
if (this.voiceList[index]) {
const currentTime = audioContext.currentTime
const duration = audioContext.duration
this.voiceList[index].currentTime = currentTime
this.voiceList[index].duration = duration
if (duration > 0) {
this.voiceList[index].progress = (currentTime / duration) * 100
}
}
})
// 监听播放结束
audioContext.onEnded(() => {
if (this.voiceList[index]) {
this.voiceList[index].isPlaying = false
this.voiceList[index].progress = 0
this.voiceList[index].currentTime = 0
}
audioContext.destroy()
delete this.audioContexts[index]
})
// 监听播放错误
audioContext.onError((err) => {
console.log('播放失败', err)
if (this.voiceList[index]) {
this.voiceList[index].isPlaying = false
}
uni.showToast({
title: '播放失败',
icon: 'none',
duration: 2000
})
audioContext.destroy()
delete this.audioContexts[index]
})
// 监听可以播放
audioContext.onCanplay(() => {
if (this.voiceList[index] && audioContext.duration > 0) {
this.voiceList[index].duration = audioContext.duration
}
})
// 监听时长变化某些情况下duration可能延迟获取
audioContext.onWaiting(() => {
// 等待加载
})
audioContext.onSeeked(() => {
if (this.voiceList[index] && audioContext.duration > 0) {
this.voiceList[index].duration = audioContext.duration
}
})
// 开始播放
audioContext.play()
this.audioContexts[index] = audioContext
this.voiceList[index].isPlaying = true
},
// 暂停音频
pauseAudio(index) {
if (this.audioContexts[index]) {
this.audioContexts[index].pause()
if (this.voiceList[index]) {
this.voiceList[index].isPlaying = false
}
}
},
// 暂停所有音频
pauseAllAudio(exceptIndex) {
Object.keys(this.audioContexts).forEach(key => {
const idx = parseInt(key)
if (idx !== exceptIndex) {
this.pauseAudio(idx)
}
})
},
// 销毁所有音频
destroyAllAudio() {
Object.keys(this.audioContexts).forEach(key => {
const audioContext = this.audioContexts[key]
if (audioContext) {
audioContext.stop()
audioContext.destroy()
}
})
this.audioContexts = {}
},
// 预加载音频时长
preloadAudioDurations() {
this.voiceList.forEach((item, index) => {
if (item.fileUrl) {
// 延迟加载,避免同时创建太多音频上下文
setTimeout(() => {
if (!this.voiceList[index] || this.voiceList[index].duration > 0) {
return
}
const audioContext = uni.createInnerAudioContext()
audioContext.src = item.fileUrl
audioContext.autoplay = false
let hasGotDuration = false
// 监听可以播放时获取时长
const onCanplay = () => {
if (!hasGotDuration && this.voiceList[index] && audioContext.duration > 0) {
hasGotDuration = true
this.voiceList[index].duration = audioContext.duration
this.$forceUpdate()
// 获取到时长后销毁
setTimeout(() => {
if (audioContext) {
audioContext.destroy()
}
}, 100)
}
}
audioContext.onCanplay(onCanplay)
// 监听时长更新
audioContext.onTimeUpdate(() => {
if (!hasGotDuration && this.voiceList[index] && audioContext.duration > 0) {
hasGotDuration = true
this.voiceList[index].duration = audioContext.duration
this.$forceUpdate()
setTimeout(() => {
if (audioContext) {
audioContext.destroy()
}
}, 100)
}
})
// 监听错误
audioContext.onError(() => {
if (audioContext) {
audioContext.destroy()
}
})
// 延迟销毁,确保能获取到时长
setTimeout(() => {
if (audioContext && !hasGotDuration) {
audioContext.destroy()
}
}, 3000)
}, index * 200) // 每个音频间隔200ms加载
}
})
},
// 格式化时间
formatTime(seconds) {
if (!seconds || isNaN(seconds) || seconds === 0) return '00:00'
const mins = Math.floor(seconds / 60)
const secs = Math.floor(seconds % 60)
return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`
}
}
}
</script>
<style lang="scss">
page {
background: #F7F7F7;
}
.page {
width: 100%;
min-height: 100vh;
display: flex;
flex-direction: column;
}
.list-container {
flex: 1;
padding-bottom: 180rpx;
}
.voice-list {
.voice-item {
background: #fff;
border-radius: 20rpx;
padding: 20rpx;
margin-bottom: 24rpx;
width: 712rpx;
margin: auto;
margin-top: 20rpx;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
position: relative;
overflow: hidden;
// &::before {
// content: '';
// position: absolute;
// left: 0;
// top: 24rpx;
// bottom: 24rpx;
// width: 6rpx;
// background: linear-gradient(180deg, #3996FD 0%, #64B6A7 100%);
// border-radius: 3rpx;
// }
}
.voice-info {
flex: 1;
padding-left: 12rpx;
}
.voice-meta {
display: flex;
flex-direction: column;
gap: 8rpx;
margin-bottom: 16rpx;
}
.meta-item {
font-size: 28rpx;
color: #333;
}
.audio-player {
margin-top: 20rpx;
display: flex;
align-items: center;
gap: 20rpx;
}
.play-control {
flex-shrink: 0;
}
.progress-container {
flex: 1;
display: flex;
flex-direction: column;
gap: 8rpx;
}
.progress-bar {
width: 100%;
}
.progress-bg {
width: 100%;
height: 6rpx;
background: #E5E7EB;
border-radius: 3rpx;
overflow: hidden;
position: relative;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #3996FD 0%, #64B6A7 100%);
border-radius: 3rpx;
transition: width 0.1s linear;
}
.time-info {
display: flex;
justify-content: space-between;
font-size: 22rpx;
color: #999;
}
.current-time,
.total-time {
font-size: 22rpx;
color: #999;
}
.voice-actions {
margin-left: 20rpx;
}
.config-btn {
padding: 14rpx 28rpx;
background: linear-gradient(135deg, #3996FD 0%, #5CB8FF 100%);
color: #fff;
border-radius: 12rpx;
font-size: 26rpx;
font-weight: 500;
box-shadow: 0 4rpx 12rpx rgba(57, 150, 253, 0.25);
}
}
.empty-tip,
.loading-tip {
text-align: center;
padding: 40rpx;
color: #999;
font-size: 28rpx;
}
.footer-placeholder {
height: 160rpx;
}
.footer-container {
position: fixed;
bottom: 60rpx;
left: 50%;
right: 0;
display: flex;
justify-content: center;
z-index: 100;
pointer-events: none;
width: 710rpx;
transform: translateX(-50%);
.add-btn-wrap {
pointer-events: auto;
position: relative;
width: 600rpx;
height: 100rpx;
.btn-content {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(135deg, #4297F3 0%, #2B76E5 100%);
border-radius: 100rpx;
display: flex;
align-items: center;
justify-content: center;
color: #FFFFFF;
font-size: 32rpx;
font-weight: bold;
z-index: 2;
box-shadow: 0 10rpx 20rpx rgba(66, 151, 243, 0.3);
}
.btn-shadow {
position: absolute;
top: 20rpx;
left: 10%;
width: 80%;
height: 100%;
background: #4297F3;
filter: blur(20rpx);
opacity: 0.4;
border-radius: 100rpx;
z-index: 1;
}
}
}
</style>