chuangte_bike_newxcx/page_shanghu/gongzuotai/yuyin/index.vue
2026-02-06 15:28:01 +08:00

585 lines
14 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>