638638 >
639639 <div class="relative max-w-[92vw] max-h-[92vh] flex flex-col items-center" @click.stop>
640640 <video
641- v-if="previewLivePhotoVideoSrc && !previewHasLivePhotoVideoError"
641+ v-if="previewIsVideo"
642+ ref="previewVideoEl"
643+ :key="previewVideoKey"
644+ :src="previewVideoSrc"
645+ :poster="previewVideoPoster"
646+ class="max-w-[90vw] max-h-[70vh] object-contain"
647+ controls
648+ autoplay
649+ playsinline
650+ @error="onPreviewVideoError"
651+ ></video>
652+ <video
653+ v-else-if="previewLivePhotoVideoSrc && !previewHasLivePhotoVideoError"
642654 ref="previewLiveVideoEl"
643655 :src="previewLivePhotoVideoSrc"
644656 :poster="previewSrc"
651663 ></video>
652664 <img v-else :src="previewSrc" alt="预览" class="max-w-[90vw] max-h-[70vh] object-contain" />
653665
666+ <div
667+ v-if="previewIsVideo && previewVideoError"
668+ class="mt-3 text-xs text-red-200 whitespace-pre-wrap text-center max-w-[90vw]"
669+ >
670+ {{ previewVideoError }}
671+ </div>
672+
654673 </div>
655674
656675 <button
@@ -1756,6 +1775,53 @@ const previewSrc = computed(() => {
17561775 return getMediaPreviewSrc(ctx.post, ctx.media, ctx.idx)
17571776})
17581777
1778+ const previewVideoEl = ref(null)
1779+ const previewVideoMode = ref('') // 'local' | 'remote' | 'raw'
1780+ const previewVideoError = ref('')
1781+ const previewVideoTried = reactive({ local: false, remote: false, raw: false })
1782+
1783+ const resetPreviewVideo = () => {
1784+ previewVideoMode.value = ''
1785+ previewVideoError.value = ''
1786+ previewVideoTried.local = false
1787+ previewVideoTried.remote = false
1788+ previewVideoTried.raw = false
1789+ }
1790+
1791+ const previewIsVideo = computed(() => {
1792+ const ctx = previewCtx.value
1793+ if (!ctx) return false
1794+ return Number(ctx.media?.type || 0) === 6
1795+ })
1796+
1797+ const previewVideoPoster = computed(() => {
1798+ const ctx = previewCtx.value
1799+ if (!ctx) return ''
1800+ if (Number(ctx.media?.type || 0) !== 6) return ''
1801+ return getMediaThumbSrc(ctx.post, ctx.media, ctx.idx) || ''
1802+ })
1803+
1804+ const previewVideoSrc = computed(() => {
1805+ const ctx = previewCtx.value
1806+ if (!ctx) return ''
1807+ if (Number(ctx.media?.type || 0) !== 6) return ''
1808+
1809+ const local = getSnsVideoUrl(ctx.post?.id, ctx.media?.id)
1810+ const remote = getSnsRemoteVideoSrc(ctx.post, ctx.media)
1811+ const raw = upgradeTencentHttps(String(ctx.media?.url || '').trim())
1812+
1813+ const mode = String(previewVideoMode.value || '').toLowerCase()
1814+ if (mode === 'local') return local
1815+ if (mode === 'remote') return remote
1816+ if (mode === 'raw') return raw
1817+ return local || remote || raw || ''
1818+ })
1819+
1820+ const previewVideoKey = computed(() => {
1821+ if (!previewIsVideo.value) return ''
1822+ return `${String(previewVideoMode.value || '')}:${String(previewVideoSrc.value || '')}`
1823+ })
1824+
17591825const previewLivePhotoVideoSrc = computed(() => {
17601826 const ctx = previewCtx.value
17611827 if (!ctx) return ''
@@ -1879,6 +1945,7 @@ const loadPreviewCandidates = async ({ reset }) => {
18791945
18801946const openImagePreview = async (post, m, idx = 0) => {
18811947 if (!process.client) return
1948+ resetPreviewVideo()
18821949 // Stop any background hover-playing live photo when opening the preview.
18831950 activeLivePhotoKey.value = ''
18841951 // Preview is an intentional action; allow retry even if hover playback failed once.
@@ -1898,11 +1965,58 @@ const openImagePreview = async (post, m, idx = 0) => {
18981965 await loadPreviewCandidates({ reset: true })
18991966}
19001967
1968+ const openVideoPreview = (post, m, idx = 0) => {
1969+ if (!process.client) return
1970+ resetPreviewVideo()
1971+ activeLivePhotoKey.value = ''
1972+
1973+ const local = getSnsVideoUrl(post?.id, m?.id)
1974+ const remote = getSnsRemoteVideoSrc(post, m)
1975+ const raw = upgradeTencentHttps(String(m?.url || '').trim())
1976+
1977+ if (local) previewVideoMode.value = 'local'
1978+ else if (remote) previewVideoMode.value = 'remote'
1979+ else if (raw) previewVideoMode.value = 'raw'
1980+ else previewVideoError.value = '视频地址缺失。'
1981+
1982+ previewCtx.value = { post, media: m, idx: Number(idx) || 0 }
1983+ previewCandidatesOpen.value = false
1984+ resetPreviewCandidates()
1985+ document.body.style.overflow = 'hidden'
1986+ }
1987+
1988+ const onPreviewVideoError = () => {
1989+ const ctx = previewCtx.value
1990+ if (!ctx) return
1991+ if (Number(ctx.media?.type || 0) !== 6) return
1992+
1993+ const current = String(previewVideoMode.value || '').toLowerCase()
1994+ if (current === 'local') previewVideoTried.local = true
1995+ if (current === 'remote') previewVideoTried.remote = true
1996+ if (current === 'raw') previewVideoTried.raw = true
1997+
1998+ // Fallback order: local -> remote -> raw
1999+ const remote = getSnsRemoteVideoSrc(ctx.post, ctx.media)
2000+ if (!previewVideoTried.remote && remote) {
2001+ previewVideoMode.value = 'remote'
2002+ return
2003+ }
2004+
2005+ const raw = upgradeTencentHttps(String(ctx.media?.url || '').trim())
2006+ if (!previewVideoTried.raw && raw) {
2007+ previewVideoMode.value = 'raw'
2008+ return
2009+ }
2010+
2011+ previewVideoError.value = '视频加载失败:可能是本地缓存不存在,或远程下载/解密失败。'
2012+ }
2013+
19012014const closeImagePreview = () => {
19022015 if (!process.client) return
19032016 previewCtx.value = null
19042017 previewCandidatesOpen.value = false
19052018 resetPreviewCandidates()
2019+ resetPreviewVideo()
19062020 document.body.style.overflow = ''
19072021}
19082022
@@ -1912,16 +2026,7 @@ const onMediaClick = (post, m, idx = 0) => {
19122026
19132027 // 视频点击逻辑
19142028 if (mt === 6) {
1915- // Open a playable mp4 via backend (downloads+decrypts as needed).
1916- const remoteUrl = getSnsRemoteVideoSrc(post, m)
1917- if (remoteUrl) {
1918- window.open(remoteUrl, '_blank', 'noopener,noreferrer')
1919- return
1920- }
1921-
1922- // Last-resort: open raw CDN url.
1923- const u = String(m?.url || '').trim()
1924- if (u) window.open(u, '_blank', 'noopener,noreferrer')
2029+ openVideoPreview(post, m, idx)
19252030 return
19262031 }
19272032
0 commit comments