<template>
<view class="music_player recording-audio">
<image class="big-image" :src="picUrl" mode="aspectFill"></image>
<view class="big-cover"></view>
<view class="content">
<nav-bar :statusHeight="statusHeight" :navBarHeight="navBarHeight" class="navswipper">
<view class="tab">
<view :class="currentPage === 0 ? 'active': ''">音频</view>
<view class="divider">|</view>
<view :class="currentPage === 1 ? 'active': ''">字幕</view>
</view>
</nav-bar>
<swiper :style="{height: contentHeight}" @change="handleSwiperChange">
<swiper-item class="music">
<!-- old -->
<view class="album">
<image :src="picUrl" mode="widthFix"></image>
</view>
<view class="info">
<view class="title">{{songName}}</view>
<view class="subtitle">
<view class="singer">{{singer}}</view>
<view class="alias">专辑:{{albumName}}</view>
</view>
</view>
<view class="lyric">{{currentLyricInfo}}</view>
<!-- new -->
<view class="progress">
<!-- <view class="audio-number">{{format(current)}}</view> -->
<slider class="audio-slider" step="1" :activeColor="color" block-size="16" :value="current" :max="duration" @changing="[status.seeking = true,current=$event.detail.value]" @change="seek($event.detail.value)"></slider>
<!-- <view class="audio-number">{{format(duration)}}</view> -->
<view class="time">
<view class="current">{{format(current)}}</view>
<view class="duration">{{format(duration)}}</view>
</view>
</view>
<view class="operation">
<image class="btn btn-mode" :src="'/static/player/play_'+playMode[playModeIndex]+'.png'" @tap="handleModeChange"></image>
<image class="btn btn-prev" src="/static/player/play_prev.png" @click="handlePrev"></image>
<view class="audio-control audio-control-prev" @click="seek(current-5)" :style="{borderColor:color}">
<image src="./img/back.png"></image>
</view>
<view class="audio-control audio-control-switch" :class="{audioLoading:(status.playing && status.waiting)}" @click="!status.playing?play():pausePlay()">
<!-- <image :src="${(status.playing%20&&%20status.waiting)?'./img/more.png':(!status.playing?'./img/pause_icon.png':'./img/playvv_icon.png')}
" ></image> -->
<image class="btn" src="./img/more.png" v-if="(status.playing && status.waiting)"></image>
<image class="btn" src="/static/player/play_resume.png" v-else-if="!status.playing"></image>
<image class="btn" src="/static/player/play_pause.png" v-else></image>
</view>
<view class="audio-control audio-control-next" @click="seek(current+5)">
<image class="btn" src="./img/back.png" style="transform: rotate(180deg);"></image>
</view>
<image class="btn btn-next" src="/static/player/play_next.png" @tap="handleNext"></image>
<image class="btn btn-music" src="/static/player/play_music.png" ></image>
</view>
</swiper-item>
<swiper-item class="lyric">
<!-- old -->
<!-- <scroll-view :scroll-y="true" class="lyric-list" :scroll-top="lyricScrollTop" :scroll-with-animation="true">
<block v-for="(item,index) in lyricInfos" :key="index" :id="subtitle-${index}
">
<view class="item" :class="currentLyricIndex === index ? 'active':''">{{item.text}}</view>
</block>
</scroll-view> -->
<!-- new -->
<!-- <view class="text_box" v-if="showText"> -->
<!-- <input class="search_input" type="text" confirm-type="search" v-model="searchText"
placeholder="请输入要搜索的关键词" @confirm="search" placeholder-style="font-size:28rpx;color:#999999;" /> -->
<scroll-view :scroll-anchoring="true" :scroll-y="true" :scroll-into-view="textScrollTarget"
:style="{height:textPanelHeight+'px','min-height':'450rpx'}">
<view class="item" :id="'item'+index" @click="seek(item.startTime/1000)" :key="index"
v-for="(item, index) in textList"
:style="item.role === rightRoleName ? 'display:flex;justify-content: flex-end' : ''">
<view v-if="item.role === rightRoleName" style="display:flex;">
<view
:class="[item.role !== rightRoleName ? 'leftSide' : 'rightSide', textIndex === index ? 'highlight' : '']">
<view>
<text>{{ item.text }}</text>
</view>
</view>
<view class="avatar_right">{{item.role}}</view>
</view>
<view v-else style="display:flex;">
<view class="avatar_left">{{item.role}}</view>
<view :class="[item.role !== rightRoleName ? 'leftSide' : 'rightSide', textIndex === index ? 'highlight' : '']">
<view>
<text>{{ item.text }}</text>
</view>
</view>
</view>
</view>
</scroll-view>
<!-- <view class="search_result" v-if="showFoundWords">
<view class="search_result_warpper">
<button @click="prev" class="iconfont">

