feat:improve the audio function of the article
This commit is contained in:
1
components.d.ts
vendored
1
components.d.ts
vendored
@@ -16,6 +16,7 @@ declare module 'vue' {
|
||||
IconBxVolume: typeof import('~icons/bx/volume')['default']
|
||||
IconBxVolumeFull: typeof import('~icons/bx/volume-full')['default']
|
||||
IconBxVolumeLow: typeof import('~icons/bx/volume-low')['default']
|
||||
IconBxVolumeMute: typeof import('~icons/bx/volume-mute')['default']
|
||||
IconEosIconsLoading: typeof import('~icons/eos-icons/loading')['default']
|
||||
IconFluentAdd16Filled: typeof import('~icons/fluent/add16-filled')['default']
|
||||
IconFluentAdd16Regular: typeof import('~icons/fluent/add16-regular')['default']
|
||||
|
||||
@@ -834,12 +834,13 @@
|
||||
"id": "dWA516",
|
||||
"title": "Do you call that a hat?",
|
||||
"titleTranslate": "你把那个叫帽子吗?",
|
||||
"text": "Do you call that a hat? I said to my wife. \nYou neednt be so rude about it, my wife answered as she looked at herself in the mirror. \nI sat down on one of those modern chairs with holes in it and waited. We had been in the hat shop for half an hour and my wife was still in front of the mirror. \nWe mustnt buy things we dont need, I remarked suddenly. I regretted saying it almost at once. \nYou neednt have said that, my wife answered. I neednt remind you of that terrible tie you bought yesterday. \nI find it beautiful, I said. A man can never have too many ties. \nAnd a woman cant have too many hats, she answered. \nTen minutes later we walked out of the shop together. My wife was wearing a hat that looked like a lighthouse!",
|
||||
"textTranslate": "“你把那个叫帽子吗?”我对妻子说。 \n“你说话没必要这样不客气,”我的妻子边回答边照着镜子。 \n我坐在一个新式的满是网眼儿的椅子上,等待着。我们在这家帽店已经呆了半个小时了,而我的妻子仍在镜子面前。 \n“我们不应该买我们不需要的东西,”我突然发表意见说,但马上又后悔说了这话。 \n“你没必要这么说,”我妻子回答说,“我也不必提醒你昨天买的那条糟糕透了的领带。” \n“我觉得它好看,”我说,“男人有多少领带也不会嫌多。” \n“女人有多少帽子也不嫌多。”她回答。 \n10分钟以后,我们一道走出了商店。我妻子戴着一顶像灯塔一样的帽子。",
|
||||
"text": "Do you call that a hat? I said to my wife. \nYou neednt be so rude about it, my wife answered as she looked at herself in the mirror. \nI sat down on one of those modern chairs with holes in it and waited. \nWe had been in the hat shop for half an hour and my wife was still in front of the mirror. \nWe mustnt buy things we dont need, I remarked suddenly. \nI regretted saying it almost at once. \nYou neednt have said that, my wife answered. \nI neednt remind you of that terrible tie you bought yesterday. \nI find it beautiful, I said. \nA man can never have too many ties. \nAnd a woman cant have too many hats, she answered. \nTen minutes later we walked out of the shop together. \nMy wife was wearing a hat that looked like a lighthouse!",
|
||||
"textTranslate": "“你把那个叫帽子吗?”我对妻子说。 \n“你说话没必要这样不客气,”我的妻子边回答边照着镜子。 \n我坐在一个新式的满是网眼儿的椅子上,等待着。 \n我们在这家帽店已经呆了半个小时了,而我的妻子仍在镜子面前。 \n“我们不应该买我们不需要的东西,”我突然发表意见说, \n但马上又后悔说了这话。 \n“你没必要这么说,”我妻子回答说, \n“我也不必提醒你昨天买的那条糟糕透了的领带。” \n“我觉得它好看,”我说, \n“男人有多少领带也不会嫌多。” \n“女人有多少帽子也不嫌多。”她回答。 \n10分钟以后,我们一道走出了商店。 \n我妻子戴着一顶像灯塔一样的帽子。",
|
||||
"newWords": [],
|
||||
"textAllWords": [],
|
||||
"audioSrc": "",
|
||||
"lrcPosition": []
|
||||
"lrcPosition": [],
|
||||
"audioFileId": "TNJNgS"
|
||||
},
|
||||
{
|
||||
"id": "XrDkqH",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
62
src/pages/pc/article/components/ArticleAudio.vue
Normal file
62
src/pages/pc/article/components/ArticleAudio.vue
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
2、LRC 文件用于解析句子对应音频的位置,不一定准确,后续可自行修改
|
||||
</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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user