整体效果
前言
本篇我们将实现Mini播放器。应该是最后一篇。整篇下来其实没什么技术难点,技术太浅且毫无新意。赶紧结束掉吧。React Native开发实在。就像上篇所说,快速且无成本切换框架是我们使用JSX的理由,同时扩展我们的思路、仅此而已。
实现功能
完整Mini和全屏播放器功能。如: 特效、拖动、播放/暂停、歌词解析滚动后面补上。
- Vant3组件使用。例: Popup、Slider、Circle。
- Vue3特性使用。例: Vuex、reactive、refs、computed
- JSX基本操作
前菜: 掌握JSX基础语法
- 替换v-model语法
// template
<van-circle v-model:current-rate="currentRate" />
// jsx
<van-circle v-model={[state.currentRate, 'current-rate']} />
- 替换v-for语法
/* key尽量使用item.id。迫不得已使用index */
const list = [1,2,3]
<li v-for="item in [1,2,3]" :key="item">{{item}}</li>
// jsx
{
list.map(item => (
<li key={item}>{item}</li>
))
}
- 替换v-if跟v-else语法
const flag = true;
/* v-if */
<div v-if="flag" />
// jsx
{ flag && <div /> }
/* v-else */
<div v-if="flag" />
<div v-else />
// jsx
{
flag ? <div>真</div> : <div>假</div>
}
- 替换@click等事件
<div @click="handleClick" />
<div @change="handleChange">
// jsx
<div onClick={handleClick} />
<div onChange={handleChange}>
- 替换插槽
<van-nav-bar title="标题" left-text="返回" left-arrow>
<template #right>
<van-icon name="search" size="18" />
</template>
</van-nav-bar>
// jsx
<van-nav-bar title="标题" left-text="返回" left-arrow
v-slots={{ 'left': () => <van-icon name="search" size="18" /> }}
/>
第一部: Mini播放器的变量和事件
效果图:
- 变量和事件代码(setup)
import { reactive, ref, onMounted, computed } from 'vue'
// 使用vuex
import { useStore } from 'vuex';
const Player = ({
// setup没有this
setup() {
// 获得audio refs
const audioRef = ref(null);
// 使用vuex
const store = useStore();
// 实时监控isMusicPlay状态
const isMusicPlay = computed(() => {
return store.state.isMusicPlay
});
// 本地状态
const state = reactive({
rate: 0, // 播放进度
timer: null, // 计时器
allSec: '00:00', // 总时长
curSec: '00:00', // 正在时长
fullScreenShow: false, // 全屏
})
/* 基础事件 */
// 处理播放/暂停点击
const handlePlayClick = () => {
// vuex更改状态
store.commit('handleChangeIsMusicPlay', !isMusicPlay.value);
isMusicPlay.value ? audioRef.value.play() : audioRef.value.pause() ;
audioListener();
state.allSec = secondIntoMin(state.audioDuration);
};
// 秒转分秒
const secondIntoMin = (SECONDS) => {
let allMin = Math.floor(SECONDS / 60);
let allSec = Math.floor(SECONDS) - allMin * 60;
allMin = allMin >= 10 ? allMin : '0' + allMin ;
allSec = allSec >= 10 ? allSec : '0' + allSec;
return `${allMin}:${allSec}`
};
// 监听音乐播放
const audioListener = () => {
if (isMusicPlay.value) {
state.timer = setInterval(() => {
try {
state.currentTime = audioRef.value.currentTime;
const rate = parseInt(audioRef.value.currentTime / audioRef.value.duration * 100);
state.rate = rate;
state.curSec = secondIntoMin(state.currentTime);
if (rate === 100) {
// 播放完毕复原状态、等待下次播放
clearInterval(state.timer);
state.rate = 0;
state.curSec = '00:00';
store.commit('handleChangeIsMusicPlay', false);
}
} catch (e) {
clearInterval(state.timer);
}
}, 1000)
} else {
clearInterval(state.timer);
}
};
// 生命周期
onMounted(() => {
// 获得audio元素
audioRef.value.load();
audioRef.value.oncanplay = () => {
state.audioDuration = audioRef.value.duration;
};
});
// 最终的渲染
return () => (
{renderMiniPlay()}
{renderFullScreenPlayer()}
<audio id="audio" src={testMp3} ref={audioRef}/>
// 第二步代码内容
)
}
})
第二步: Mini音乐播放器渲染(Render)
-
渲染代码(render)
// 渲染圆形进度条, 使用Circle组件。其中v-model:current-rate转jsx需要注意语法 const renderCircle = () => { // 图标插槽。通过播放/暂停状态切换图标 const renderWhichPlayStatus = isMusicPlay.value ? <van-icon onClick={handlePlayClick} color='#d44439' name="pause" size="22"/> : <van-icon onClick={handlePlayClick} color='#d44439' name="play" size="22"/>; // 逻辑处理完再return return ( <van-circle v-model={[state.currentRate, 'current-rate']} rate={state.rate} layer-color="#ebedf0" size="32" color="#d44439" stroke-width="60" v-slots={{ 'default': () => renderWhichPlayStatus }} /> ) }; // 渲染Mini播放器 const renderMiniPlay = () => ( <div className='miniPlay'> // CD图片: 点击切换全屏播放器 <div className="icon" onClick={() => state.fullScreenShow = true}> <div className="imgWrapper"> <img className={ isMusicPlay.value ? 'play' : 'play pause'} src="http://p4.music.126.net/FJWZe1aQV2-iuYeq8gUR5A==/19022650672277889.jpg" width="40" height="40" alt="img"/> </div> </div> // 文字说明 <div className="text"> <div className="name">Out of Love</div> <div className="desc">Peter Manos</div> </div> <div className='console'> // 进度条: 点击图标播放/暂停 <div> {renderCircle()} </div> // 控制器: 专辑弹窗 <div> <van-icon name="wap-nav" size="29" color="#d44439" onClick={() => { state.actionsSheetShow = true }}/> </div> </div> </div> );
第三步: 样式
// 公共变量
$bgColor: #f2f3f4;
$fontColor: #2E3030;
// 增加旋转效果
@keyframes rotate {
0% {
transform: rotate(0);
}
100%{
transform: rotate(360deg);
}
}
.miniPlay {
display: flex;
align-items: center;
width: 100%;
height: 60px;
background: #fff;
padding: 0 10px 0 20px;
justify-content: space-between;
.console{
display: flex;
align-items: center;
div:first-child{
margin-right: 6px;
}
}
.icon {
width: 40px;
height: 40px;
flex-shrink: 0;
img{
border-radius: 50%;
&.play {
animation: rotate 10s ease-in infinite;
}
&.pause {
animation-play-state: paused;
}
}
}
.text{
margin-left: 10px;
display: flex;
flex-direction: column;
justify-content: center;
flex: 1;
line-height: 20px;
overflow: hidden;
.name{
margin-bottom: 2px;
font-size: 14px;
color: #2E3030;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.desc {
font-size: 12px;
color: #bba8a8;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
}
.van-circle{
display: flex;
justify-content: center;
align-items: center;
}
}