|
@@ -0,0 +1,266 @@
|
|
|
+import {
|
|
|
+ computed,
|
|
|
+ defineComponent,
|
|
|
+ reactive,
|
|
|
+ watch,
|
|
|
+ ref,
|
|
|
+ PropType,
|
|
|
+ onMounted
|
|
|
+} from 'vue';
|
|
|
+import styles from './index.module.less';
|
|
|
+import { Cell, Icon, Image, Popup, Slider } from 'vant';
|
|
|
+import musicBg from '../../images/music_bg.png';
|
|
|
+import iconMenu from '../../images/icon-menu.png';
|
|
|
+import iconPause from '../../images/icon-pause.png';
|
|
|
+import iconPlay from '../../images/icon-play.png';
|
|
|
+import songPrev from '../../images/song-prev.png';
|
|
|
+import songDown from '../../images/song-down.png';
|
|
|
+import songPause from '../../images/song-pause.png';
|
|
|
+import songPlay from '../../images/song-play.png';
|
|
|
+import { getSecondRPM } from '@/helpers/utils';
|
|
|
+import { nextTick } from 'process';
|
|
|
+import { usePageVisibility } from '@vant/use';
|
|
|
+
|
|
|
+export default defineComponent({
|
|
|
+ name: 'audio-player',
|
|
|
+ props: {
|
|
|
+ musicList: {
|
|
|
+ type: Array as PropType<any>,
|
|
|
+ default: () => []
|
|
|
+ }
|
|
|
+ },
|
|
|
+ setup(props) {
|
|
|
+ const pageVisibility = usePageVisibility();
|
|
|
+ const state = reactive({
|
|
|
+ songPopup: false,
|
|
|
+ playState: 'pause',
|
|
|
+ audioObj: {} as any,
|
|
|
+ listActive: 0,
|
|
|
+ dragStatus: false // 是否开始拖动
|
|
|
+ });
|
|
|
+ let timer = null as any;
|
|
|
+ const audioData = reactive({
|
|
|
+ isFirst: true,
|
|
|
+ duration: 0.1,
|
|
|
+ currentTime: 0
|
|
|
+ });
|
|
|
+ const audioRef = ref();
|
|
|
+ /** 加载成功 */
|
|
|
+ const onLoadedmetadata = () => {
|
|
|
+ audioData.duration = audioRef.value.duration;
|
|
|
+ if (audioData.isFirst) {
|
|
|
+ audioData.isFirst = false;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (state.playState === 'play') {
|
|
|
+ audioRef.value.play();
|
|
|
+ }
|
|
|
+ };
|
|
|
+ /** 改变时间 */
|
|
|
+ const handleChangeTime = (val: number) => {
|
|
|
+ audioRef.value.pause();
|
|
|
+ audioData.currentTime = val;
|
|
|
+ clearTimeout(timer);
|
|
|
+ timer = setTimeout(() => {
|
|
|
+ audioRef.value.currentTime = val;
|
|
|
+ if (state.playState === 'play') {
|
|
|
+ audioRef.value.play();
|
|
|
+ }
|
|
|
+ timer = null;
|
|
|
+ }, 300);
|
|
|
+ };
|
|
|
+ const time = computed(() => {
|
|
|
+ return `${getSecondRPM(audioData.currentTime)} / ${getSecondRPM(
|
|
|
+ audioData.duration
|
|
|
+ )}`;
|
|
|
+ });
|
|
|
+
|
|
|
+ /** 播放曲目 */
|
|
|
+ const handlePlay = (item: any) => {
|
|
|
+ const index = props.musicList.findIndex(
|
|
|
+ (_item: any) => _item.id === item.id
|
|
|
+ );
|
|
|
+ if (index > -1) {
|
|
|
+ if (state.listActive === index) {
|
|
|
+ state.playState = state.playState === 'play' ? 'pause' : 'play';
|
|
|
+ } else {
|
|
|
+ state.playState = 'play';
|
|
|
+ }
|
|
|
+ state.listActive = index;
|
|
|
+ state.audioObj = props.musicList[index];
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ /** 音频控制 */
|
|
|
+ const handleChangeAudio = (type: 'play' | 'pause' | 'pre' | 'next') => {
|
|
|
+ if (type === 'play') {
|
|
|
+ state.playState = 'play';
|
|
|
+ } else if (type === 'pause') {
|
|
|
+ state.playState = 'pause';
|
|
|
+ } else if (type === 'pre') {
|
|
|
+ if (props.musicList[state.listActive - 1]) {
|
|
|
+ handlePlay(props.musicList[state.listActive - 1]);
|
|
|
+ }
|
|
|
+ } else if (type === 'next') {
|
|
|
+ if (props.musicList[state.listActive + 1]) {
|
|
|
+ handlePlay(props.musicList[state.listActive + 1]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ watch(
|
|
|
+ () => pageVisibility.value,
|
|
|
+ val => {
|
|
|
+ if (val === 'hidden') {
|
|
|
+ audioRef.value.pause();
|
|
|
+ state.playState = 'pause';
|
|
|
+ }
|
|
|
+ }
|
|
|
+ );
|
|
|
+ watch(
|
|
|
+ () => state.playState,
|
|
|
+ val => {
|
|
|
+ if (val === 'play') {
|
|
|
+ audioRef.value.play();
|
|
|
+ } else {
|
|
|
+ audioRef.value.pause();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ );
|
|
|
+
|
|
|
+ onMounted(() => {
|
|
|
+ nextTick(() => {
|
|
|
+ const musicList = props.musicList;
|
|
|
+ if (musicList.length > 0) {
|
|
|
+ state.audioObj = musicList[0];
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+ return () => (
|
|
|
+ <div class={styles.audioPlayer}>
|
|
|
+ <div class={styles.playerHeader}>
|
|
|
+ <div class={styles.musicInfo}>
|
|
|
+ <div
|
|
|
+ class={[
|
|
|
+ styles.cover,
|
|
|
+ state.playState === 'pause' && styles.imgRotate
|
|
|
+ ]}>
|
|
|
+ <Image src={musicBg} class={styles.musicBg} />
|
|
|
+ </div>
|
|
|
+ <div class={styles.musicName}>{state.audioObj?.name || '--'}</div>
|
|
|
+ </div>
|
|
|
+ <div class={styles.controls}>
|
|
|
+ <Icon
|
|
|
+ name={songPrev}
|
|
|
+ class={styles.icon}
|
|
|
+ onClick={() => {
|
|
|
+ handleChangeAudio('pre');
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ <Icon
|
|
|
+ name={state.playState === 'play' ? iconPlay : iconPause}
|
|
|
+ class={[styles.icon, styles.iconPlay]}
|
|
|
+ onClick={() => {
|
|
|
+ if (state.playState === 'pause') {
|
|
|
+ handleChangeAudio('play');
|
|
|
+ } else {
|
|
|
+ handleChangeAudio('pause');
|
|
|
+ }
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ <Icon
|
|
|
+ name={songDown}
|
|
|
+ class={styles.icon}
|
|
|
+ onClick={() => {
|
|
|
+ handleChangeAudio('next');
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ <Icon
|
|
|
+ name={iconMenu}
|
|
|
+ class={[styles.icon, styles.iconMenu]}
|
|
|
+ onClick={() => {
|
|
|
+ state.songPopup = true;
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class={styles.playerFooter}>
|
|
|
+ <Slider
|
|
|
+ step={0.01}
|
|
|
+ class={styles.timeProgress}
|
|
|
+ v-model={audioData.currentTime}
|
|
|
+ max={audioData.duration}
|
|
|
+ onUpdate:modelValue={val => {
|
|
|
+ handleChangeTime(val);
|
|
|
+ }}
|
|
|
+ onDragStart={() => {
|
|
|
+ state.dragStatus = true;
|
|
|
+ }}
|
|
|
+ onDragEnd={() => {
|
|
|
+ state.dragStatus = false;
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ <div class={styles.playerTimer}>{time.value}</div>
|
|
|
+
|
|
|
+ <audio
|
|
|
+ ref={audioRef}
|
|
|
+ src={state.audioObj.url}
|
|
|
+ onLoadedmetadata={onLoadedmetadata}
|
|
|
+ onEnded={() => {
|
|
|
+ handleChangeAudio('pause');
|
|
|
+ }}
|
|
|
+ onTimeupdate={() => {
|
|
|
+ if (timer) return;
|
|
|
+ // 开始拖动时也不能更新
|
|
|
+ if (state.dragStatus) return;
|
|
|
+ audioData.currentTime = audioRef.value?.currentTime;
|
|
|
+ }}></audio>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <Popup
|
|
|
+ v-model:show={state.songPopup}
|
|
|
+ round
|
|
|
+ position="bottom"
|
|
|
+ class={styles.songPopup}>
|
|
|
+ <div class={styles.songContainer}>
|
|
|
+ <div class={styles.songTitle}>代表作</div>
|
|
|
+
|
|
|
+ <div class={styles.songCellGroup}>
|
|
|
+ {props.musicList.map((item: any, index: number) => (
|
|
|
+ <Cell
|
|
|
+ border={false}
|
|
|
+ class={[
|
|
|
+ styles.songCell,
|
|
|
+ index === state.listActive && styles.active
|
|
|
+ ]}
|
|
|
+ center
|
|
|
+ onClick={() => {
|
|
|
+ if (index === state.listActive) return;
|
|
|
+ state.audioObj = props.musicList[index];
|
|
|
+ state.listActive = index;
|
|
|
+ state.playState = 'play';
|
|
|
+ }}>
|
|
|
+ {{
|
|
|
+ icon: () => <Image src={musicBg} class={styles.songImg} />,
|
|
|
+ title: () => <div class={styles.songName}>{item.name}</div>,
|
|
|
+ value: () => (
|
|
|
+ <Icon
|
|
|
+ name={
|
|
|
+ index === state.listActive &&
|
|
|
+ state.playState === 'play'
|
|
|
+ ? songPlay
|
|
|
+ : songPause
|
|
|
+ }
|
|
|
+ class={[styles.iconSong]}
|
|
|
+ />
|
|
|
+ )
|
|
|
+ }}
|
|
|
+ </Cell>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </Popup>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ }
|
|
|
+});
|