feat: 支持微信视频消息解析与播放

支持视频 XML 解析、本地文件映射与拖动播放。

修复源码乱码注释并统一 UTF-8 编码。
This commit is contained in:
Wxw-Gu
2026-07-24 14:16:34 +08:00
parent 430a36333b
commit 8488e81bb5
13 changed files with 410 additions and 23 deletions
+1 -1
View File
@@ -7,7 +7,7 @@
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; media-src 'self' blob: data:;"
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https: wxe-media:; media-src 'self' blob: data: wxe-media:;"
/>
</head>
+1 -1
View File
@@ -1,4 +1,4 @@
import React, { useState } from 'react'
import React, { useState } from 'react'
import { Sidebar } from './components/Sidebar'
import ChatWindow from './components/ChatWindow'
import { AppShell } from './components/layout/AppShell'
+48
View File
@@ -1375,6 +1375,54 @@ body {
display: none;
}
.video-message-bubble {
padding: 0;
overflow: hidden;
background: #101412;
border-color: rgba(0, 0, 0, 0.12);
}
.wechat-message-row.mine .video-message-bubble {
background: #101412;
}
.video-message {
position: relative;
width: min(320px, 48vw);
min-height: 140px;
background: #101412;
}
.video-content {
display: block;
width: 100%;
max-height: 360px;
object-fit: contain;
background: #101412;
}
.video-duration {
position: absolute;
right: 8px;
top: 8px;
padding: 2px 6px;
border-radius: 8px;
color: #fff;
background: rgba(0, 0, 0, 0.55);
font-size: 11px;
pointer-events: none;
}
.video-placeholder {
display: grid;
place-items: center;
width: min(260px, 44vw);
min-height: 120px;
padding: 16px;
color: rgba(255, 255, 255, 0.78);
text-align: center;
}
.image-bubble {
position: relative;
max-width: min(260px, 46vw);
@@ -0,0 +1,53 @@
import { useEffect, useMemo, useState } from 'react'
interface VideoBubbleProps {
md5?: string
newMd5?: string
rawMd5?: string
duration?: number
}
export function VideoBubble({
md5,
newMd5,
rawMd5,
duration
}: VideoBubbleProps): React.ReactElement {
const hashes = useMemo(
() => [rawMd5, newMd5, md5].filter((value): value is string => Boolean(value)),
[md5, newMd5, rawMd5]
)
const [media, setMedia] = useState<{ url?: string; poster?: string; error?: string }>({})
useEffect(() => {
let cancelled = false
window.api
.getVideo(hashes)
.then((result) => {
if (!cancelled) setMedia(result.success ? result : { error: result.error })
})
.catch((error) => {
if (!cancelled) setMedia({ error: error instanceof Error ? error.message : String(error) })
})
return () => {
cancelled = true
}
}, [hashes])
if (!media.url) {
return <div className="video-placeholder">{media.error || '视频加载中…'}</div>
}
return (
<div className="video-message">
<video
className="video-content"
src={media.url}
poster={media.poster}
controls
preload="metadata"
/>
{duration ? <span className="video-duration">{duration} </span> : null}
</div>
)
}
@@ -3,6 +3,7 @@ import { Contact, Message } from '../../../../shared/types'
import { ImageBubble } from '../ImageBubble'
import { RichMessageBubble } from '../RichMessageBubble'
import { VoicePlayer } from '../VoicePlayer'
import { VideoBubble } from '../VideoBubble'
import { renderWechatEmojiText } from '../../utils/wechatEmojiText'
import { formatMessageTime } from './messageGrouping'
@@ -27,6 +28,7 @@ export function MessageBubble({
}: MessageBubbleProps): React.ReactElement {
const isVoice = message.type === '语音'
const isImage = message.type === '图片'
const isVideo = message.type === '视频'
const isRichMedia = RICH_MESSAGE_TYPES.includes(message.type)
const hoverTime = formatMessageTime(message)
@@ -35,7 +37,7 @@ export function MessageBubble({
<div
className={`message-bubble ${isVoice ? 'voice-bubble' : ''} ${
isImage ? 'image-message-bubble' : ''
}`}
} ${isVideo ? 'video-message-bubble' : ''}`}
>
{isVoice && message.sessionId ? (
<VoicePlayer
@@ -50,6 +52,13 @@ export function MessageBubble({
sessionId={message.sessionId}
onImageClick={onImageClick}
/>
) : isVideo && message.contentData && message.contentData.type === 'video' ? (
<VideoBubble
md5={message.contentData.md5}
newMd5={message.contentData.newMd5}
rawMd5={message.contentData.rawMd5}
duration={message.contentData.duration}
/>
) : isRichMedia && message.contentData ? (
<RichMessageBubble contentData={message.contentData} />
) : (