滑动式滚动条
不使用audio默认样式 自定义音频播放样式
当前时间 开始时间 结束时间 可播放可暂停 滚动条可拖动进行同步
具体样式可调整
npm install howler --save
<template><div class="audio-player"><div v-if="isLoading" class="loading-message">音频加载中...</div><div v-else><div class="audio-time-info"><span>开始时间: {{ formatDuration(startTime) }}</span><span>当前时间: {{ formatDuration(currentTime) }}</span><span>结束时间: {{ formatDuration(endTime) }}</span></div><div class="progress-container"><inputtype="range"class="progress-bar":min="0":max="duration"v-model.number="currentTime"@input="onSeek"/><div class="progress-fill" :style="{ width: progressWidth }"></div></div><div class="control-buttons"><button @click="togglePlayPause">{{ playPauseButtonText }}</button></div></div></div>
</template>
<script lang="ts">
import {defineComponent,ref,onMounted,onBeforeUnmount,watch,computed
} from 'vue';
import { Howl, Howler } from 'howler';
function formatDuration(seconds: number): string {const hours = Math.floor(seconds / 3600);const minutes = Math.floor((seconds % 3600) / 60);const secs = Math.floor(seconds % 60);if (hours > 0) {return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;} else {return `${minutes}:${secs.toString().padStart(2, '0')}`;}
}
export default defineComponent({setup() {const audioSrc = ref('../20250109.wav'); // 请替换为实际的音频文件路径const sound = ref<Howl | null>(null);const currentTime = ref(0);const duration = ref(0);const startTime = ref(0);const endTime = ref(0);const playing = ref(false);const isLoading = ref(true);let requestId: number | null = null;const initAudio = () => {isLoading.value = true;sound.value = new Howl({src: [audioSrc.value],onload: () => {duration.value = sound.value!.duration();endTime.value = sound.value!.duration();isLoading.value = false;},onplay: () => {playing.value = true;startTime.value = sound.value!.seek();startUpdatingTime();},onpause: () => {playing.value = false;stopUpdatingTime();},onend: () => {playing.value = false;startTime.value = 0;currentTime.value = 0;stopUpdatingTime();}});};const togglePlayPause = () => {if (sound.value) {if (playing.value) {sound.value.pause();} else {sound.value.play();}}};const onSeek = () => {if (sound.value) {sound.value.seek(currentTime.value);}};const startUpdatingTime = () => {const update = () => {if (sound.value && playing.value) {currentTime.value = sound.value!.seek();requestId = requestAnimationFrame(update);}};requestId = requestAnimationFrame(update);};const stopUpdatingTime = () => {if (requestId) {cancelAnimationFrame(requestId);requestId = null;}};onMounted(() => {initAudio();});onBeforeUnmount(() => {if (sound.value) {sound.value.unload();}});const progressWidth = computed(() => {if (duration.value > 0) {const percentage = (currentTime.value / duration.value) * 100;return `${percentage}%`;}return '0%';});return {sound,currentTime,duration,startTime,endTime,playing,togglePlayPause,onSeek,playPauseButtonText: computed(() => playing.value ? '暂停' : '播放'),formatDuration,isLoading,progressWidth};}
});
</script>
<style scoped>
.audio-player {display: flex;flex-direction: column;align-items: center;padding: 20px;border: 1px solid #ccc;border-radius: 5px;background-color: #f9f9f9;
}.audio-time-info {display: flex;justify-content: space-between;margin-bottom: 10px;width: 100%;
}.audio-time-info span {font-size: 14px;color: #666;
}/* Progress Container */
.progress-container {position: relative;width: 100%;height: 8px; /* 设置一个合适的高度 */background-color: #ddd;border-radius: 4px; /* 圆角边框 */overflow: hidden;cursor: pointer;
}/* Progress Bar (Hidden Input) */
.progress-bar {position: absolute;top: 0;left: 0;width: 100%;height: 100%;-webkit-appearance: none;appearance: none;background: transparent;outline: none;opacity: 0; /* 使输入不可见 */z-index: 2; /* 确保它覆盖在 .progress-fill 上 */cursor: pointer;
}/* Webkit 浏览器的滑块样式 */
.progress-bar::-webkit-slider-thumb {-webkit-appearance: none;appearance: none;width: 10px;height: 10px;border-radius: 50%;background: #4CAF50;cursor: pointer;box-shadow: 0 0 2px rgba(0, 0, 0, 0.5);
}/* Firefox 浏览器的滑块样式 */
.progress-bar::-moz-range-thumb {width: 10px;height: 10px;border-radius: 50%;background: #4CAF50;cursor: pointer;box-shadow: 0 0 2px rgba(0, 0, 0, 0.5);
}/* Progress Fill */
.progress-fill {position: absolute;top: 0;left: 0;height: 100%;background-color: #4CAF50;transition: width 0.2s linear;/* appearance: none; 改变元素默认风格 *//* background-color: #0074fd; */z-index: 1;
}.control-buttons button {width:60px;margin:10px 0 0 0;padding: 8px;border: none;background-color: #4CAF50;color: white;cursor: pointer;border-radius: 5px;transition: background-color 0.3s;
}.control-buttons button:hover {background-color: #45a049;
}.loading-message {color: #999;text-align: center;margin-bottom: 10px;
}
</style>
跳跃式滚动条
不使用audio默认样式 自定义音频播放样式
当前时间 开始时间 结束时间 可播放可暂停 滚动条可拖动进行同步
具体样式可调整
npm install howler --save
<template>
<!-- 跳跃式进度 --><div class="audio-player"><div v-if="isLoading" class="loading-message">音频加载中...</div><div v-else><div class="audio-time-info"><span>开始时间: {{ formatDuration(startTime) }}</span><span>结束时间: {{ formatDuration(endTime) }}</span><span>当前时间: {{ formatDuration(currentTime) }}</span></div><inputtype="range"class="progress-bar":min="0":max="duration"v-model.number="currentTime"@input="onSeek"/><div class="control-buttons"><button @click="togglePlayPause">{{ playPauseButtonText }}</button></div></div></div>
</template><script lang="ts">
import {defineComponent,ref,onMounted,onBeforeUnmount,watch,computed,onUnmounted
} from 'vue';
import { Howl, Howler } from 'howler';function formatDuration(seconds: number): string {const hours = Math.floor(seconds / 3600);const minutes = Math.floor((seconds % 3600) / 60);const secs = Math.floor(seconds % 60);if (hours > 0) {return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;} else {return `${minutes}:${secs.toString().padStart(2, '0')}`;}
}export default defineComponent({setup() {const audioSrc = ref('../20250109.wav'); // 请替换为实际的音频文件路径const sound = ref<Howl | null>(null);const currentTime = ref(0);const duration = ref(0);const startTime = ref(0);const endTime = ref(0);const playing = ref(false);const isLoading = ref(true);let requestId: number | null = null;const initAudio = () => {isLoading.value = true;sound.value = new Howl({src: [audioSrc.value],onload: () => {duration.value = sound.value!.duration();endTime.value = sound.value!.duration();isLoading.value = false;},onplay: () => {playing.value = true;startTime.value = sound.value!.seek();startUpdatingTime();},onpause: () => {playing.value = false;stopUpdatingTime();},onend: () => {playing.value = false;startTime.value = 0;currentTime.value = 0;stopUpdatingTime();}});};const togglePlayPause = () => {if (sound.value) {if (playing.value) {sound.value.pause();} else {sound.value.play();}}};const onSeek = () => {if (sound.value) {sound.value.seek(currentTime.value);}};const startUpdatingTime = () => {const update = () => {if (sound.value && playing.value) {currentTime.value = sound.value!.seek();requestId = requestAnimationFrame(update);}};requestId = requestAnimationFrame(update);};const stopUpdatingTime = () => {if (requestId) {cancelAnimationFrame(requestId);requestId = null;}};onMounted(() => {initAudio();});onBeforeUnmount(() => {if (sound.value) {sound.value.unload();}});onUnmounted(() => {stopUpdatingTime();});watch(currentTime, () => {if (sound.value) {sound.value.seek(currentTime.value);}});return {sound,currentTime,duration,startTime,endTime,playing,togglePlayPause,onSeek,playPauseButtonText: computed(() => (playing.value? '暂停' : '播放')),formatDuration,isLoading};}
});
</script><style scoped>
.audio-player {display: flex;flex-direction: column;align-items: center;padding: 20px;border: 1px solid #ccc;border-radius: 5px;background-color: #f9f9f9;
}
.audio-time-info {display: flex;justify-content: space-between;margin-bottom: 10px;width: 100%;
}
.audio-time-info span {font-size: 14px;color: #666;
}
.progress-bar {width: 100%;margin-bottom: 10px;-webkit-appearance: none;appearance: none;height: 5px;background: #ddd;outline: none;opacity: 0.7;-webkit-transition: 0.2s;transition: 0.2s;border-radius: 5px;
}
.progress-bar::-webkit-slider-thumb {-webkit-appearance: none;appearance: none;width: 15px;height: 15px;border-radius: 50%;background: #4CAF50;cursor: pointer;
}
.progress-bar::-moz-range-thumb {width: 15px;height: 15px;border-radius: 50%;background: #4CAF50;cursor: pointer;
}
.control-buttons button {padding: 10px 20px;border: none;background-color: #4CAF50;color: white;cursor: pointer;border-radius: 5px;
}
.loading-message {color: #999;text-align: center;margin-bottom: 10px;
}
</style>
自定义音频播放组件
如需更改可进行更改样式
不使用audio默认样式 自定义音频播放样式
当前时间 开始时间 结束时间 可播放可暂停 滚动条可拖动进行同步
具体样式可调整
npm install howler --save
父组件
<template><div><AudioPlayer :src="'../20250109.wav'" @play="handlePlay" @pause="handlePause" @end="handleEnd" />// src对应你的音频文件</div>
</template><script lang="ts">
import { defineComponent } from 'vue';
import AudioPlayer from './components/pop.vue';export default defineComponent({components: {AudioPlayer},methods: {handlePlay() {console.log('音频开始播放');},handlePause() {console.log('音频暂停');},handleEnd() {console.log('音频播放结束');}}
});
</script>
子组件
components-pop.vue
<template><div class="audio-player"><div v-if="isLoading" class="loading-message">音频加载中...</div><div v-else><div class="audio-time-info"><span>开始时间: {{ formatDuration(startTime) }}</span><span>当前时间: {{ formatDuration(currentTime) }}</span><span>结束时间: {{ formatDuration(endTime) }}</span></div><div class="progress-container"><inputtype="range"class="progress-bar":min="0":max="duration"v-model.number="currentTime"@input="onSeek"/><div class="progress-fill" :style="{ width: progressWidth }"></div></div><div class="control-buttons"><button @click="togglePlayPause">{{ playPauseButtonText }}</button></div></div></div>
</template><script lang="ts">
import { defineComponent, PropType, ref, onMounted, onBeforeUnmount, watch, computed } from 'vue';
import { Howl, Howler } from 'howler';export default defineComponent({name: 'AudioPlayer',props: {src: {type: String as PropType<string>,required: true,},},emits: ['play', 'pause', 'end'],setup(props, { emit }) {const sound = ref<Howl | null>(null);const currentTime = ref(0);const duration = ref(0);const startTime = ref(0);const endTime = ref(0);const playing = ref(false);const isLoading = ref(true);let requestId: number | null = null;const initAudio = () => {isLoading.value = true;sound.value = new Howl({src: [props.src],onload: () => {duration.value = sound.value!.duration();endTime.value = sound.value!.duration();isLoading.value = false;},onplay: () => {playing.value = true;startTime.value = sound.value!.seek();startUpdatingTime();emit('play');},onpause: () => {playing.value = false;stopUpdatingTime();emit('pause');},onend: () => {playing.value = false;startTime.value = 0;currentTime.value = 0;stopUpdatingTime();emit('end');}});};const togglePlayPause = () => {if (sound.value) {if (playing.value) {sound.value.pause();} else {sound.value.play();}}};const onSeek = () => {if (sound.value) {sound.value.seek(currentTime.value);}};const startUpdatingTime = () => {const update = () => {if (sound.value && playing.value) {currentTime.value = sound.value!.seek()!;requestId = requestAnimationFrame(update);}};requestId = requestAnimationFrame(update);};const stopUpdatingTime = () => {if (requestId) {cancelAnimationFrame(requestId);requestId = null;}};onMounted(() => {initAudio();});onBeforeUnmount(() => {if (sound.value) {sound.value.unload();}});const progressWidth = computed(() => {if (duration.value > 0) {const percentage = (currentTime.value / duration.value) * 100;return `${percentage}%`;}return '0%';});const formatDuration = (seconds: number): string => {const hours = Math.floor(seconds / 3600);const minutes = Math.floor((seconds % 3600) / 60);const secs = Math.floor(seconds % 60);if (hours > 0) {return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;} else {return `${minutes}:${secs.toString().padStart(2, '0')}`;}};return {sound,currentTime,duration,startTime,endTime,playing,togglePlayPause,onSeek,playPauseButtonText: computed(() => playing.value ? '暂停' : '播放'),formatDuration,isLoading,progressWidth,};}
});
</script><style scoped>
/* 省略了之前的样式部分,因为它们已经符合要求 */
.audio-player {display: flex;flex-direction: column;align-items: center;padding: 20px;border: 1px solid #ccc;border-radius: 5px;background-color: #f9f9f9;
}.audio-time-info {display: flex;justify-content: space-between;margin-bottom: 10px;width: 100%;
}.audio-time-info span {font-size: 14px;color: #666;
}/* Progress Container */
.progress-container {position: relative;width: 100%;height: 8px; /* 设置一个合适的高度 */background-color: #ddd;border-radius: 4px; /* 圆角边框 */overflow: hidden;cursor: pointer;
}/* Progress Bar (Hidden Input) */
.progress-bar {position: absolute;top: 0;left: 0;width: 100%;height: 100%;-webkit-appearance: none;appearance: none;background: transparent;outline: none;opacity: 0; /* 使输入不可见 */z-index: 2; /* 确保它覆盖在 .progress-fill 上 */cursor: pointer;
}/* Webkit 浏览器的滑块样式 */
.progress-bar::-webkit-slider-thumb {-webkit-appearance: none;appearance: none;width: 10px;height: 10px;border-radius: 50%;background: #4CAF50;cursor: pointer;box-shadow: 0 0 2px rgba(0, 0, 0, 0.5);
}/* Firefox 浏览器的滑块样式 */
.progress-bar::-moz-range-thumb {width: 10px;height: 10px;border-radius: 50%;background: #4CAF50;cursor: pointer;box-shadow: 0 0 2px rgba(0, 0, 0, 0.5);
}/* Progress Fill */
.progress-fill {position: absolute;top: 0;left: 0;height: 100%;background-color: #4CAF50;transition: width 0.2s linear;z-index: 1;
}.control-buttons button {width:60px;margin:10px 0 0 0;padding: 8px;border: none;background-color: #4CAF50;color: white;cursor: pointer;border-radius: 5px;transition: background-color 0.3s;
}.control-buttons button:hover {background-color: #45a049;
}.loading-message {color: #999;text-align: center;margin-bottom: 10px;
}
</style>