feat:improve the audio function of the article

This commit is contained in:
zyronon
2025-09-14 16:41:33 +08:00
parent e921d00a15
commit 3b5d8d94ed
10 changed files with 189 additions and 276 deletions

View File

@@ -1,7 +1,8 @@
@use "anim" as *;
:root {
--color-reverse: blank;
--color-reverse-white: white;
--color-reverse-black: black;
--color-item-bg: rgb(228, 230, 232);
--color-item-hover: white;
//--color-item-active: rgb(75, 110, 175);
@@ -76,6 +77,9 @@
}
html.dark {
--color-reverse-white: black;
--color-reverse-black: white;
--color-primary: #0E1217;
--color-second: rgb(30, 31, 34);
--color-third: rgb(43, 45, 48);

View File

@@ -555,7 +555,7 @@ export function usePlaySentenceAudio() {
timer = setTimeout(() => {
console.log('停')
ref.pause()
}, (end - start) * 1000)
}, (end - start) / ref.playbackRate * 1000)
}
} else {
playWordAudio(sentence.text)

View File

@@ -16,7 +16,7 @@ import BaseIcon from "@/components/BaseIcon.vue";
import {useArticleOptions} from "@/hooks/dict.ts";
import {getDefaultArticle, getDefaultDict} from "@/types/func.ts";
import Toast from "@/pages/pc/components/base/toast/Toast.ts";
import Audio from "@/pages/pc/components/base/Audio.vue";
import ArticleAudio from "@/pages/pc/article/components/ArticleAudio.vue";
const runtimeStore = useRuntimeStore()
const base = useBaseStore()
@@ -135,10 +135,8 @@ const {
<div class="right flex-[4] shrink-0 pl-4 overflow-auto">
<div v-if="selectArticle.id">
<div class="en-article-family title text-xl">
<div class="text-center text-2xl" v-if="selectArticle.audioSrc">
<audio :src="selectArticle.audioSrc" controls></audio>
<Audio :src="selectArticle.audioSrc" controls></Audio>
<div class="text-center text-2xl my-2" v-if="selectArticle.audioSrc">
<ArticleAudio :article="selectArticle"></ArticleAudio>
</div>
<div class="text-center text-2xl">{{ selectArticle.title }}</div>
<div class="text-2xl" v-if="selectArticle.text">

View File

@@ -24,6 +24,8 @@ import {useRoute, useRouter} from "vue-router";
import book_list from "@/assets/book-list.json";
import PracticeLayout from "@/pages/pc/components/PracticeLayout.vue";
import Switch from "@/pages/pc/components/base/Switch.vue";
import Audio from "@/pages/pc/components/base/Audio.vue";
import ArticleAudio from "@/pages/pc/article/components/ArticleAudio.vue";
const store = useBaseStore()
const settingStore = useSettingStore()
@@ -361,7 +363,7 @@ function play2(e) {
color="#999"/>
</Tooltip>
<div class="bottom">
<div class="flex justify-between items-center">
<div class="flex justify-between items-center gap-2">
<div class="stat">
<div class="row">
<div class="num">{{ speedMinute }}分钟</div>
@@ -385,8 +387,7 @@ function play2(e) {
<div class="name">单词总数</div>
</div>
</div>
<audio ref="audioRef" v-if="articleData.article.audioSrc" :src="articleData.article.audioSrc"
controls></audio>
<ArticleAudio ref="audioRef" :article="articleData.article"></ArticleAudio>
<div class="flex flex-col items-center justify-center gap-1">
<div class="flex gap-2 center">
<Switch v-model="settingStore.articleSound"/>

View File

@@ -0,0 +1,62 @@
<script setup lang="ts">
import {Article} from "@/types/types.ts";
import {watch} from "vue";
import {LOCAL_FILE_KEY} from "@/utils/const.ts";
import {get} from "idb-keyval";
import Audio from "@/pages/pc/components/base/Audio.vue";
const props = defineProps<{
article: Article
}>()
let file = $ref(null)
let instance = $ref<{ audioRef: HTMLAudioElement }>({audioRef: null})
watch(() => props.article.audioFileId, async () => {
if (!props.article.audioSrc && props.article.audioFileId) {
let list = await get(LOCAL_FILE_KEY)
let rItem = list.find((file) => file.id === props.article.audioFileId)
if (rItem) {
file = URL.createObjectURL(rItem.file)
}
}else {
file = null
}
}, {immediate: true})
//转发一遍这里Proxy的默认值不能为{}可能是vue做了什么
defineExpose(new Proxy({
currentTime: 0,
played: false,
src: '',
volume: 0,
playbackRate: 1,
play: () => void 0,
pause: () => void 0,
}, {
get(target, key) {
if (key === 'currentTime') return instance?.audioRef?.currentTime
if (key === 'played') return instance?.audioRef?.played
if (key === 'src') return instance?.audioRef?.src
if (key === 'volume') return instance?.audioRef?.volume
if (key === 'playbackRate') return instance?.audioRef?.playbackRate
if (key === 'play') instance?.audioRef?.play()
if (key === 'pause') instance?.audioRef?.pause()
return target[key]
},
set(_, key, value) {
if (key === 'currentTime') instance.audioRef.currentTime = value
if (key === 'volume') return instance.audioRef.volume = value
return true
}
}))
</script>
<template>
<Audio v-bind="$attrs" ref="instance"
v-if="props.article.audioSrc"
:src="props.article.audioSrc"/>
<Audio ref="instance" v-else-if="file"
:src="file"
/>
</template>

View File

@@ -1,38 +0,0 @@
<script setup lang="ts">
import {Article} from "@/types/types.ts";
import {watch, ref} from "vue";
import {LOCAL_FILE_KEY} from "@/utils/const.ts";
import {get} from "idb-keyval";
const props = defineProps<{
article: Article
}>()
let file = $ref(null)
//这里不能用$ref不然父组件获取不到
let el = ref()
watch(() => props.article.audioFileId, async () => {
if (!props.article.audioSrc && props.article.audioFileId) {
let list = await get(LOCAL_FILE_KEY)
let rItem = list.find((file) => file.id === props.article.audioFileId)
if (rItem) {
file = URL.createObjectURL(rItem.file)
}
}
}, {immediate: true})
defineExpose({el})
</script>
<template>
<div>
<audio v-bind="$attrs" ref="el" v-if="props.article.audioSrc" :src="props.article.audioSrc" controls></audio>
<audio ref="el" v-else-if="file" :src="file" controls></audio>
</div>
</template>
<style scoped lang="scss">
</style>

View File

@@ -19,8 +19,7 @@ import InputNumber from "@/pages/pc/components/base/InputNumber.vue";
import {nanoid} from "nanoid";
import {update} from "idb-keyval";
import {LOCAL_FILE_KEY} from "@/utils/const.ts";
// import Audio from "@/pages/pc/article/components/Audio.vue";
import Audio from "@/pages/pc/components/base/Audio.vue";
import ArticleAudio from "@/pages/pc/article/components/ArticleAudio.vue";
import BaseInput from "@/pages/pc/components/base/BaseInput.vue";
const Dialog = defineAsyncComponent(() => import('@/pages/pc/components/dialog/Dialog.vue'))
@@ -160,7 +159,6 @@ function save(option: 'save' | 'saveAndNext') {
emit(option as any, editArticle)
return resolve(true)
}
saveTemp()
})
}
@@ -237,15 +235,16 @@ let currentSentence = $ref<Sentence>({} as any)
let editSentence = $ref<Sentence>({} as any)
let preSentence = $ref<Sentence>({} as any)
let showEditAudioDialog = $ref(false)
let sentenceAudioRef = $ref<{ el: HTMLAudioElement }>({el: null})
let audioRef = $ref<{ el: HTMLAudioElement }>({el: null})
let showAudioDialog = $ref(false)
let sentenceAudioRef = $ref<HTMLAudioElement>()
let audioRef = $ref<HTMLAudioElement>()
function handleShowEditAudioDialog(val: Sentence, i: number, j: number) {
showEditAudioDialog = true
currentSentence = val
editSentence = cloneDeep(val)
preSentence = null
audioRef.el.pause()
audioRef.pause()
if (j == 0) {
if (i != 0) {
preSentence = last(editArticle.sections[i - 1])
@@ -260,25 +259,25 @@ function handleShowEditAudioDialog(val: Sentence, i: number, j: number) {
}
}
_nextTick(() => {
sentenceAudioRef.el.currentTime = editSentence.audioPosition[0]
sentenceAudioRef.currentTime = editSentence.audioPosition[0]
})
}
function recordStart() {
if (sentenceAudioRef.el.paused) {
sentenceAudioRef.el.play()
if (sentenceAudioRef.paused) {
sentenceAudioRef.play()
}
editSentence.audioPosition[0] = Number(sentenceAudioRef.el.currentTime.toFixed(2))
editSentence.audioPosition[0] = Number(sentenceAudioRef.currentTime.toFixed(2))
if (editSentence.audioPosition[0] > editSentence.audioPosition[1]) {
editSentence.audioPosition[1] = editSentence.audioPosition[0]
}
}
function recordEnd() {
if (!sentenceAudioRef.el.paused) {
sentenceAudioRef.el.pause()
if (!sentenceAudioRef.paused) {
sentenceAudioRef.pause()
}
editSentence.audioPosition[1] = Number(sentenceAudioRef.el.currentTime.toFixed(2))
editSentence.audioPosition[1] = Number(sentenceAudioRef.currentTime.toFixed(2))
}
const {playSentenceAudio} = usePlaySentenceAudio()
@@ -290,7 +289,7 @@ function saveLrcPosition() {
}
function jumpAudio(time: number) {
sentenceAudioRef.el.currentTime = time
sentenceAudioRef.currentTime = time
}
function setPreEndTimeToCurrentStartTime() {
@@ -311,17 +310,13 @@ function setStartTime(val: Sentence, i: number, j: number) {
if (preSentence) {
val.audioPosition[0] = preSentence.audioPosition[1]
} else {
val.audioPosition[0] = Number(Number(audioRef.el.currentTime).toFixed(2))
val.audioPosition[0] = Number(Number(audioRef.currentTime).toFixed(2))
}
if (val.audioPosition[0] > val.audioPosition[1]) {
val.audioPosition[1] = val.audioPosition[0]
}
}
function uploadFileTrigger(id: string) {
(document.querySelector('#' + id) as HTMLDivElement)?.click()
}
</script>
<template>
@@ -430,16 +425,8 @@ function uploadFileTrigger(id: string) {
<div class="row flex flex-col gap-2">
<div class="flex gap-2">
<div class="title">结果</div>
<div class="flex gap-2 flex-1">
<div class="upload relative">
<BaseButton>添加音频LRC文件</BaseButton>
<input type="file"
accept=".lrc"
@change="handleChange"
class="w-full h-full absolute left-0 top-0 opacity-0"/>
</div>
<!-- <Audio ref="audioRef" :article="editArticle"/>-->
<Audio ref="audioRef" :src="editArticle.audioSrc"/>
<div class="flex gap-2 flex-1 justify-end">
<ArticleAudio ref="audioRef" :article="editArticle"/>
</div>
</div>
<template v-if="editArticle?.sections?.length">
@@ -450,16 +437,9 @@ function uploadFileTrigger(id: string) {
<div>|</div>
<div class="center flex-[3] gap-2">
<span>音频</span>
<BaseIcon title="添加音频"
@click="uploadFileTrigger('updateFile1')"
>
<BaseIcon title="音频管理" @click="showAudioDialog = true">
<IconIconParkOutlineAddMusic/>
</BaseIcon>
<input type="file"
id="updateFile1"
accept="audio/*"
@change="handleAudioChange"
class="w-0 h-0"/>
</div>
</div>
<div class="article-translate">
@@ -495,7 +475,7 @@ function uploadFileTrigger(id: string) {
<div v-if="sentence.audioPosition?.[1] !== -1">{{ sentence.audioPosition?.[1] ?? 0 }}s</div>
<div v-else> 结束</div>
<BaseIcon
@click="sentence.audioPosition[1] = Number(Number(audioRef.el.currentTime).toFixed(2))"
@click="sentence.audioPosition[1] = Number(Number(audioRef.currentTime).toFixed(2))"
title="设置结束时间"
>
<IconFluentMyLocation20Regular/>
@@ -513,7 +493,7 @@ function uploadFileTrigger(id: string) {
<BaseIcon
title="播放"
v-if="sentence.audioPosition?.length"
@click="playSentenceAudio(sentence,audioRef.el)">
@click="playSentenceAudio(sentence,audioRef)">
<IconFluentPlay20Regular/>
</BaseIcon>
</div>
@@ -553,7 +533,7 @@ function uploadFileTrigger(id: string) {
教程点击音频播放按钮当播放到句子开始时点击开始时间的 <span class="color-red">记录</span>
按钮当播放到句子结束时点击结束时间的 <span class="color-red">记录</span> 按钮最后再试听是否正确
</div>
<Audio ref="sentenceAudioRef" :article="editArticle" class="w-full"/>
<ArticleAudio ref="sentenceAudioRef" :article="editArticle" class="w-full"/>
<div class="flex items-center gap-2 space-between mb-2" v-if="editSentence.audioPosition?.length">
<div>{{ editSentence.text }}</div>
<div class="flex items-center gap-2 shrink-0">
@@ -564,7 +544,7 @@ function uploadFileTrigger(id: string) {
</div>
<BaseIcon
title="播放"
@click="playSentenceAudio(editSentence,sentenceAudioRef.el)">
@click="playSentenceAudio(editSentence,sentenceAudioRef)">
<IconFluentPlay20Regular/>
</BaseIcon>
</div>
@@ -606,6 +586,35 @@ function uploadFileTrigger(id: string) {
</div>
</div>
</Dialog>
<Dialog title="音频管理"
v-model="showAudioDialog"
:footer="false"
@close="showAudioDialog = false"
>
<div class="p-4 pt-0 color-main w-150 flex flex-col gap-2">
<div class="">
1上传的文件保存在本地电脑上更换电脑数据将丢失请及时备份数据
<br>
2LRC 文件用于解析句子对应音频的位置不一定准确后续可自行修改
</div>
<!-- <ArticleAudio ref="sentenceAudioRef" :article="editArticle" class="w-full"/>-->
<div class="upload relative">
<BaseButton>上传音频</BaseButton>
<input type="file"
accept="audio/*"
@change="handleAudioChange"
class="w-full h-full absolute left-0 top-0 opacity-0"/>
</div>
<div class="upload relative">
<BaseButton>上传 LRC 文件</BaseButton>
<input type="file"
accept=".lrc"
@change="handleChange"
class="w-full h-full absolute left-0 top-0 opacity-0"/>
</div>
</div>
</Dialog>
</div>
</template>

View File

@@ -72,29 +72,6 @@ const togglePlay = async () => {
}
};
const seekTo = (event: MouseEvent) => {
if (!audioRef.value || !progressBarRef.value || props.disabled) return;
const rect = progressBarRef.value.getBoundingClientRect();
const clickX = event.clientX - rect.left;
const percentage = Math.max(0, Math.min(1, clickX / rect.width));
const newTime = percentage * duration.value;
audioRef.value.currentTime = newTime;
currentTime.value = newTime;
};
const setVolume = (event: MouseEvent) => {
if (!audioRef.value || !volumeBarRef.value || props.disabled) return;
const rect = volumeBarRef.value.getBoundingClientRect();
const clickX = event.clientX - rect.left;
const percentage = Math.max(0, Math.min(1, clickX / rect.width));
volume.value = percentage;
audioRef.value.volume = percentage;
};
const toggleMute = () => {
if (!audioRef.value || props.disabled) return;
@@ -131,9 +108,6 @@ const handleLoadedMetadata = () => {
duration.value = audioRef.value?.duration || 0;
};
const handleCanPlay = () => {
};
const handleCanPlayThrough = () => {
};
@@ -282,14 +256,14 @@ const handleVolumeMouseDown = (event: MouseEvent) => {
// 立即跳转到点击位置
const clickY = event.clientY - rect.top;
// 反转百分比,因为我们希望底部是0%顶部是100%
const percentage = Math.max(0, Math.min(1, 1 - (clickY / rect.height)));
// 计算百分比,最上面是0%最下面是100%
const percentage = Math.max(0, Math.min(1, clickY / rect.height));
// 直接更新DOM样式
if (volumeFill && volumeThumb) {
volumeFill.style.height = `${percentage * 100}%`;
// 设置bottom而不是left
volumeThumb.style.bottom = `${percentage * 100}%`;
// 设置top而不是bottom
volumeThumb.style.top = `${percentage * 100}%`;
// 重置left样式
volumeThumb.style.left = '50%';
}
@@ -316,14 +290,14 @@ const handleVolumeMouseDown = (event: MouseEvent) => {
const rect = volumeBarRef.value!.getBoundingClientRect();
const clickY = e.clientY - rect.top;
// 反转百分比,因为我们希望底部是0%顶部是100%
const percentage = Math.max(0, Math.min(1, 1 - (clickY / rect.height)));
// 计算百分比,最上面是0%最下面是100%
const percentage = Math.max(0, Math.min(1, clickY / rect.height));
// 直接更新DOM样式不使用响应式变量
if (volumeFill && volumeThumb) {
volumeFill.style.height = `${percentage * 100}%`;
// 设置bottom而不是left
volumeThumb.style.bottom = `${percentage * 100}%`;
// 设置top而不是bottom
volumeThumb.style.top = `${percentage * 100}%`;
}
// 更新响应式变量和音频音量
@@ -357,10 +331,6 @@ const handleVolumeMouseDown = (event: MouseEvent) => {
document.addEventListener('mouseup', handleMouseUp);
};
const endVolumeDrag = () => {
isVolumeDragging.value = false;
};
// 监听属性变化
watch(() => props.src, (newSrc) => {
if (audioRef.value) {
@@ -403,12 +373,13 @@ watch(() => props.playbackRate, (newRate) => {
}
});
defineExpose({audioRef})
</script>
<template>
<div
class="custom-audio"
:class="{ 'disabled': disabled, 'has-error': error }"
:class="{ 'disabled': disabled||error, 'has-error': error }"
v-bind="attrs"
>
<!-- 隐藏的原生audio元素 -->
@@ -422,7 +393,6 @@ watch(() => props.playbackRate, (newRate) => {
@loadstart="handleLoadStart"
@loadeddata="handleLoadedData"
@loadedmetadata="handleLoadedMetadata"
@canplay="handleCanPlay"
@canplaythrough="handleCanPlayThrough"
@play="handlePlay"
@pause="handlePause"
@@ -438,7 +408,7 @@ watch(() => props.playbackRate, (newRate) => {
<!-- 播放/暂停按钮 -->
<button
class="play-button"
:class="{ 'playing': isPlaying, 'loading': isLoading }"
:class="{ 'loading': isLoading }"
@click="togglePlay"
:disabled="disabled"
:aria-label="isPlaying ? '暂停' : '播放'"
@@ -455,8 +425,7 @@ watch(() => props.playbackRate, (newRate) => {
<!-- 进度条区域 -->
<div class="progress-section">
<!-- 时间显示 -->
<span class="time-display">{{ formatTime(currentTime) }}</span>
<span class="time-display">{{ formatTime(currentTime) }} / {{ formatTime(duration) }}</span>
<!-- 进度条 -->
<div
class="progress-container"
@@ -475,8 +444,6 @@ watch(() => props.playbackRate, (newRate) => {
</div>
</div>
<!-- 总时长 -->
<span class="time-display">{{ formatTime(duration) }}</span>
</div>
<!-- 音量控制 -->
@@ -491,17 +458,9 @@ watch(() => props.playbackRate, (newRate) => {
:disabled="disabled"
:aria-label="volume > 0 ? '静音' : '取消静音'"
>
<svg v-if="volume === 0" class="icon" viewBox="0 0 24 24" fill="currentColor">
<path
d="M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 = 13.5 21 = 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 = 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z"/>
</svg>
<svg v-else-if="volume < 0.5" class="icon" viewBox="0 0 24 24" fill="currentColor">
<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02z"/>
</svg>
<svg v-else class="icon" viewBox="0 0 24 24" fill="currentColor">
<path
d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/>
</svg>
<IconBxVolumeMute v-if="volume === 0" class="icon"></IconBxVolumeMute>
<IconBxVolumeLow v-else-if="volume < 0.5" class="icon"></IconBxVolumeLow>
<IconBxVolumeFull v-else class="icon"></IconBxVolumeFull>
</button>
<!-- 音量下拉控制条 -->
@@ -514,11 +473,11 @@ watch(() => props.playbackRate, (newRate) => {
<div class="volume-track">
<div
class="volume-fill"
:style="{ height: volumeProgress + '%' }"
:style="{ height: volumeProgress + '%', top: 0 }"
></div>
<div
class="volume-thumb"
:style="{ bottom: volumeProgress + '%' }"
:style="{ top: volumeProgress + '%' }"
></div>
</div>
</div>
@@ -543,41 +502,30 @@ watch(() => props.playbackRate, (newRate) => {
<style scoped lang="scss">
.custom-audio {
// CSS变量定义可以通过外部覆盖来自定义样式
--audio-bg: #fff;
--audio-border-radius: 8px;
--audio-box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
--audio-container-bg: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--audio-text-color: white;
--audio-box-shadow: 0 2px 2px rgba(0, 0, 0, 0.1);
--audio-button-bg: rgba(255, 255, 255, 0.2);
--audio-button-hover-bg: rgba(255, 255, 255, 0.3);
--audio-button-playing-bg: rgba(255, 255, 255, 0.3);
--audio-progress-bg: rgba(255, 255, 255, 0.3);
--audio-progress-fill: rgba(255, 255, 255, 0.8);
--audio-thumb-bg: white;
--audio-thumb-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
--audio-volume-thumb-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
--audio-speed-button-bg: rgba(255, 255, 255, 0.1);
--audio-speed-button-border: rgba(255, 255, 255, 0.3);
--audio-speed-button-hover-bg: rgba(255, 255, 255, 0.2);
--audio-speed-button-hover-border: rgba(255, 255, 255, 0.5);
--audio-error-bg: #f56c6c;
--audio-error-color: white;
--height: 32px;
--gap: 8px;
display: inline-block;
box-sizing: border-box;
width: 100%;
max-width: 600px;
background: var(--color-primary);
border-radius: var(--audio-border-radius);
box-shadow: var(--audio-box-shadow);
color: black;
//overflow: hidden;
color: var(--color-reverse-black);
transition: all 0.3s ease;
font-family: var(--font-family);
padding: 0.3rem 0.4rem;
position: relative;
&.disabled {
opacity: 0.6;
pointer-events: none;
}
@@ -596,26 +544,18 @@ watch(() => props.playbackRate, (newRate) => {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border: none;
width: var(--height);
height: var(--height);
color: var(--color-reverse-black);
border-radius: 50%;
background: var(--color-second);
cursor: pointer;
transition: all 0.3s ease;
flex-shrink: 0;
border: 1px solid var(--audio-speed-button-border);
&:hover:not(:disabled) {
background: var(--audio-button-hover-bg);
transform: scale(1.05);
}
&:active:not(:disabled) {
transform: scale(0.95);
}
&.playing {
background: var(--audio-button-playing-bg);
&:hover {
background: var(--color-card-active) !important;
}
&.loading {
@@ -655,7 +595,6 @@ watch(() => props.playbackRate, (newRate) => {
.progress-container {
flex: 1;
height: 20px;
display: flex;
align-items: center;
cursor: pointer;
@@ -667,9 +606,7 @@ watch(() => props.playbackRate, (newRate) => {
width: 100%;
height: 6px;
background: var(--color-second);
//background: white;
border-radius: 2px;
overflow: hidden;
}
.progress-fill {
@@ -683,8 +620,8 @@ watch(() => props.playbackRate, (newRate) => {
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
width: 8px;
height: 8px;
width: 10px;
height: 10px;
background: var(--color-fourth);
border-radius: 50%;
box-shadow: var(--audio-thumb-shadow);
@@ -709,17 +646,17 @@ watch(() => props.playbackRate, (newRate) => {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border: none;
width: var(--height);
height: var(--height);
border-radius: 4px;
background: var(--color-second);
color: inherit;
cursor: pointer;
color: var(--color-reverse-black);
transition: all 0.2s ease;
border: 1px solid var(--audio-speed-button-border);
&:hover:not(:disabled) {
background: var(--audio-button-hover-bg);
&:hover {
background: var(--color-card-active);
}
.icon {
@@ -747,17 +684,6 @@ watch(() => props.playbackRate, (newRate) => {
opacity: 1;
visibility: visible;
}
&:before {
content: '';
position: absolute;
top: -4px;
left: 50%;
transform: translateX(-50%) rotate(45deg);
width: 8px;
height: 8px;
background: var(--audio-container-bg);
}
}
.volume-container {
@@ -772,30 +698,30 @@ watch(() => props.playbackRate, (newRate) => {
.volume-track {
position: relative;
width: 4px;
width: 6px;
height: 100%;
background: var(--audio-progress-bg);
background: var(--color-second);
border-radius: 2px;
overflow: hidden;
}
.volume-fill {
position: absolute;
bottom: 0;
top: 0;
width: 100%;
height: var(--fill-height);
background: var(--audio-progress-fill);
background: var(--color-fourth);
border-radius: 2px;
}
.volume-thumb {
position: absolute;
left: 50%;
bottom: var(--thumb-bottom);
transform: translate(-50%, 50%);
top: var(--thumb-top);
transform: translate(-50%, -50%);
width: 10px;
height: 10px;
background: var(--audio-thumb-bg);
background: var(--color-fourth);
border-radius: 50%;
box-shadow: var(--audio-volume-thumb-shadow);
cursor: grab;
@@ -808,28 +734,33 @@ watch(() => props.playbackRate, (newRate) => {
}
.speed-button {
padding: 6px 12px;
padding: 0 0.5rem;
border: 1px solid var(--audio-speed-button-border);
border-radius: 4px;
background: var(--color-second);
font-size: 12px;
font-weight: 500;
height: var(--height);
cursor: pointer;
color: var(--color-reverse-black);
transition: all 0.2s ease;
flex-shrink: 0;
&:hover:not(:disabled) {
background: var(--audio-speed-button-hover-bg);
border-color: var(--audio-speed-button-hover-border);
&:hover {
background: var(--color-card-active);
}
}
.error-message {
padding: 8px 16px;
position: absolute;
right: 0;
left: 2.6rem;
top: 0;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
background: var(--audio-error-bg);
color: var(--audio-error-color);
color: var(--color-reverse-white);
font-size: 12px;
text-align: center;
border-radius: var(--audio-border-radius);
}
// 动画
@@ -841,60 +772,4 @@ watch(() => props.playbackRate, (newRate) => {
transform: rotate(360deg);
}
}
// 响应式设计
@media (max-width: 480px) {
.custom-audio {
max-width: 100%;
}
.audio-container {
padding: 8px 12px;
gap: 8px;
min-height: 50px;
}
.play-button {
width: 36px;
height: 36px;
.icon {
width: 18px;
height: 18px;
}
}
.progress-section {
gap: 8px;
}
.time-display {
font-size: 11px;
min-width: 35px;
}
.volume-section {
gap: 6px;
}
.volume-button {
width: 28px;
height: 28px;
.icon {
width: 14px;
height: 14px;
}
}
.volume-container {
width: 50px;
}
.speed-button {
padding: 4px 8px;
font-size: 11px;
}
}
</style>