| <template> | |
| <div | |
| class="audio-player" | |
| :style="{ transform: `scale(${1 / scale})` }" | |
| > | |
| <audio | |
| class="audio" | |
| ref="audioRef" | |
| :src="src" | |
| :autoplay="autoplay" | |
| @durationchange="handleDurationchange()" | |
| @timeupdate="handleTimeupdate()" | |
| @play="handlePlayed()" | |
| @ended="handleEnded()" | |
| @progress="handleProgress()" | |
| @error="handleError()" | |
| ></audio> | |
| <div class="controller"> | |
| <div class="icons"> | |
| <div class="icon play-icon" @click="toggle()"> | |
| <span class="icon-content"> | |
| <IconPlayOne v-if="paused" /> | |
| <IconPause v-else /> | |
| </span> | |
| </div> | |
| <div class="volume"> | |
| <div class="icon volume-icon" @click="toggleVolume()"> | |
| <span class="icon-content"> | |
| <IconVolumeMute v-if="volume === 0" /> | |
| <IconVolumeNotice v-else-if="volume === 1" /> | |
| <IconVolumeSmall v-else /> | |
| </span> | |
| </div> | |
| <div | |
| class="volume-bar-wrap" | |
| @mousedown="handleMousedownVolumeBar()" | |
| @touchstart="handleMousedownVolumeBar()" | |
| @click="$event => handleClickVolumeBar($event)" | |
| > | |
| <div class="volume-bar" ref="volumeBarRef"> | |
| <div class="volume-bar-inner" :style="{ width: volumeBarWidth }"> | |
| <span class="thumb"></span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <span class="time"> | |
| <span class="ptime">{{ptime}}</span> / <span class="dtime">{{dtime}}</span> | |
| </span> | |
| <div | |
| class="bar-wrap" | |
| ref="playBarWrap" | |
| @mousedown="handleMousedownPlayBar()" | |
| @touchstart="handleMousedownPlayBar()" | |
| @mousemove="$event => handleMousemovePlayBar($event)" | |
| @mouseenter="playBarTimeVisible = true" | |
| @mouseleave="playBarTimeVisible = false" | |
| > | |
| <div class="bar-time" :class="{ 'hidden': !playBarTimeVisible }" :style="{ left: playBarTimeLeft }">{{playBarTime}}</div> | |
| <div class="bar"> | |
| <div class="loaded" :style="{ width: loadedBarWidth }"></div> | |
| <div class="played" :style="{ width: playedBarWidth }"> | |
| <span class="thumb"></span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </template> | |
| <script lang="ts" setup> | |
| import { computed, ref } from 'vue' | |
| import message from '@/utils/message' | |
| const props = withDefaults(defineProps<{ | |
| src: string | |
| loop: boolean | |
| autoplay?: boolean | |
| scale?: number | |
| }>(), { | |
| autoplay: false, | |
| scale: 1, | |
| }) | |
| const secondToTime = (second = 0) => { | |
| if (second === 0 || isNaN(second)) return '00:00' | |
| const add0 = (num: number) => (num < 10 ? '0' + num : '' + num) | |
| const hour = Math.floor(second / 3600) | |
| const min = Math.floor((second - hour * 3600) / 60) | |
| const sec = Math.floor(second - hour * 3600 - min * 60) | |
| return (hour > 0 ? [hour, min, sec] : [min, sec]).map(add0).join(':') | |
| } | |
| const getBoundingClientRectViewLeft = (element: HTMLElement) => { | |
| return element.getBoundingClientRect().left | |
| } | |
| const audioRef = ref<HTMLAudioElement>() | |
| const playBarWrap = ref<HTMLElement>() | |
| const volumeBarRef = ref<HTMLElement>() | |
| const volume = ref(0.5) | |
| const paused = ref(true) | |
| const currentTime = ref(0) | |
| const duration = ref(0) | |
| const loaded = ref(0) | |
| const playBarTimeVisible = ref(false) | |
| const playBarTime = ref('00:00') | |
| const playBarTimeLeft = ref('0') | |
| const ptime = computed(() => secondToTime(currentTime.value)) | |
| const dtime = computed(() => secondToTime(duration.value)) | |
| const playedBarWidth = computed(() => currentTime.value / duration.value * 100 + '%') | |
| const loadedBarWidth = computed(() => loaded.value / duration.value * 100 + '%') | |
| const volumeBarWidth = computed(() => volume.value * 100 + '%') | |
| const seek = (time: number) => { | |
| if (!audioRef.value) return | |
| time = Math.max(time, 0) | |
| time = Math.min(time, duration.value) | |
| audioRef.value.currentTime = time | |
| currentTime.value = time | |
| } | |
| const play = () => { | |
| if (!audioRef.value) return | |
| paused.value = false | |
| audioRef.value.play() | |
| } | |
| const pause = () => { | |
| if (!audioRef.value) return | |
| paused.value = true | |
| audioRef.value.pause() | |
| } | |
| const toggle = () => { | |
| if (paused.value) play() | |
| else pause() | |
| } | |
| const setVolume = (percentage: number) => { | |
| if (!audioRef.value) return | |
| percentage = Math.max(percentage, 0) | |
| percentage = Math.min(percentage, 1) | |
| audioRef.value.volume = percentage | |
| volume.value = percentage | |
| if (audioRef.value.muted && percentage !== 0) audioRef.value.muted = false | |
| } | |
| const handleDurationchange = () => { | |
| duration.value = audioRef.value?.duration || 0 | |
| } | |
| const handleTimeupdate = () => { | |
| currentTime.value = audioRef.value?.currentTime || 0 | |
| } | |
| const handlePlayed = () => { | |
| paused.value = false | |
| } | |
| const handleEnded = () => { | |
| if (!props.loop) pause() | |
| else { | |
| seek(0) | |
| play() | |
| } | |
| } | |
| const handleProgress = () => { | |
| loaded.value = audioRef.value?.buffered.length ? audioRef.value.buffered.end(audioRef.value.buffered.length - 1) : 0 | |
| } | |
| const handleError = () => message.error('视频加载失败') | |
| const thumbMove = (e: MouseEvent | TouchEvent) => { | |
| if (!audioRef.value || !playBarWrap.value) return | |
| const clientX = 'clientX' in e ? e.clientX : e.changedTouches[0].clientX | |
| let percentage = (clientX - getBoundingClientRectViewLeft(playBarWrap.value)) / playBarWrap.value.clientWidth | |
| percentage = Math.max(percentage, 0) | |
| percentage = Math.min(percentage, 1) | |
| const time = percentage * duration.value | |
| audioRef.value.currentTime = time | |
| currentTime.value = time | |
| } | |
| const thumbUp = (e: MouseEvent | TouchEvent) => { | |
| if (!audioRef.value || !playBarWrap.value) return | |
| const clientX = 'clientX' in e ? e.clientX : e.changedTouches[0].clientX | |
| let percentage = (clientX - getBoundingClientRectViewLeft(playBarWrap.value)) / playBarWrap.value.clientWidth | |
| percentage = Math.max(percentage, 0) | |
| percentage = Math.min(percentage, 1) | |
| const time = percentage * duration.value | |
| audioRef.value.currentTime = time | |
| currentTime.value = time | |
| document.removeEventListener('mousemove', thumbMove) | |
| document.removeEventListener('touchmove', thumbMove) | |
| document.removeEventListener('mouseup', thumbUp) | |
| document.removeEventListener('touchend', thumbUp) | |
| } | |
| const handleMousedownPlayBar = () => { | |
| document.addEventListener('mousemove', thumbMove) | |
| document.addEventListener('touchmove', thumbMove) | |
| document.addEventListener('mouseup', thumbUp) | |
| document.addEventListener('touchend', thumbUp) | |
| } | |
| const volumeMove = (e: MouseEvent | TouchEvent) => { | |
| if (!volumeBarRef.value) return | |
| const clientX = 'clientX' in e ? e.clientX : e.changedTouches[0].clientX | |
| const percentage = (clientX - getBoundingClientRectViewLeft(volumeBarRef.value)) / 45 | |
| setVolume(percentage) | |
| } | |
| const volumeUp = () => { | |
| document.removeEventListener('mousemove', volumeMove) | |
| document.removeEventListener('touchmove', volumeMove) | |
| document.removeEventListener('mouseup', volumeUp) | |
| document.removeEventListener('touchend', volumeUp) | |
| } | |
| const handleMousedownVolumeBar = () => { | |
| document.addEventListener('mousemove', volumeMove) | |
| document.addEventListener('touchmove', volumeMove) | |
| document.addEventListener('mouseup', volumeUp) | |
| document.addEventListener('touchend', volumeUp) | |
| } | |
| const handleClickVolumeBar = (e: MouseEvent) => { | |
| if (!volumeBarRef.value) return | |
| const percentage = (e.clientX - getBoundingClientRectViewLeft(volumeBarRef.value)) / 45 | |
| setVolume(percentage) | |
| } | |
| const handleMousemovePlayBar = (e: MouseEvent) => { | |
| if (duration.value && playBarWrap.value) { | |
| const px = playBarWrap.value.getBoundingClientRect().left | |
| const tx = e.clientX - px | |
| if (tx < 0 || tx > playBarWrap.value.offsetWidth) return | |
| const time = duration.value * (tx / playBarWrap.value.offsetWidth) | |
| playBarTimeLeft.value = `${tx - (time >= 3600 ? 25 : 20)}px` | |
| playBarTime.value = secondToTime(time) | |
| playBarTimeVisible.value = true | |
| } | |
| } | |
| const toggleVolume = () => { | |
| if (!audioRef.value) return | |
| if (audioRef.value.muted) { | |
| audioRef.value.muted = false | |
| setVolume(0.5) | |
| } | |
| else { | |
| audioRef.value.muted = true | |
| setVolume(0) | |
| } | |
| } | |
| defineExpose({ | |
| toggle, | |
| }) | |
| </script> | |
| <style scoped lang="scss"> | |
| .audio-player { | |
| width: 280px; | |
| height: 50px; | |
| position: relative; | |
| user-select: none; | |
| line-height: 1; | |
| transform-origin: 0 0; | |
| background: #000; | |
| } | |
| .controller { | |
| position: absolute; | |
| bottom: 0; | |
| left: 0; | |
| right: 0; | |
| height: 41px; | |
| padding: 0 20px; | |
| user-select: none; | |
| transition: all 0.3s ease; | |
| .bar-wrap { | |
| padding: 5px 0; | |
| cursor: pointer; | |
| position: absolute; | |
| bottom: 35px; | |
| width: calc(100% - 40px); | |
| height: 3px; | |
| &:hover .bar .played .thumb { | |
| transform: scale(1); | |
| } | |
| .bar-time { | |
| position: absolute; | |
| left: 0; | |
| top: -20px; | |
| border-radius: 4px; | |
| padding: 5px 7px; | |
| background-color: rgba(0, 0, 0, 0.62); | |
| color: #fff; | |
| font-size: 12px; | |
| text-align: center; | |
| opacity: 1; | |
| transition: opacity 0.1s ease-in-out; | |
| word-wrap: normal; | |
| word-break: normal; | |
| z-index: 2; | |
| pointer-events: none; | |
| &.hidden { | |
| opacity: 0; | |
| } | |
| } | |
| .bar { | |
| position: relative; | |
| height: 3px; | |
| width: 100%; | |
| background: rgba(255, 255, 255, 0.2); | |
| cursor: pointer; | |
| .loaded { | |
| position: absolute; | |
| left: 0; | |
| top: 0; | |
| bottom: 0; | |
| background: rgba(255, 255, 255, 0.4); | |
| height: 3px; | |
| transition: all 0.5s ease; | |
| will-change: width; | |
| } | |
| .played { | |
| position: absolute; | |
| left: 0; | |
| top: 0; | |
| bottom: 0; | |
| height: 3px; | |
| will-change: width; | |
| background-color: #fff; | |
| .thumb { | |
| position: absolute; | |
| top: 0; | |
| right: 5px; | |
| margin-top: -4px; | |
| margin-right: -10px; | |
| height: 11px; | |
| width: 11px; | |
| border-radius: 50%; | |
| cursor: pointer; | |
| transition: all 0.3s ease-in-out; | |
| transform: scale(0); | |
| background-color: #fff; | |
| } | |
| } | |
| } | |
| } | |
| .icons { | |
| height: 38px; | |
| position: absolute; | |
| bottom: 0; | |
| left: 14px; | |
| display: flex; | |
| align-items: center; | |
| .icon { | |
| width: 36px; | |
| height: 100%; | |
| position: relative; | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| font-size: 20px; | |
| &.play-icon { | |
| font-size: 26px; | |
| } | |
| .icon-content { | |
| transition: all .2s ease-in-out; | |
| opacity: 0.8; | |
| color: #fff; | |
| } | |
| &.active .icon-content { | |
| opacity: 1; | |
| } | |
| &:hover .icon-content { | |
| opacity: 1; | |
| } | |
| } | |
| .volume { | |
| height: 100%; | |
| position: relative; | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| &:hover { | |
| .volume-bar-wrap .volume-bar { | |
| width: 45px; | |
| } | |
| .volume-bar-wrap .volume-bar .volume-bar-inner .thumb { | |
| transform: scale(1); | |
| } | |
| } | |
| &.volume-active { | |
| .volume-bar-wrap .volume-bar { | |
| width: 45px; | |
| } | |
| .volume-bar-wrap .volume-bar .volume-bar-inner .thumb { | |
| transform: scale(1); | |
| } | |
| } | |
| } | |
| .volume-bar-wrap { | |
| display: inline-block; | |
| margin: 0 15px 0 -5px; | |
| vertical-align: middle; | |
| height: 100%; | |
| } | |
| .volume-bar { | |
| position: relative; | |
| top: 17px; | |
| width: 0; | |
| height: 3px; | |
| background: #aaa; | |
| transition: all 0.3s ease-in-out; | |
| .volume-bar-inner { | |
| position: absolute; | |
| bottom: 0; | |
| left: 0; | |
| height: 100%; | |
| transition: all 0.1s ease; | |
| will-change: width; | |
| background-color: #fff; | |
| .thumb { | |
| position: absolute; | |
| top: 0; | |
| right: 5px; | |
| margin-top: -4px; | |
| margin-right: -10px; | |
| height: 11px; | |
| width: 11px; | |
| border-radius: 50%; | |
| cursor: pointer; | |
| transition: all 0.3s ease-in-out; | |
| transform: scale(0); | |
| background-color: #fff; | |
| } | |
| } | |
| } | |
| } | |
| .time { | |
| height: 38px; | |
| position: absolute; | |
| right: 20px; | |
| bottom: 0; | |
| display: flex; | |
| align-items: center; | |
| line-height: 38px; | |
| color: #eee; | |
| text-shadow: 0 0 2px rgba(0, 0, 0, 0.5); | |
| vertical-align: middle; | |
| font-size: 13px; | |
| cursor: default; | |
| .ptime { | |
| margin-right: 2px; | |
| } | |
| .dtime { | |
| margin-left: 2px; | |
| } | |
| } | |
| } | |
| </style> |