feat: align with upstream 1.5.9.9
This commit is contained in:
@@ -0,0 +1,340 @@
|
||||
<template>
|
||||
<div
|
||||
class="volume-control-container"
|
||||
@mouseenter="handleMouseEnter"
|
||||
@mouseleave="handleMouseLeave"
|
||||
@wheel.prevent="handleWheel"
|
||||
>
|
||||
<!-- 弹出的音量条 (仅 PC 端 hover 或移动端点击时显示) -->
|
||||
<Transition name="volume-slide">
|
||||
<div v-show="showSlider" class="volume-slider-popup">
|
||||
<div
|
||||
ref="sliderRef"
|
||||
class="volume-slider"
|
||||
@mousedown="startDrag"
|
||||
@touchstart.prevent="startTouchDrag"
|
||||
@click="handleSliderClick"
|
||||
>
|
||||
<div class="volume-slider-bg">
|
||||
<div
|
||||
class="volume-slider-fill"
|
||||
:style="{ height: `${volumePercentage}%` }"
|
||||
/>
|
||||
<div
|
||||
class="volume-slider-thumb"
|
||||
:style="{ bottom: `${volumePercentage}%` }"
|
||||
:class="{ dragging: isDragging }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<!-- 底部按钮区 -->
|
||||
<div
|
||||
class="volume-btn"
|
||||
:class="{ active: showSlider }"
|
||||
@click="toggleMuteOrShowSlider"
|
||||
>
|
||||
<!-- 显示百分比或图标 -->
|
||||
<Transition name="fade-icon" mode="out-in">
|
||||
<span v-if="showSlider" class="volume-text sf-pro">
|
||||
{{ Math.round(volumePercentage) }}%
|
||||
</span>
|
||||
<Icon
|
||||
v-else
|
||||
:name="volumeIcon"
|
||||
:size="20"
|
||||
:class="{ 'muted-icon': isMuted }"
|
||||
/>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import Icon from '../Icon.vue'
|
||||
import { useAudioPlayerControl } from '~/composables/useAudioPlayerControl'
|
||||
|
||||
const control = useAudioPlayerControl()
|
||||
|
||||
const isMobile = ref(false)
|
||||
const showSlider = ref(false)
|
||||
const sliderRef = ref<HTMLElement | null>(null)
|
||||
const isDragging = ref(false)
|
||||
const hideTimer = ref<number | null>(null)
|
||||
|
||||
const volumePercentage = computed(() => {
|
||||
return (control.volume.value || 0) * 100
|
||||
})
|
||||
|
||||
const isMuted = computed(() => {
|
||||
return control.isMuted.value || control.volume.value === 0
|
||||
})
|
||||
|
||||
const volumeIcon = computed(() => {
|
||||
if (isMuted.value || control.volume.value === 0) return 'volume-mute'
|
||||
return 'volume'
|
||||
})
|
||||
|
||||
const checkMobile = () => {
|
||||
if (import.meta.client) {
|
||||
isMobile.value = window.innerWidth <= 768
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
checkMobile()
|
||||
window.addEventListener('resize', checkMobile)
|
||||
document.addEventListener('click', handleClickOutside)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', checkMobile)
|
||||
document.removeEventListener('click', handleClickOutside)
|
||||
if (hideTimer.value !== null) {
|
||||
clearTimeout(hideTimer.value)
|
||||
}
|
||||
})
|
||||
|
||||
// PC端 Hover 效果
|
||||
const handleMouseEnter = () => {
|
||||
if (isMobile.value || isDragging.value) return
|
||||
if (hideTimer.value !== null) clearTimeout(hideTimer.value)
|
||||
showSlider.value = true
|
||||
}
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
if (isMobile.value || isDragging.value) return
|
||||
hideTimer.value = window.setTimeout(() => {
|
||||
showSlider.value = false
|
||||
}, 300)
|
||||
}
|
||||
|
||||
// 移动端点击处理
|
||||
const toggleMuteOrShowSlider = (e: Event) => {
|
||||
if (isMobile.value) {
|
||||
e.stopPropagation()
|
||||
showSlider.value = !showSlider.value
|
||||
} else {
|
||||
// PC端点击直接静音/取消静音
|
||||
control.toggleMute()
|
||||
}
|
||||
}
|
||||
|
||||
// 滚轮控制音量
|
||||
const handleWheel = (e: WheelEvent) => {
|
||||
if (!showSlider.value && !isMobile.value) return
|
||||
// 向上滚增加音量,向下滚减小音量
|
||||
const delta = e.deltaY > 0 ? -0.05 : 0.05
|
||||
control.setVolume(control.volume.value + delta)
|
||||
}
|
||||
|
||||
// 拖拽控制
|
||||
const updateVolumeFromEvent = (clientY: number) => {
|
||||
if (!sliderRef.value) return
|
||||
const rect = sliderRef.value.getBoundingClientRect()
|
||||
// 计算相对于底部的百分比
|
||||
const y = Math.max(0, Math.min(rect.height, rect.bottom - clientY))
|
||||
const percentage = y / rect.height
|
||||
control.setVolume(percentage)
|
||||
}
|
||||
|
||||
const handleSliderClick = (e: MouseEvent) => {
|
||||
updateVolumeFromEvent(e.clientY)
|
||||
}
|
||||
|
||||
const startDrag = (e: MouseEvent) => {
|
||||
isDragging.value = true
|
||||
updateVolumeFromEvent(e.clientY)
|
||||
|
||||
const onMouseMove = (e: MouseEvent) => {
|
||||
updateVolumeFromEvent(e.clientY)
|
||||
}
|
||||
|
||||
const onMouseUp = () => {
|
||||
isDragging.value = false
|
||||
document.removeEventListener('mousemove', onMouseMove)
|
||||
document.removeEventListener('mouseup', onMouseUp)
|
||||
// 如果鼠标已经离开区域,隐藏 slider
|
||||
if (!isMobile.value) {
|
||||
hideTimer.value = window.setTimeout(() => {
|
||||
showSlider.value = false
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('mousemove', onMouseMove)
|
||||
document.addEventListener('mouseup', onMouseUp)
|
||||
}
|
||||
|
||||
const startTouchDrag = (e: TouchEvent) => {
|
||||
isDragging.value = true
|
||||
updateVolumeFromEvent(e.touches[0].clientY)
|
||||
|
||||
const onTouchMove = (e: TouchEvent) => {
|
||||
updateVolumeFromEvent(e.touches[0].clientY)
|
||||
}
|
||||
|
||||
const onTouchEnd = () => {
|
||||
isDragging.value = false
|
||||
document.removeEventListener('touchmove', onTouchMove)
|
||||
document.removeEventListener('touchend', onTouchEnd)
|
||||
}
|
||||
|
||||
document.addEventListener('touchmove', onTouchMove, { passive: false })
|
||||
document.addEventListener('touchend', onTouchEnd)
|
||||
}
|
||||
|
||||
const handleClickOutside = (e: Event) => {
|
||||
if (isMobile.value && showSlider.value) {
|
||||
const target = e.target as HTMLElement
|
||||
if (!target.closest('.volume-control-container')) {
|
||||
showSlider.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.volume-control-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 39px;
|
||||
height: 39px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
/* 底部按钮 */
|
||||
.volume-btn {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--text-primary-60);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
border-radius: 50%;
|
||||
background: transparent;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.volume-btn:hover {
|
||||
color: var(--text-primary);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.volume-btn.active {
|
||||
color: var(--text-primary);
|
||||
background: var(--overlay-10);
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.muted-icon {
|
||||
color: var(--volume-control-muted-color);
|
||||
}
|
||||
|
||||
.volume-text {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
/* 图标/文本切换动画 */
|
||||
.fade-icon-enter-active,
|
||||
.fade-icon-leave-active {
|
||||
transition: opacity 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
.fade-icon-enter-from,
|
||||
.fade-icon-leave-to {
|
||||
opacity: 0;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
|
||||
/* 弹出层 */
|
||||
.volume-slider-popup {
|
||||
position: absolute;
|
||||
bottom: calc(100% + 8px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 36px;
|
||||
height: 120px;
|
||||
background: linear-gradient(135deg, var(--overlay-15), var(--overlay-5)),
|
||||
var(--volume-control-panel-bg);
|
||||
backdrop-filter: blur(20px) saturate(1.8);
|
||||
-webkit-backdrop-filter: blur(20px) saturate(1.8);
|
||||
border: 1px solid var(--overlay-10);
|
||||
border-radius: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 10px 30px var(--mask-30), inset 0 1px 0 var(--overlay-20);
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
/* 弹出动画 */
|
||||
.volume-slide-enter-active {
|
||||
transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
transform-origin: bottom center;
|
||||
}
|
||||
.volume-slide-leave-active {
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transform-origin: bottom center;
|
||||
}
|
||||
.volume-slide-enter-from,
|
||||
.volume-slide-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(-50%) translateY(10px) scale(0.8);
|
||||
}
|
||||
|
||||
/* 滑动条容器 */
|
||||
.volume-slider {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.volume-slider-bg {
|
||||
position: relative;
|
||||
width: 6px;
|
||||
height: 100%;
|
||||
background: var(--overlay-20);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.volume-slider-fill {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
background: linear-gradient(to top, var(--color-accent-light), var(--color-cyan));
|
||||
border-radius: 3px;
|
||||
transition: height 0.1s linear;
|
||||
}
|
||||
|
||||
.volume-slider-thumb {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 50%);
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background: var(--text-primary);
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 2px 6px var(--mask-30);
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.volume-slider-thumb.dragging,
|
||||
.volume-slider:hover .volume-slider-thumb {
|
||||
transform: translate(-50%, 50%) scale(1.2);
|
||||
box-shadow: 0 4px 10px var(--mask-40);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user