</button>
<view class="num">
{{ searchReulst.curRow + 1 }} / {{ searchReulst.rows.length }}
</view>
<button @click="next" class="iconfont" style="transform: rotate(180deg);">

</button>
</view>
</view> -->
<!-- </view> -->
</swiper-item>
</swiper>
</view>
</view>
</template>
<script>
import { formatTime } from '@/util/format.js'
export default {
name: "gw-audiott",
props: {
src: String, //url
startTime: { //播放起点
type: Number,
default: 0,
},
showText: { //是否显示对话
type: Boolean,
default: false
},
rightRole: { //显示在右边的对话的role 默认为第一条的role showText=true时生效
type: String,
require: false
},
list: { //文本集合 showText=true时生效
type: Array,
default () {
// 如下 属性名可以通过fields映射
// return [{
// startTime: 111,
// endTime: 222,
// role: 'kefu',
// text: '你好吗'
// },
// {
// startTime: 333,
// endTime: 444,
// role: 'kehu',
// text: '我很好'
// }]
return [];
}
},
fields: { //属性映射 showText=true时生效
type: Object,
default () {
return {
startTime: 'startTime',
endTime: 'endTime',
role: 'role',
text: 'text'
}
}
},
autoplay: { //是否自动播放
type: Boolean,
default: false
}, //是否自动播放
color: {
type: String,
default: '#169af3'
}, //主色调
debug: {
type: Boolean,
default: false
},
},
computed: {
setDuration() {
return formatTime(this.duration)
},
setCurrentTime() {
return formatTime(this.current)
}
},
data() {
return {
isIOS: '', //手机系统
audio: null,
current: 0, //当前进度(s)
duration: 0, //总时长(s)
/**逻辑状态**/
status: {
playing: false, //只有点击控制面板和播放结束可修改此状态
seeking: false, //是否处于拖动状态
waiting: false, //等待加载数据
},
textPanelHeight: 0, //对话框高度
textScrollTarget: null, //滚动到当前说话位置
textList: [], //对话信息
rightRoleName: '', //显示在右边的人
textIndex: -1, //当前高亮的文本
searchText: '',
showFoundWords: false,
searchReulst: {
curRow: -1,
rows: []
},
currentPage: 0,
picUrl: 'https://www.msnao.com/2019/03/835-200x100.jpg', //https://picsum.photos/200/100?random=101',
songName: '',
singer: '',
albumName: '',
statusHeight: 0,
navBarHeight: 44,
currentLyricInfo: '',
playModeIndex: 0,
playMode: ['order', 'random', 'repeat'],
}
},
methods: {
handleNext() {
this.$emit('handleNext')
},
handlePrev() {
this.$emit('handlePrev')
},
handleModeChange() {
if (this.playModeIndex === 2) {
this.playModeIndex = 0
} else {
this.playModeIndex++
}
this.$emit('handleModeChange', this.playModeIndex)
},
handleSwiperChange(event) {
this.currentPage = event.detail.current
},
calcTextPosition() {
if (this.status.seeking) {
return;
}
if (!this.showText) {
return;
}
if (!this.textList || this.textList.length <= 0) {
return;
}
const curTime = new Date().getTime();
if ((this.duration - this.current > 1) &&
(this.lastCalcTime && curTime - this.lastCalcTime <= 300)) {
return;
}
this.lastCalcTime = curTime;
const that = this;
const _current = parseInt((this.current * 1000).toFixed(0))
const _duration = parseInt((this.duration * 1000).toFixed(0))
const _len = this.textList.length;
let result = -1;
let nearest = -1;
if (this.textIndex > -1) {
for (let i = this.textIndex; i < _len; i++) {
const item = this.textList[i];
if (_current >= item.startTime && _current <= item.endTime) { //符合时间区
result = i;
break;
}
if (_current < item.startTime && _current < item.endTime) {
break;
}
}
}
if (result < 0) {
for (let i = 0; i < _len; i++) {
const item = this.textList[i];
if (_current >= item.startTime && _current <= item.endTime) { //符合时间区
result = i;
break;
}
if (_current <= item.startTime) {
nearest = (i > 0 ? i - 1 : i);
break;
}
}
}
if (result < 0 && nearest > -1) {
result = nearest;
}
if (result > -1 && result !== this.textIndex) { //不要改变这个代码的顺序
this.textScrollTarget = 'item' + result;
}
if (result > -1) {
this.textIndex = result;
}
this.assignItem(this.textList[result])
},
assignItem(item) {
this.currentLyricInfo = item.text
console.log(item)
},
seek(value) {
if (value <= 0) {
value = 0
}
if (value >= this.duration) {
value = this.duration;
}
this.debug && console.log('调用Seek,当前audio状态=' + this.audio.paused + ',value=' + value);
this.status.afterseek = true;
this.status.seeking = true;
this.status.playing = true;
this.current = value;
if (!this.audio.paused) { //暂停事件里调用this.audio.seek
this.audio.pause()
} else { //已经是停止状态 必须先播放后再调用this.seek
this.status.afterseek = true;
if (!this.status.waiting) {
this.play();
}
}
},
//点击播放按钮
play() {
console.log("1111111")
this.debug && console.log('调用播放,当前audio状态=' + this.audio.paused);
this.status.playing = true;
if (this.audio.paused) {
if (this.isIOS) {
this.audio.play();
} else {
this.audio.play();
//兼容
// this.audio.autoplay = true;
}
}
},
pausePlay() {
console.log("22222222")
this.debug && console.log('调用暂停,当前audio状态=' + this.audio.paused);
this.status.playing = false;
if (!this.audio.paused) {
if (this.isIOS) {
this.audio.pause();
} else {
this.audio.pause();
//兼容
//this.audio.autoplay = false;
}
}
},
prev() {
if (this.searchReulst.curRow <= 0) {
this.showFoundWords = false;
this.searchReulst = {
curRow: -1,
rows: []
}
return;
}
if (this.searchReulst.rows.length <= 0) {
this.showFoundWords = false;
this.searchReulst = {
curRow: -1,
rows: []
}
return;
}
const index = this.searchReulst.curRow - 1;
this.searchReulst.curRow = index;
this.seek(this.textList[this.searchReulst.rows[index].row].startTime / 1000);
},
next() {
if (this.searchReulst.curRow >= this.searchReulst.rows.length) {
this.showFoundWords = false;
this.searchReulst = {
curRow: -1,
rows: []
}
return;
}
if (this.searchReulst.rows.length <= 0) {
this.showFoundWords = false;
this.searchReulst = {
curRow: -1,
rows: []
}
return;
}
const index = this.searchReulst.curRow + 1;
if (index >= this.searchReulst.rows.length) {
this.showFoundWords = false;
this.searchReulst = {
curRow: -1,
rows: []
}
return;
}
this.searchReulst.curRow = index;
this.seek(this.textList[this.searchReulst.rows[index].row].startTime / 1000);
},
search() {
this.showFoundWords = false
let result = [];
this.textList.forEach(v => {
v.__FF__ = [{
value: [],
indexList: []
}];
});
if (!this.searchText) {
return (this.searchReulst = {
curRow: -1,
rows: []
});
}
this.textList.forEach((v, i) => {
if (v.text.includes(this.searchText)) {
let b = [...v.text.matchAll(new RegExp(this.searchText, 'ig'))];
b.forEach(v => {
let length = v[0].length;
let start = v.index;
v.index = [];
for (let i = start; i < start + length; i++) {
v.index.push(i);
}
});
let indexs = [];
b.map(v => v.index).forEach(v => {
v.forEach(o => indexs.push(o));
});
result.push({
row: i,
value: b,
indexList: indexs
});
}
});
if (result.length > 0) {
this.showFoundWords = true
this.searchReulst = {
curRow: -1,
rows: result
};
this.next()
} else {
this.showFoundWords = false
this.searchReulst = {
curRow: -1,
rows: []
}
}
console.log(result, '搜索结果');
this.textList.forEach((v, i) => {
result.forEach(o => {
if (i == o.row) {
v.__FF__ = o;
}
});
});
},
mappedFields() {
this.textScrollTarget = null; //滚动到当前说话位置
this.textList = []; //对话信息
this.textIndex = -1; //当前高亮的文本
this.searchText = ''
this.showFoundWords = false
this.searchReulst = {
curRow: -1,
rows: []
}
this.rightRoleName = this.rightRole;
if (!this.showText) {
return;
}
if (this.list.length <= 0) {
return;
}
if (this.rightRoleName == null) { //默认取第一条
this.rightRoleName = this.list[0][this.fields.role || 'role'] || '';
}
this.list.forEach(v => {
this.textList.push({
startTime: parseInt(v[this.fields.startTime || 'startTime'] || '0'),
endTime: parseInt(v[this.fields.endTime || 'endTime'] || '0'),
role: v[this.fields.role || 'role'] || '',
text: v[this.fields.text || 'text'] || ''
});
});
},
init() {
const res = uni.getSystemInfoSync()
this.statusHeight = res.statusBarHeight
this.contentHeight = (res.screenHeight - this.statusHeight - 90) + 'px'
if (this.audio) {
this.audio.destroy();
}
this.resetStatus();
this.audio = uni.createInnerAudioContext();
//this.audio.obeyMuteSwitch = false;
this.audio.startTime = this.startTime;
this.audio.autoplay = this.autoplay;
this.duration = 0;
this.mappedFields();
this.audio.src = this.src;
if (this.autoplay) {
this.status.playing = true;
}
//音频进度更新事件
this.audio.onTimeUpdate(() => {
console.log(this.duration, this.audio.currentTime)
if (!this.duration) {
this.duration = this.audio.duration
}
if (!this.status.seeking) {
this.current = this.audio.currentTime
this.calcTextPosition()
this.$emit('current', this.current);
}
})
//音频播放事件
this.audio.onPlay(() => {
this.debug && console.log('开始播放,当前播放器状态=' + this.audio.paused);
this.status.waiting = false;
if (this.status.seeking && this.status.afterseek) {
this.status.afterseek = false;
this.seek(this.current);
} else {
this.status.seeking = false;
this.status.afterseek = false;
}
})
//音频暂停事件
this.audio.onPause(() => {
this.debug && console.log('暂停播放,当前播放器状态=' + this.audio.paused);
//seek必须要在暂停播放后调用 否则没效果
if (this.status.seeking && this.status.playing) {
this.audio.seek(this.current);
} else {
this.status.seeking = false;
this.status.afterseek = false;
}
})
this.audio.onStop(() => {
this.debug && console.log('停止播放,当前播放器状态=' + this.audio.paused);
})
//音频等待
this.audio.onWaiting(() => {
this.debug && console.log('等待音频数据,当前播放器状态=' + this.audio.paused)
this.status.waiting = true;
if (!this.audio.paused) {
this.audio.pause()
}
})
this.audio.onCanplay(() => {
this.debug && console.log('数据准备就绪,当前播放器状态=' + this.audio.paused)
this.status.waiting = false;
if (this.status.playing && !this.status.seeking && !this.status.afterseek) {
this.play()
}
})
//音频完成更改进度事件
this.audio.onSeeked(() => {
if (this.status.seeking) { //这个事件触发可能不对 自己控制下
this.debug && console.log('Seeked,当前播放器状态=' + this.audio.paused)
this.status.seeking = false
if (this.status.playing && !this.status.waiting) {
this.play()
}
}
})
//音频结束事件
this.audio.onEnded(() => {
this.debug && console.log('播放结束,当前播放器状态=' + this.audio.paused)
this.resetStatus();
this.$emit('end');
})
this.audio.onError((err) => {
this.debug && console.log('播放出错,当前播放器状态=' + this.audio.paused)
this.debug && console.error(err)
this.pausePlay();
this.resetStatus();
this.$emit('error', err);
})
},
resetStatus() {
this.status.playing = false;
this.status.seeking = false;
this.status.waiting = false;
this.current = 0;
},
//格式化时长
format(num) {
return '0'.repeat(2 - String(Math.floor(num / 60)).length) + Math.floor(num / 60) + ':' + '0'.repeat(2 -
String(Math.floor(num % 60)).length) + Math.floor(num % 60)
},
},
created() {
const that = this;
uni.getSystemInfo({
success(res) {
that.isIOS = (res.system || '').startsWith('iOS')
}
});
this.init();
},
beforeDestroy() {
this.audio.destroy()
},
watch: {
//监听切换,重新初始化
src(src, old) {
this.init();
},
list(list, old) {
this.init();
},
},
mounted() {
//计算内容高度
if (!this.showText) {
return;
}
const that = this;
this.$nextTick(function() {
const query = uni.createSelectorQuery().in(that);
query.select('.recording-audio').boundingClientRect()
.exec(res1 => {
if (res1) {
query.select('.navswipper').boundingClientRect()
.exec(res2 => {
if (res2) {
that.textPanelHeight = res2[0].height - res2[1].height
// query.select('.search_input')
// .boundingClientRect()
// .exec(res3 => {
// if (res3) {
// that.textPanelHeight = res3[0].height - res3[1].height - res3[2].height
// }
// })
}
})
}
})
})
},
}
</script>
<style lang="less" scoped>
@font-face {
font-family: 'icon';
src: url('//at.alicdn.com/t/font_1104838_fxzimc34xw.eot');
src: url('//at.alicdn.com/t/font_1104838_fxzimc34xw.eot?#iefix') format('embedded-opentype'),
url('//at.alicdn.com/t/font_1104838_fxzimc34xw.woff2') format('woff2'),
url('//at.alicdn.com/t/font_1104838_fxzimc34xw.woff') format('woff'),
url('//at.alicdn.com/t/font_1104838_fxzimc34xw.ttf') format('truetype'),
url('//at.alicdn.com/t/font_1104838_fxzimc34xw.svg#iconfont') format('svg');
}
.recording-audio {
height: 100%;
width: 100%;
}
.operation image{
width: 60rpx;
height: 60rpx;
}
</style>
<style scoped>
@import url("music_player.css");
</style>
<style lang="less">
.item {
overflow: hidden;
display: flex;
margin: 10upx 20upx;
.leftSide,
.rightSide {
flex: 1;
}
.leftSide {
float: left;
padding: 20upx 20upx;
background-color: #0299f9;
color: #FFFFFF;
border-radius: 10upx;
}
.rightSide {
float: right;
padding: 20upx 20upx;
background-color: #f8f0d7;
color: #666666;
border-radius: 10upx;
}
.avatar_right {
width: 85upx;
height: 85upx;
background-color: #fae4b8;
color: #bf7900;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
margin-left: 20upx;
overflow: hidden;
}
.avatar_left {
width: 85upx;
height: 85upx;
background-color: #027cfe;
color: #FFFFFF;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
margin-right: 20upx;
overflow: hidden;
}
.highlight {
color: red;
}
}
</style>