研究了一天终于完美实现了UI图的设计
实现的效果:
代码如下:
import { useRef, useState, useEffect, forwardRef, useImperativeHandle } from 'react'
import clsx from 'clsx'
import { useStores } from '@/hooks'
import styles from './index.less'
const AudioPlay = (props, ref) => {
const { src, duration } = props
const [isPlay, setIsPlay] = useState(false)
const [isCanplay, setIsCanplay] = useState(false) // 是否可以播放
const [currentTime, setCurrentTime] = useState(0) // 当前播放进度时间,单位秒
const [totalTime, setTotalTime] = useState(0) // 总时长,单位秒
const audioRef = useRef(null)
useEffect(() => {
if (duration) {
setTotalTime(duration < 1 ? 1 : duration) // 小于1秒,会显示成00:00,至少1秒
setIsCanplay(true)
}
}, [duration])
useEffect(() => {
const audio = audioRef.current
if (audio) {
audio.addEventListener('loadedmetadata', onLoadedMetadata, false)
audio.addEventListener('timeupdate', onTimeUpdate, false)
audio.addEventListener('ended', onEnd, false)
audio.addEventListener('pause', onPause, false)
}
return () => {
const audio = audioRef.current
if (audio) {
audio.removeEventListener('loadedmetadata', onLoadedMetadata)
audio.removeEventListener('timeupdate', onTimeUpdate)
audio.removeEventListener('ended', onEnd)
audio.removeEventListener('pause', onPause)
audio.pause()
}
}
}, [])
const formatSecond = (time) => {
const second = Math.floor(time % 60)
const minute = Math.floor(time / 60)
return `${minute}:${second >= 10 ? second : `0${second}`}`
}
const onLoadedMetadata = () => {
if (audioRef.current) {
const { duration } = audioRef.current
setTotalTime(duration < 1 ? 1 : duration) // 小于1秒,会显示成00:00,至少1秒
setIsCanplay(true)
}
}
const onTimeUpdate = () => {
if (audioRef.current) {
const { currentTime, duration } = audioRef.current
setCurrentTime(currentTime)
if (currentTime >= duration) {
pauseAudio()
}
}
}
const onEnd = () => {
setIsPlay(false)
window.setTimeout(() => {
setCurrentTime(0)
}, 300)
}
const onPause = () => {
setIsPlay(false)
}
const playAudio = () => {
const audioList = document.querySelectorAll('.kb-audio audio')
audioList.forEach((v) => {
if (audioRef.current !== v) {
v.pause() // 暂停其他音频
v.currentTime = 0 // 进度回到0
}
})
if (audioRef.current) {
audioRef.current.play()
setIsPlay(true)
}
}
const pauseAudio = () => {
if (audioRef.current) {
audioRef.current.pause()
setIsPlay(false)
}
}
const changeTime = (e) => {
const { value } = e.target
const audio = audioRef.current
if (audio) {
setCurrentTime(value)
audio.currentTime = value
if (value === audioRef.current.duration) {
setIsPlay(false)
}
}
}
// 暴露 audio 和一些方法给外部
useImperativeHandle(ref, () => ({
audio: audioRef.current,
isPlay,
isCanplay,
currentTime,
totalTime,
playAudio,
pauseAudio,
}))
return (
<div className={clsx(styles['basic-audio'], 'kb-audio')} onClick={(e) => e.stopPropagation()}>
<audio src={src || null} ref={audioRef} preload="auto">
<track src={src || null} kind="captions" />
</audio>
<div className={styles['audio-control-wrap']}>
<div className={styles['audio-control']}>
{!isCanplay ? (
<div className={styles['audio-loading']}></div>
) : (
<>
{isPlay ? (
<div className={styles['audio-pause']} onClick={pauseAudio}></div>
) : (
<div className={styles['audio-play']} onClick={playAudio}></div>
)}
</>
)}
</div>
<div className={styles['audio-progress']}>
<div className={styles['audio-progress-bg']}>
<div
className={styles['audio-progress-bar']}
style={{ width: `${(currentTime / totalTime) * 100}%` }}
></div>
</div>
<input
title="音频进度"
type="range"
step="0.01"
max={totalTime}
value={currentTime}
onChange={changeTime}
disabled={!isCanplay}
className={styles['audio-progress']}
/>
</div>
<span className={styles['audio-time']}>
{formatSecond(currentTime)}/{formatSecond(totalTime)}
</span>
</div>
</div>
)
}
export default forwardRef(AudioPlay)
.basic-audio {
width: 435px;
height: 48px;
background: #e6f4ff;
border-radius: 7px;
padding-left: 20px;
padding-right: 15px;
box-sizing: border-box;
display: flex;
align-items: center;
.audio-control-wrap {
flex: 1;
display: flex;
justify-content: space-between;
align-items: center;
.audio-control {
.audio-loading {
width: 20px;
height: 20px;
border: 4px solid #5671ee;
border-top-color: transparent;
border-radius: 100%;
animation: circle infinite 1s linear;
}
// 转转转动画
@keyframes circle {
0% {
transform: rotate(0);
}
100% {
transform: rotate(360deg);
}
}
.audio-pause {
width: 20px;
height: 26px;
background: url('@{cdnDomain}fe/xh-sls/common/audio-pause.png') no-repeat center center;
background-size: 100% 100%;
cursor: pointer;
&:hover,
&:active {
opacity: 0.9;
}
}
.audio-play {
width: 20px;
height: 26px;
background: url('@{cdnDomain}fe/xh-sls/common/audio-play.png') no-repeat center center;
background-size: 100% 100%;
cursor: pointer;
&:hover,
&:active {
opacity: 0.9;
}
}
}
.audio-time {
font-weight: bold;
font-size: 14px;
color: #5671ee;
}
.audio-progress {
flex: 1;
margin-left: 20px;
margin-right: 15px;
position: relative;
z-index: 1;
display: flex;
justify-content: center;
align-items: center;
.audio-progress-bg {
height: 14px;
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
z-index: 1;
width: 100%;
background: rgba(56, 89, 245, 0.21);
border-radius: 7px;
padding: 2px 3px;
.audio-progress-bar {
height: 100%;
background-color: #5671ee;
border-top-left-radius: 7px;
border-bottom-left-radius: 7px;
}
}
// 进度条样式
[type='range'] {
position: relative;
z-index: 2;
-webkit-appearance: none;
appearance: none;
margin: 0;
outline: 0;
width: 100%;
height: 14px;
background-color: transparent;
}
/* 设置滑块颜色 */
[type='range']::-webkit-slider-thumb {
appearance: none;
width: 24px;
height: 24px;
border-radius: 50%;
background-color: #ffffff;
border: 2px solid #5671ee;
cursor: pointer;
}
[type='range']::-moz-range-thumb {
appearance: none;
width: 24px;
height: 24px;
border-radius: 50%;
background-color: #ffffff;
border: 2px solid #5671ee;
cursor: pointer;
}
[type='range']::-ms-thumb {
appearance: none;
width: 24px;
height: 24px;
border-radius: 50%;
background-color: #ffffff;
border: 2px solid #5671ee;
cursor: pointer;
}
}
}
}