mirror of
https://github.com/LifeArchiveProject/WeChatDataAnalysis.git
synced 2026-06-18 15:54:08 +08:00
Compare commits
1 Commits
@@ -589,17 +589,16 @@
|
||||
:class="message.isSent ? 'bg-[#95EC69] text-black bubble-tail-r' : 'bg-white text-gray-800 bubble-tail-l'">
|
||||
{{ message.content }}
|
||||
</div>
|
||||
<a
|
||||
<button
|
||||
v-if="message.videoThumbUrl && message.videoUrl"
|
||||
:href="message.videoUrl"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
type="button"
|
||||
class="absolute inset-0 flex items-center justify-center"
|
||||
@click.stop="openVideoPreview(message.videoUrl, message.videoThumbUrl)"
|
||||
>
|
||||
<div class="w-12 h-12 rounded-full bg-black/45 flex items-center justify-center">
|
||||
<svg class="w-6 h-6 text-white" fill="currentColor" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>
|
||||
</div>
|
||||
</a>
|
||||
</button>
|
||||
<div class="absolute inset-0 flex items-center justify-center" v-else-if="message.videoThumbUrl">
|
||||
<div class="w-12 h-12 rounded-full bg-black/45 flex items-center justify-center">
|
||||
<svg class="w-6 h-6 text-white" fill="currentColor" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>
|
||||
@@ -1388,6 +1387,40 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 视频预览弹窗 (全局固定定位) -->
|
||||
<div
|
||||
v-if="previewVideoUrl"
|
||||
class="fixed inset-0 z-[13000] bg-black/90 flex items-center justify-center"
|
||||
@click="closeVideoPreview"
|
||||
>
|
||||
<div class="relative max-w-[92vw] max-h-[92vh] flex flex-col items-center" @click.stop>
|
||||
<video
|
||||
:key="previewVideoUrl"
|
||||
:src="previewVideoUrl"
|
||||
:poster="previewVideoPosterUrl"
|
||||
class="max-w-[90vw] max-h-[90vh] object-contain"
|
||||
controls
|
||||
autoplay
|
||||
playsinline
|
||||
@error="onPreviewVideoError"
|
||||
></video>
|
||||
<div
|
||||
v-if="previewVideoError"
|
||||
class="mt-3 text-xs text-red-200 whitespace-pre-wrap text-center max-w-[90vw]"
|
||||
>
|
||||
{{ previewVideoError }}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="absolute top-4 right-4 text-white/80 hover:text-white p-2 rounded-full bg-black/30 hover:bg-black/50 transition-colors"
|
||||
@click.stop="closeVideoPreview"
|
||||
>
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 浮动窗口(可拖动):合并消息 / 链接卡片 -->
|
||||
<div
|
||||
v-for="win in floatingWindows"
|
||||
@@ -1561,17 +1594,16 @@
|
||||
@error="onChatHistoryVideoThumbError(rec)"
|
||||
/>
|
||||
<div v-else class="px-3 py-2 text-sm text-gray-700">{{ rec.content || '[视频]' }}</div>
|
||||
<a
|
||||
<button
|
||||
v-if="rec.videoUrl"
|
||||
:href="rec.videoUrl"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
type="button"
|
||||
class="absolute inset-0 flex items-center justify-center"
|
||||
@click.stop="openVideoPreview(rec.videoUrl, rec.videoThumbUrl)"
|
||||
>
|
||||
<div class="w-12 h-12 rounded-full bg-black/45 flex items-center justify-center">
|
||||
<svg class="w-6 h-6 text-white" fill="currentColor" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>
|
||||
</div>
|
||||
</a>
|
||||
</button>
|
||||
<div
|
||||
v-if="rec.videoDuration"
|
||||
class="absolute bottom-2 right-2 text-xs text-white bg-black/55 px-1.5 py-0.5 rounded"
|
||||
@@ -1814,17 +1846,16 @@
|
||||
/>
|
||||
<div v-else class="px-3 py-2 text-sm text-gray-700">{{ rec.content || '[视频]' }}</div>
|
||||
|
||||
<a
|
||||
<button
|
||||
v-if="rec.videoThumbUrl && rec.videoUrl"
|
||||
:href="rec.videoUrl"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
type="button"
|
||||
class="absolute inset-0 flex items-center justify-center"
|
||||
@click.stop="openVideoPreview(rec.videoUrl, rec.videoThumbUrl)"
|
||||
>
|
||||
<div class="w-12 h-12 rounded-full bg-black/45 flex items-center justify-center">
|
||||
<svg class="w-6 h-6 text-white" fill="currentColor" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>
|
||||
</div>
|
||||
</a>
|
||||
</button>
|
||||
<div class="absolute inset-0 flex items-center justify-center" v-else-if="rec.videoThumbUrl">
|
||||
<div class="w-12 h-12 rounded-full bg-black/45 flex items-center justify-center">
|
||||
<svg class="w-6 h-6 text-white" fill="currentColor" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>
|
||||
@@ -4070,15 +4101,42 @@ const scrollToMessageId = async (id) => {
|
||||
|
||||
// 图片预览状态
|
||||
const previewImageUrl = ref(null)
|
||||
const previewVideoUrl = ref(null)
|
||||
const previewVideoPosterUrl = ref('')
|
||||
const previewVideoError = ref('')
|
||||
|
||||
const openImagePreview = (url) => {
|
||||
if (!process.client) return
|
||||
previewImageUrl.value = url
|
||||
document.body.style.overflow = 'hidden'
|
||||
}
|
||||
|
||||
const closeImagePreview = () => {
|
||||
if (!process.client) return
|
||||
previewImageUrl.value = null
|
||||
document.body.style.overflow = ''
|
||||
if (!previewVideoUrl.value) document.body.style.overflow = ''
|
||||
}
|
||||
|
||||
const openVideoPreview = (url, poster) => {
|
||||
if (!process.client) return
|
||||
const u = String(url || '').trim()
|
||||
if (!u) return
|
||||
previewVideoError.value = ''
|
||||
previewVideoPosterUrl.value = String(poster || '').trim()
|
||||
previewVideoUrl.value = u
|
||||
document.body.style.overflow = 'hidden'
|
||||
}
|
||||
|
||||
const closeVideoPreview = () => {
|
||||
if (!process.client) return
|
||||
previewVideoUrl.value = null
|
||||
previewVideoPosterUrl.value = ''
|
||||
previewVideoError.value = ''
|
||||
if (!previewImageUrl.value) document.body.style.overflow = ''
|
||||
}
|
||||
|
||||
const onPreviewVideoError = () => {
|
||||
previewVideoError.value = '视频加载失败。'
|
||||
}
|
||||
|
||||
const voiceRefs = ref({})
|
||||
@@ -7047,9 +7105,7 @@ const openChatHistoryQuote = (rec) => {
|
||||
if (!url) return
|
||||
|
||||
if (kind === 'video') {
|
||||
try {
|
||||
window.open(url, '_blank', 'noreferrer')
|
||||
} catch {}
|
||||
openVideoPreview(url, q?.thumbUrl)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -7392,6 +7448,7 @@ const onGlobalKeyDown = (e) => {
|
||||
if (key === 'Escape') {
|
||||
if (contextMenu.value.visible) closeContextMenu()
|
||||
if (previewImageUrl.value) closeImagePreview()
|
||||
if (previewVideoUrl.value) closeVideoPreview()
|
||||
if (Array.isArray(floatingWindows.value) && floatingWindows.value.length) closeTopFloatingWindow()
|
||||
if (chatHistoryModalVisible.value) closeChatHistoryModal()
|
||||
if (contactProfileCardOpen.value) {
|
||||
@@ -9311,4 +9368,3 @@ const LinkCard = defineComponent({
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
+116
-11
@@ -638,7 +638,19 @@
|
||||
>
|
||||
<div class="relative max-w-[92vw] max-h-[92vh] flex flex-col items-center" @click.stop>
|
||||
<video
|
||||
v-if="previewLivePhotoVideoSrc && !previewHasLivePhotoVideoError"
|
||||
v-if="previewIsVideo"
|
||||
ref="previewVideoEl"
|
||||
:key="previewVideoKey"
|
||||
:src="previewVideoSrc"
|
||||
:poster="previewVideoPoster"
|
||||
class="max-w-[90vw] max-h-[70vh] object-contain"
|
||||
controls
|
||||
autoplay
|
||||
playsinline
|
||||
@error="onPreviewVideoError"
|
||||
></video>
|
||||
<video
|
||||
v-else-if="previewLivePhotoVideoSrc && !previewHasLivePhotoVideoError"
|
||||
ref="previewLiveVideoEl"
|
||||
:src="previewLivePhotoVideoSrc"
|
||||
:poster="previewSrc"
|
||||
@@ -651,6 +663,13 @@
|
||||
></video>
|
||||
<img v-else :src="previewSrc" alt="预览" class="max-w-[90vw] max-h-[70vh] object-contain" />
|
||||
|
||||
<div
|
||||
v-if="previewIsVideo && previewVideoError"
|
||||
class="mt-3 text-xs text-red-200 whitespace-pre-wrap text-center max-w-[90vw]"
|
||||
>
|
||||
{{ previewVideoError }}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<button
|
||||
@@ -1756,6 +1775,53 @@ const previewSrc = computed(() => {
|
||||
return getMediaPreviewSrc(ctx.post, ctx.media, ctx.idx)
|
||||
})
|
||||
|
||||
const previewVideoEl = ref(null)
|
||||
const previewVideoMode = ref('') // 'local' | 'remote' | 'raw'
|
||||
const previewVideoError = ref('')
|
||||
const previewVideoTried = reactive({ local: false, remote: false, raw: false })
|
||||
|
||||
const resetPreviewVideo = () => {
|
||||
previewVideoMode.value = ''
|
||||
previewVideoError.value = ''
|
||||
previewVideoTried.local = false
|
||||
previewVideoTried.remote = false
|
||||
previewVideoTried.raw = false
|
||||
}
|
||||
|
||||
const previewIsVideo = computed(() => {
|
||||
const ctx = previewCtx.value
|
||||
if (!ctx) return false
|
||||
return Number(ctx.media?.type || 0) === 6
|
||||
})
|
||||
|
||||
const previewVideoPoster = computed(() => {
|
||||
const ctx = previewCtx.value
|
||||
if (!ctx) return ''
|
||||
if (Number(ctx.media?.type || 0) !== 6) return ''
|
||||
return getMediaThumbSrc(ctx.post, ctx.media, ctx.idx) || ''
|
||||
})
|
||||
|
||||
const previewVideoSrc = computed(() => {
|
||||
const ctx = previewCtx.value
|
||||
if (!ctx) return ''
|
||||
if (Number(ctx.media?.type || 0) !== 6) return ''
|
||||
|
||||
const local = getSnsVideoUrl(ctx.post?.id, ctx.media?.id)
|
||||
const remote = getSnsRemoteVideoSrc(ctx.post, ctx.media)
|
||||
const raw = upgradeTencentHttps(String(ctx.media?.url || '').trim())
|
||||
|
||||
const mode = String(previewVideoMode.value || '').toLowerCase()
|
||||
if (mode === 'local') return local
|
||||
if (mode === 'remote') return remote
|
||||
if (mode === 'raw') return raw
|
||||
return local || remote || raw || ''
|
||||
})
|
||||
|
||||
const previewVideoKey = computed(() => {
|
||||
if (!previewIsVideo.value) return ''
|
||||
return `${String(previewVideoMode.value || '')}:${String(previewVideoSrc.value || '')}`
|
||||
})
|
||||
|
||||
const previewLivePhotoVideoSrc = computed(() => {
|
||||
const ctx = previewCtx.value
|
||||
if (!ctx) return ''
|
||||
@@ -1879,6 +1945,7 @@ const loadPreviewCandidates = async ({ reset }) => {
|
||||
|
||||
const openImagePreview = async (post, m, idx = 0) => {
|
||||
if (!process.client) return
|
||||
resetPreviewVideo()
|
||||
// Stop any background hover-playing live photo when opening the preview.
|
||||
activeLivePhotoKey.value = ''
|
||||
// Preview is an intentional action; allow retry even if hover playback failed once.
|
||||
@@ -1898,11 +1965,58 @@ const openImagePreview = async (post, m, idx = 0) => {
|
||||
await loadPreviewCandidates({ reset: true })
|
||||
}
|
||||
|
||||
const openVideoPreview = (post, m, idx = 0) => {
|
||||
if (!process.client) return
|
||||
resetPreviewVideo()
|
||||
activeLivePhotoKey.value = ''
|
||||
|
||||
const local = getSnsVideoUrl(post?.id, m?.id)
|
||||
const remote = getSnsRemoteVideoSrc(post, m)
|
||||
const raw = upgradeTencentHttps(String(m?.url || '').trim())
|
||||
|
||||
if (local) previewVideoMode.value = 'local'
|
||||
else if (remote) previewVideoMode.value = 'remote'
|
||||
else if (raw) previewVideoMode.value = 'raw'
|
||||
else previewVideoError.value = '视频地址缺失。'
|
||||
|
||||
previewCtx.value = { post, media: m, idx: Number(idx) || 0 }
|
||||
previewCandidatesOpen.value = false
|
||||
resetPreviewCandidates()
|
||||
document.body.style.overflow = 'hidden'
|
||||
}
|
||||
|
||||
const onPreviewVideoError = () => {
|
||||
const ctx = previewCtx.value
|
||||
if (!ctx) return
|
||||
if (Number(ctx.media?.type || 0) !== 6) return
|
||||
|
||||
const current = String(previewVideoMode.value || '').toLowerCase()
|
||||
if (current === 'local') previewVideoTried.local = true
|
||||
if (current === 'remote') previewVideoTried.remote = true
|
||||
if (current === 'raw') previewVideoTried.raw = true
|
||||
|
||||
// Fallback order: local -> remote -> raw
|
||||
const remote = getSnsRemoteVideoSrc(ctx.post, ctx.media)
|
||||
if (!previewVideoTried.remote && remote) {
|
||||
previewVideoMode.value = 'remote'
|
||||
return
|
||||
}
|
||||
|
||||
const raw = upgradeTencentHttps(String(ctx.media?.url || '').trim())
|
||||
if (!previewVideoTried.raw && raw) {
|
||||
previewVideoMode.value = 'raw'
|
||||
return
|
||||
}
|
||||
|
||||
previewVideoError.value = '视频加载失败:可能是本地缓存不存在,或远程下载/解密失败。'
|
||||
}
|
||||
|
||||
const closeImagePreview = () => {
|
||||
if (!process.client) return
|
||||
previewCtx.value = null
|
||||
previewCandidatesOpen.value = false
|
||||
resetPreviewCandidates()
|
||||
resetPreviewVideo()
|
||||
document.body.style.overflow = ''
|
||||
}
|
||||
|
||||
@@ -1912,16 +2026,7 @@ const onMediaClick = (post, m, idx = 0) => {
|
||||
|
||||
// 视频点击逻辑
|
||||
if (mt === 6) {
|
||||
// Open a playable mp4 via backend (downloads+decrypts as needed).
|
||||
const remoteUrl = getSnsRemoteVideoSrc(post, m)
|
||||
if (remoteUrl) {
|
||||
window.open(remoteUrl, '_blank', 'noopener,noreferrer')
|
||||
return
|
||||
}
|
||||
|
||||
// Last-resort: open raw CDN url.
|
||||
const u = String(m?.url || '').trim()
|
||||
if (u) window.open(u, '_blank', 'noopener,noreferrer')
|
||||
openVideoPreview(post, m, idx)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user