fix(short-video): prevent scroll jank on mobile views and persist sound toggle state

pull/805/head
wuqinchuan 2025-05-25 21:56:52 +08:00 committed by zanllp
parent dcd4d62c0f
commit 4f999abca5
1 changed files with 249 additions and 16 deletions

View File

@ -3,6 +3,7 @@ import { ref, computed, onMounted, onUnmounted, nextTick, watch } from 'vue'
import { useTiktokStore, type TiktokMediaItem } from '@/store/useTiktokStore'
import { useTagStore } from '@/store/useTagStore'
import { useGlobalStore } from '@/store/useGlobalStore'
import { useLocalStorage } from '@vueuse/core'
import { isVideoFile } from '@/util'
import { openAddNewTagModal } from '@/components/functionalCallableComp'
import { toggleCustomTagToImg } from '@/api/db'
@ -13,7 +14,11 @@ import {
FullscreenExitOutlined,
UpOutlined,
DownOutlined,
TagsOutlined
TagsOutlined,
SoundOutlined,
SoundFilled,
HeartOutlined,
HeartFilled
} from '@/icon'
import { t } from '@/i18n'
import type { StyleValue } from 'vue'
@ -24,6 +29,9 @@ const tiktokStore = useTiktokStore()
const tagStore = useTagStore()
const global = useGlobalStore()
// 使 @vueuse
const isMuted = useLocalStorage('tiktok-viewer-muted', true) //
//
const isMac = computed(() => {
return /Mac|iPhone|iPad|iPod/.test(navigator.userAgent) ||
@ -87,7 +95,7 @@ const controlVideoPlayback = async () => {
if (index === 1) {
//
video.currentTime = 0 //
video.muted = true //
video.muted = isMuted.value //
await video.play()
} else {
//
@ -126,6 +134,21 @@ const isTagSelected = (tagId: string | number) => {
return !!tagStore.tagMap.get(fullpath)?.some(v => v.id === tagId)
}
// Like
const likeTag = computed(() => {
return global.conf?.all_custom_tags?.find(v => v.type === 'custom' && v.name === 'like')
})
const isLiked = computed(() => {
if (!likeTag.value) return false
return isTagSelected(likeTag.value.id)
})
const toggleLike = async () => {
if (!likeTag.value) return
await onTagClick(likeTag.value.id)
}
const onTagClick = async (tagId: string | number) => {
const currentUrl = currentItem.value?.url
if (!currentUrl) return
@ -165,15 +188,31 @@ const goToPrev = (isTriggerByTouch: boolean = false) => {
if (isAnimating.value || !tiktokStore.hasPrev) return
isAnimating.value = true
//
dragOffset.value = 0
bufferTransform.value = 100 //
setTimeout(() => {
tiktokStore.prev()
updateBuffer()
bufferTransform.value = 0
setTimeout(() => {
isAnimating.value = false
}, getAnimationDelay(isTriggerByTouch)) // Mac
//
nextTick(() => {
//
if (bufferTransform.value !== 0) {
bufferTransform.value = 0
}
if (dragOffset.value !== 0) {
dragOffset.value = 0
}
setTimeout(() => {
isAnimating.value = false
}, getAnimationDelay(isTriggerByTouch))
})
}, 200) //
}
@ -182,15 +221,31 @@ const goToNext = (isTriggerByTouch: boolean = false) => {
if (isAnimating.value || !tiktokStore.hasNext) return
isAnimating.value = true
//
dragOffset.value = 0
bufferTransform.value = -100 //
setTimeout(() => {
tiktokStore.next()
updateBuffer()
bufferTransform.value = 0
setTimeout(() => {
isAnimating.value = false
}, getAnimationDelay(isTriggerByTouch)) // Mac
//
nextTick(() => {
//
if (bufferTransform.value !== 0) {
bufferTransform.value = 0
}
if (dragOffset.value !== 0) {
dragOffset.value = 0
}
setTimeout(() => {
isAnimating.value = false
}, getAnimationDelay(isTriggerByTouch))
})
}, 200) //
}
@ -203,6 +258,11 @@ const handleTouchStart = (e: TouchEvent) => {
touchCurrentY.value = e.touches[0].clientY
isDragging.value = true
dragOffset.value = 0
// transform
if (bufferTransform.value !== 0) {
bufferTransform.value = 0
}
}
const handleTouchMove = (e: TouchEvent) => {
@ -220,26 +280,98 @@ const handleTouchMove = (e: TouchEvent) => {
}
const handleTouchEnd = () => {
if (!isDragging.value || isAnimating.value) return
if (!isDragging.value) return
isDragging.value = false
const deltaY = touchCurrentY.value - touchStartY.value
const threshold = 80 //
//
isDragging.value = false
if (isAnimating.value) {
//
dragOffset.value = 0
return
}
if (Math.abs(deltaY) > threshold) {
if (deltaY > 0 && tiktokStore.hasPrev) {
//
goToPrev()
goToPrev(true)
} else if (deltaY < 0 && tiktokStore.hasNext) {
//
goToNext()
goToNext(true)
} else {
//
dragOffset.value = 0
// -
resetToCenter()
}
} else {
//
// -
resetToCenter()
}
}
//
const handleTouchCancel = () => {
if (!isDragging.value) return
isDragging.value = false
if (!isAnimating.value) {
resetToCenter()
}
}
//
const resetToCenter = () => {
if (isAnimating.value) return
isAnimating.value = true
dragOffset.value = 0
// bufferTransform
bufferTransform.value = 0
setTimeout(() => {
isAnimating.value = false
}, 300) // CSS
}
//
const fixMisalignment = () => {
if (isAnimating.value || isDragging.value) return
//
if (bufferTransform.value !== 0 || dragOffset.value !== 0) {
console.warn('检测到错位,正在修复...', {
bufferTransform: bufferTransform.value,
dragOffset: dragOffset.value
})
//
bufferTransform.value = 0
dragOffset.value = 0
// buffer
updateBuffer()
}
}
//
let alignmentCheckInterval: number | null = null
const startAlignmentCheck = () => {
if (!tiktokStore.isMobile) return
alignmentCheckInterval = window.setInterval(() => {
fixMisalignment()
}, 1000) //
}
const stopAlignmentCheck = () => {
if (alignmentCheckInterval) {
clearInterval(alignmentCheckInterval)
alignmentCheckInterval = null
}
}
@ -278,6 +410,11 @@ const handleKeydown = (e: KeyboardEvent) => {
e.preventDefault()
handleFullscreenToggle()
break
case 'l':
case 'L':
e.preventDefault()
if (likeTag.value) toggleLike()
break
}
}
@ -314,6 +451,17 @@ const exitFullscreen = async () => {
}
}
//
const toggleMute = () => {
isMuted.value = !isMuted.value
//
const currentVideo = videoRefs.value[1]
if (currentVideo) {
currentVideo.muted = isMuted.value
}
}
//
const handleFullscreenChange = () => {
tiktokStore.isFullscreen = !!document.fullscreenElement
@ -351,12 +499,18 @@ onMounted(() => {
document.addEventListener('keydown', handleKeydown)
document.addEventListener('fullscreenchange', handleFullscreenChange)
updateBuffer()
//
startAlignmentCheck()
})
onUnmounted(() => {
document.removeEventListener('keydown', handleKeydown)
document.removeEventListener('fullscreenchange', handleFullscreenChange)
//
stopAlignmentCheck()
//
videoRefs.value.forEach(video => {
if (video) {
@ -389,6 +543,9 @@ watch(() => tiktokStore.visible, (visible) => {
}
})
//
stopAlignmentCheck()
// 退
if (document.fullscreenElement) {
exitFullscreen()
@ -398,8 +555,25 @@ watch(() => tiktokStore.visible, (visible) => {
nextTick(() => {
controlVideoPlayback()
})
//
startAlignmentCheck()
//
nextTick(() => {
fixMisalignment()
})
}
})
//
watch(() => isMuted.value, (muted) => {
videoRefs.value.forEach(video => {
if (video) {
video.muted = muted
}
})
})
</script>
<template>
@ -411,11 +585,15 @@ watch(() => tiktokStore.visible, (visible) => {
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
@touchcancel="handleTouchCancel"
@wheel="handleWheel"
>
<!-- 媒体内容区域 -->
<div ref="viewportRef" class="tiktok-viewport">
<!-- 3位buffer渲染 -->
<div
v-for="(item, index) in bufferItems"
:key="item?.id || `empty-${index}`"
@ -429,7 +607,6 @@ watch(() => tiktokStore.visible, (visible) => {
class="tiktok-media"
:src="item.url"
:controls="index === 1"
muted
:loop="index === 1"
playsinline
preload="metadata"
@ -467,6 +644,28 @@ watch(() => tiktokStore.visible, (visible) => {
<FullscreenOutlined v-else />
</button>
<!-- 声音切换按钮 -->
<button
class="control-btn sound-btn"
@click="toggleMute"
:title="isMuted ? '开启声音' : '关闭声音'"
>
<SoundFilled v-if="!isMuted" />
<SoundOutlined v-else />
</button>
<!-- Like 按钮 -->
<button
v-if="likeTag"
class="control-btn like-btn"
:class="{ 'like-active': isLiked }"
@click="toggleLike"
:title="isLiked ? '取消喜欢' : '喜欢'"
>
<HeartFilled v-if="isLiked" />
<HeartOutlined v-else />
</button>
<!-- TAG 按钮 -->
<button
class="control-btn tags-btn"
@ -488,6 +687,16 @@ watch(() => tiktokStore.visible, (visible) => {
<UpOutlined />
</div>
<!-- 修复错位按钮仅移动设备显示 -->
<!-- <div
v-if="tiktokStore.isMobile"
class="nav-indicator nav-fix"
@click="fixMisalignment"
title="修复错位"
>
<span style="font-size: 12px;">修复</span>
</div> -->
<!-- 下一个指示器 -->
<div
v-if="tiktokStore.hasNext"
@ -652,6 +861,22 @@ watch(() => tiktokStore.visible, (visible) => {
&:active {
transform: scale(0.95);
}
&.like-btn {
&.like-active {
background: rgba(255, 20, 147, 0.3); //
color: #ff1493; //
&:hover {
background: rgba(255, 20, 147, 0.5);
transform: scale(1.15); //
}
}
&:not(.like-active):hover {
color: #ff69b4; //
}
}
}
.tiktok-navigation {
@ -682,6 +907,14 @@ watch(() => tiktokStore.visible, (visible) => {
background: rgba(255, 255, 255, 0.5);
transform: scale(1.1);
}
&.nav-fix {
background: rgba(255, 165, 0, 0.3); //
&:hover {
background: rgba(255, 165, 0, 0.5);
}
}
}
.tiktok-progress {