modified: src/components/TTSPlayer.vue
This commit is contained in:
@@ -5,23 +5,28 @@
|
|||||||
<div class="tts-player__inner">
|
<div class="tts-player__inner">
|
||||||
<div class="tts-player__controls">
|
<div class="tts-player__controls">
|
||||||
<button class="tts-player__btn" @click="skipBackward" title="快退5秒">
|
<button class="tts-player__btn" @click="skipBackward" title="快退5秒">
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path d="M1 4v6h6"/><path d="M11 12a7 7 0 0 0-7-7"/><path d="M11 12l-3-3"/>
|
<path d="M1 4v6h6"/>
|
||||||
<text x="14" y="16" font-size="8" fill="currentColor" stroke="none">5</text>
|
<path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/>
|
||||||
|
<text x="12" y="15" font-size="8" font-weight="600" fill="currentColor" stroke="none" text-anchor="middle">5</text>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button class="tts-player__btn tts-player__btn--primary" @click="togglePlay" :title="isPlaying ? '暂停' : '播放'">
|
<button class="tts-player__btn tts-player__btn--primary" @click="togglePlay" :title="isPlaying ? '暂停' : '播放'">
|
||||||
<svg v-if="!isPlaying" width="20" height="20" viewBox="0 0 24 24" fill="currentColor" stroke="none">
|
<Transition name="icon-fade" mode="out-in">
|
||||||
|
<svg v-if="!isPlaying" key="play" width="20" height="20" viewBox="0 0 24 24" fill="currentColor" stroke="none">
|
||||||
<polygon points="5 3 19 12 5 21 5 3"/>
|
<polygon points="5 3 19 12 5 21 5 3"/>
|
||||||
</svg>
|
</svg>
|
||||||
<svg v-else width="20" height="20" viewBox="0 0 24 24" fill="currentColor" stroke="none">
|
<svg v-else key="pause" width="20" height="20" viewBox="0 0 24 24" fill="currentColor" stroke="none">
|
||||||
<rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/>
|
<rect x="6" y="4" width="4" height="16" rx="1"/>
|
||||||
|
<rect x="14" y="4" width="4" height="16" rx="1"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
</Transition>
|
||||||
</button>
|
</button>
|
||||||
<button class="tts-player__btn" @click="skipForward" title="快进5秒">
|
<button class="tts-player__btn" @click="skipForward" title="快进5秒">
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path d="M23 4v6h-6"/><path d="M13 12a7 7 0 0 1 7-7"/><path d="M13 12l3-3"/>
|
<path d="M23 4v6h-6"/>
|
||||||
<text x="4" y="16" font-size="8" fill="currentColor" stroke="none">5</text>
|
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>
|
||||||
|
<text x="12" y="15" font-size="8" font-weight="600" fill="currentColor" stroke="none" text-anchor="middle">5</text>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -75,7 +80,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<audio ref="audioRef" :src="audioSrc" @timeupdate="onTimeUpdate" @ended="onEnded" @loadedmetadata="onLoadedMetadata"></audio>
|
<audio ref="audioRef" :src="audioSrc" @timeupdate="onTimeUpdate" @ended="onEnded" @loadedmetadata="onLoadedMetadata" @play="onPlay" @pause="onPause"></audio>
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
@@ -135,8 +140,11 @@ function togglePlay() {
|
|||||||
if (!audio) return
|
if (!audio) return
|
||||||
if (isPlaying.value) {
|
if (isPlaying.value) {
|
||||||
audio.pause()
|
audio.pause()
|
||||||
|
isPlaying.value = false
|
||||||
} else {
|
} else {
|
||||||
audio.play()
|
audio.play().then(() => {
|
||||||
|
isPlaying.value = true
|
||||||
|
}).catch(() => {})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,7 +195,11 @@ function onLoadedMetadata() {
|
|||||||
duration.value = audio.duration || (props.durationMs / 1000)
|
duration.value = audio.duration || (props.durationMs / 1000)
|
||||||
audio.volume = volume.value
|
audio.volume = volume.value
|
||||||
audio.playbackRate = playbackRate.value
|
audio.playbackRate = playbackRate.value
|
||||||
audio.play()
|
audio.play().then(() => {
|
||||||
|
isPlaying.value = true
|
||||||
|
}).catch(() => {
|
||||||
|
isPlaying.value = false
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function onEnded() {
|
function onEnded() {
|
||||||
@@ -195,15 +207,14 @@ function onEnded() {
|
|||||||
currentTime.value = 0
|
currentTime.value = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(isPlaying, (val) => {
|
// 监听音频实际播放状态(用户可能通过其他方式暂停)
|
||||||
const audio = audioRef.value
|
function onPlay() {
|
||||||
if (!audio) return
|
isPlaying.value = true
|
||||||
if (val) {
|
}
|
||||||
audio.play().catch(() => { isPlaying.value = false })
|
|
||||||
} else {
|
function onPause() {
|
||||||
audio.pause()
|
isPlaying.value = false
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
|
||||||
watch(volume, (val) => {
|
watch(volume, (val) => {
|
||||||
if (audioRef.value) {
|
if (audioRef.value) {
|
||||||
@@ -220,7 +231,8 @@ watch(playbackRate, (val) => {
|
|||||||
|
|
||||||
watch(() => props.visible, (val) => {
|
watch(() => props.visible, (val) => {
|
||||||
if (val) {
|
if (val) {
|
||||||
isPlaying.value = true
|
// 打开播放器时重置状态,等待音频加载完成后才真正播放
|
||||||
|
isPlaying.value = false
|
||||||
currentTime.value = 0
|
currentTime.value = 0
|
||||||
} else {
|
} else {
|
||||||
if (audioRef.value) {
|
if (audioRef.value) {
|
||||||
@@ -245,16 +257,19 @@ onUnmounted(() => {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.tts-player {
|
.tts-player {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 0;
|
bottom: 16px;
|
||||||
left: 0;
|
left: 50%;
|
||||||
right: 0;
|
transform: translateX(-50%);
|
||||||
z-index: 99999;
|
z-index: 99999;
|
||||||
padding: 8px 16px;
|
max-width: 720px;
|
||||||
|
width: calc(100% - 32px);
|
||||||
|
padding: 12px 16px;
|
||||||
background: var(--panel-bg);
|
background: var(--panel-bg);
|
||||||
backdrop-filter: blur(16px);
|
backdrop-filter: blur(20px);
|
||||||
-webkit-backdrop-filter: blur(16px);
|
-webkit-backdrop-filter: blur(20px);
|
||||||
border-top: 1px solid var(--panel-border);
|
border: 1px solid var(--panel-border);
|
||||||
box-shadow: 0 -4px 16px rgba(0, 0, 0, 0.1);
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15), 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tts-player__inner {
|
.tts-player__inner {
|
||||||
@@ -295,12 +310,18 @@ onUnmounted(() => {
|
|||||||
height: 36px;
|
height: 36px;
|
||||||
background: var(--btn-hover-bg);
|
background: var(--btn-hover-bg);
|
||||||
color: var(--btn-hover-fg);
|
color: var(--btn-hover-fg);
|
||||||
|
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tts-player__btn--primary:hover {
|
.tts-player__btn--primary:hover {
|
||||||
|
transform: scale(1.08);
|
||||||
filter: brightness(1.1);
|
filter: brightness(1.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tts-player__btn--primary:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
.tts-player__btn--close:hover {
|
.tts-player__btn--close:hover {
|
||||||
background: rgba(220, 38, 38, 0.15);
|
background: rgba(220, 38, 38, 0.15);
|
||||||
color: var(--danger-text);
|
color: var(--danger-text);
|
||||||
@@ -421,12 +442,23 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
.tts-player-slide-enter-active,
|
.tts-player-slide-enter-active,
|
||||||
.tts-player-slide-leave-active {
|
.tts-player-slide-leave-active {
|
||||||
transition: transform 0.25s ease, opacity 0.2s ease;
|
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.25s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tts-player-slide-enter-from,
|
.tts-player-slide-enter-from,
|
||||||
.tts-player-slide-leave-to {
|
.tts-player-slide-leave-to {
|
||||||
transform: translateY(100%);
|
transform: translateX(-50%) translateY(24px);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon-fade-enter-active,
|
||||||
|
.icon-fade-leave-active {
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-fade-enter-from,
|
||||||
|
.icon-fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.7);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user