feat: 一些非常帅气的优化

This commit is contained in:
cc
2026-02-01 22:56:43 +08:00
committed by xuncha
parent 123a088a39
commit f90822694f
27 changed files with 563 additions and 191 deletions

View File

@@ -91,7 +91,7 @@ function AnnualReportPage() {
<div className="annual-report-page">
<Sparkles size={32} className="header-icon" />
<h1 className="page-title"></h1>
<p className="page-desc"></p>
<p className="page-desc"></p>
<div className="report-sections">
<section className="report-section">

View File

@@ -917,7 +917,7 @@ function AnnualReportWindow() {
<Avatar url={selfAvatarUrl} name="我" size="lg" />
</div>
</div>
<p className="hero-desc"><br /></p>
<p className="hero-desc"><br /></p>
</section>
{/* 双向奔赴 */}
@@ -1025,7 +1025,7 @@ function AnnualReportWindow() {
</div>
<p className="hero-desc">
<span className="hl">{midnightKing.displayName}</span>
<br />Ta的对话占深夜期间聊天的 <span className="gold">{midnightKing.percentage}%</span>
<br />Ta的对话占深夜期间聊天的 <span className="gold">{midnightKing.percentage}%</span>
</p>
</section>
)}

View File

@@ -286,6 +286,11 @@ function ChatPage(_props: ChatPageProps) {
setSessions
])
// 同步 currentSessionId 到 ref
useEffect(() => {
currentSessionRef.current = currentSessionId
}, [currentSessionId])
// 加载会话列表(优化:先返回基础数据,异步加载联系人信息)
const loadSessions = async (options?: { silent?: boolean }) => {
if (options?.silent) {
@@ -301,6 +306,19 @@ function ChatPage(_props: ChatPageProps) {
const nextSessions = options?.silent ? mergeSessions(sessionsArray) : sessionsArray
// 确保 nextSessions 也是数组
if (Array.isArray(nextSessions)) {
// 【核心优化】检查当前会话是否有更新(通过 lastTimestamp 对比)
const currentId = currentSessionRef.current
if (currentId) {
const newSession = nextSessions.find(s => s.username === currentId)
const oldSession = sessionsRef.current.find(s => s.username === currentId)
// 如果会话存在且时间戳变大(有新消息)或者之前没有该会话
if (newSession && (!oldSession || newSession.lastTimestamp > oldSession.lastTimestamp)) {
console.log(`[Frontend] Detected update for current session ${currentId}, refreshing messages...`)
void handleIncrementalRefresh()
}
}
setSessions(nextSessions)
// 立即启动联系人信息加载,不再延迟 500ms
void enrichSessionsContactInfo(nextSessions)
@@ -330,14 +348,14 @@ function ChatPage(_props: ChatPageProps) {
// 防止重复加载
if (isEnrichingRef.current) {
console.log('[性能监控] 联系人信息正在加载中,跳过重复请求')
return
}
isEnrichingRef.current = true
enrichCancelledRef.current = false
console.log(`[性能监控] 开始加载联系人信息,会话数: ${sessions.length}`)
const totalStart = performance.now()
// 移除初始 500ms 延迟,让后台加载与 UI 渲染并行
@@ -352,12 +370,12 @@ function ChatPage(_props: ChatPageProps) {
// 找出需要加载联系人信息的会话(没有头像或者没有显示名称的)
const needEnrich = sessions.filter(s => !s.avatarUrl || !s.displayName || s.displayName === s.username)
if (needEnrich.length === 0) {
console.log('[性能监控] 所有联系人信息已缓存,跳过加载')
isEnrichingRef.current = false
return
}
console.log(`[性能监控] 需要加载的联系人信息: ${needEnrich.length}`)
// 进一步减少批次大小每批3个避免DLL调用阻塞
const batchSize = 3
@@ -366,7 +384,7 @@ function ChatPage(_props: ChatPageProps) {
for (let i = 0; i < needEnrich.length; i += batchSize) {
// 如果正在滚动,暂停加载
if (isScrollingRef.current) {
console.log('[性能监控] 检测到滚动,暂停加载联系人信息')
// 等待滚动结束
while (isScrollingRef.current && !enrichCancelledRef.current) {
await new Promise(resolve => setTimeout(resolve, 200))
@@ -410,9 +428,9 @@ function ChatPage(_props: ChatPageProps) {
const totalTime = performance.now() - totalStart
if (!enrichCancelledRef.current) {
console.log(`[性能监控] 联系人信息加载完成,总耗时: ${totalTime.toFixed(2)}ms, 已加载: ${loadedCount}/${needEnrich.length}`)
} else {
console.log(`[性能监控] 联系人信息加载被取消,已加载: ${loadedCount}/${needEnrich.length}`)
}
} catch (e) {
console.error('加载联系人信息失败:', e)
@@ -514,7 +532,7 @@ function ChatPage(_props: ChatPageProps) {
// 如果是自己的信息且当前个人头像为空,同步更新
if (myWxid && username === myWxid && contact.avatarUrl && !myAvatarUrl) {
console.log('[ChatPage] 从联系人同步获取到个人头像')
setMyAvatarUrl(contact.avatarUrl)
}
@@ -542,6 +560,50 @@ function ChatPage(_props: ChatPageProps) {
// 刷新当前会话消息(增量更新新消息)
const [isRefreshingMessages, setIsRefreshingMessages] = useState(false)
/**
* 极速增量刷新:基于最后一条消息时间戳,获取后续新消息
* (由用户建议:记住上一条消息时间,自动取之后的并渲染,然后后台兜底全量同步)
*/
const handleIncrementalRefresh = async () => {
if (!currentSessionId || isRefreshingMessages) return
// 找出当前已渲染消息中的最大时间戳
const lastMsg = messages[messages.length - 1]
const minTime = lastMsg?.createTime || 0
// 1. 优先执行增量查询并渲染(第一步)
try {
const result = await (window.electronAPI.chat as any).getNewMessages(currentSessionId, minTime) as {
success: boolean;
messages?: Message[];
error?: string
}
if (result.success && result.messages && result.messages.length > 0) {
// 过滤去重
const existingKeys = new Set(messages.map(getMessageKey))
const newOnes = result.messages.filter(m => !existingKeys.has(getMessageKey(m)))
if (newOnes.length > 0) {
appendMessages(newOnes, false)
flashNewMessages(newOnes.map(getMessageKey))
// 滚动到底部
requestAnimationFrame(() => {
if (messageListRef.current) {
messageListRef.current.scrollTop = messageListRef.current.scrollHeight
}
})
}
}
} catch (e) {
console.warn('[IncrementalRefresh] 失败,将依赖全量同步兜底:', e)
}
// 2. 后台兜底:执行之前的完整游标刷新,确保没有遗漏(比如跨库的消息)
void handleRefreshMessages()
}
const handleRefreshMessages = async () => {
if (!currentSessionId || isRefreshingMessages) return
setJumpStartTime(0)
@@ -584,6 +646,31 @@ function ChatPage(_props: ChatPageProps) {
}
}
// 监听数据库变更实时刷新
useEffect(() => {
const handleDbChange = (_event: any, data: { type: string; json: string }) => {
try {
const payload = JSON.parse(data.json)
const tableName = payload.table
// 会话列表更新(主要靠这个触发,因为 wcdb_api 已经只监控 session 了)
if (tableName === 'Session' || tableName === 'session') {
void loadSessions({ silent: true })
}
} catch (e) {
console.error('解析数据库变更通知失败:', e)
}
}
if (window.electronAPI.chat.onWcdbChange) {
const removeListener = window.electronAPI.chat.onWcdbChange(handleDbChange)
return () => {
removeListener()
}
}
return () => { }
}, [loadSessions, handleRefreshMessages])
// 加载消息
const loadMessages = async (sessionId: string, offset = 0, startTime = 0, endTime = 0) => {
const listEl = messageListRef.current
@@ -621,7 +708,7 @@ function ChatPage(_props: ChatPageProps) {
.map(m => m.senderUsername as string)
)]
if (unknownSenders.length > 0) {
console.log(`[性能监控] 预取消息发送者信息: ${unknownSenders.length}`)
// 在批量请求前,先将这些发送者标记为加载中,防止 MessageBubble 触发重复请求
const batchPromise = loadContactInfoBatch(unknownSenders)
unknownSenders.forEach(username => {
@@ -1549,23 +1636,13 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat, o
useEffect(() => {
if (!isVideo) return
console.log('[Video Debug] Full message object:', JSON.stringify(message, null, 2))
console.log('[Video Debug] Message keys:', Object.keys(message))
console.log('[Video Debug] Message:', {
localId: message.localId,
localType: message.localType,
hasVideoMd5: !!message.videoMd5,
hasContent: !!message.content,
hasParsedContent: !!message.parsedContent,
hasRawContent: !!(message as any).rawContent,
contentPreview: message.content?.substring(0, 200),
parsedContentPreview: message.parsedContent?.substring(0, 200),
rawContentPreview: (message as any).rawContent?.substring(0, 200)
})
// 优先使用数据库中的 videoMd5
if (message.videoMd5) {
console.log('[Video Debug] Using videoMd5 from message:', message.videoMd5)
setVideoMd5(message.videoMd5)
return
}
@@ -1573,11 +1650,11 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat, o
// 尝试从多个可能的字段获取原始内容
const contentToUse = message.content || (message as any).rawContent || message.parsedContent
if (contentToUse) {
console.log('[Video Debug] Parsing MD5 from content, length:', contentToUse.length)
window.electronAPI.video.parseVideoMd5(contentToUse).then((result: { success: boolean; md5?: string; error?: string }) => {
console.log('[Video Debug] Parse result:', result)
if (result && result.success && result.md5) {
console.log('[Video Debug] Parsed MD5:', result.md5)
setVideoMd5(result.md5)
} else {
console.error('[Video Debug] Failed to parse MD5:', result)
@@ -2061,11 +2138,7 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat, o
String(message.localId),
message.createTime
)
console.log('[ChatPage] 调用转写:', {
sessionId: session.username,
msgId: message.localId,
createTime: message.createTime
})
if (result.success) {
const transcriptText = (result.transcript || '').trim()
voiceTranscriptCache.set(voiceTranscriptCacheKey, transcriptText)
@@ -2138,14 +2211,14 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat, o
useEffect(() => {
if (!isVideo || !isVideoVisible || videoInfo || videoLoading) return
if (!videoMd5) {
console.log('[Video Debug] No videoMd5 available yet')
return
}
console.log('[Video Debug] Loading video info for MD5:', videoMd5)
setVideoLoading(true)
window.electronAPI.video.getVideoInfo(videoMd5).then((result: { success: boolean; exists: boolean; videoUrl?: string; coverUrl?: string; thumbUrl?: string; error?: string }) => {
console.log('[Video Debug] getVideoInfo result:', result)
if (result && result.success) {
setVideoInfo({
exists: result.exists,
@@ -2642,7 +2715,7 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat, o
const fileName = message.fileName || title || '文件'
const fileSize = message.fileSize
const fileExt = message.fileExt || fileName.split('.').pop()?.toLowerCase() || ''
// 根据扩展名选择图标
const getFileIcon = () => {
const archiveExts = ['zip', 'rar', '7z', 'tar', 'gz', 'bz2']
@@ -2662,7 +2735,7 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat, o
</svg>
)
}
return (
<div className="file-message">
<div className="file-icon">
@@ -2682,10 +2755,10 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat, o
if (appMsgType === '2000') {
try {
const content = message.rawContent || message.content || message.parsedContent || ''
// 添加调试日志
console.log('[Transfer Debug] Raw content:', content.substring(0, 500))
const parser = new DOMParser()
const doc = parser.parseFromString(content, 'text/xml')
@@ -2693,11 +2766,11 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat, o
const payMemo = doc.querySelector('pay_memo')?.textContent || ''
const paysubtype = doc.querySelector('paysubtype')?.textContent || '1'
console.log('[Transfer Debug] Parsed:', { feedesc, payMemo, paysubtype, title })
// paysubtype: 1=待收款, 3=已收款
const isReceived = paysubtype === '3'
// 如果 feedesc 为空,使用 title 作为降级
const displayAmount = feedesc || title || '微信转账'
@@ -2743,7 +2816,7 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat, o
<div className="miniapp-message">
<div className="miniapp-icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" />
</svg>
</div>
<div className="miniapp-info">

View File

@@ -41,15 +41,10 @@ function ContactsPage() {
return
}
const contactsResult = await window.electronAPI.chat.getContacts()
console.log('📞 getContacts结果:', contactsResult)
if (contactsResult.success && contactsResult.contacts) {
console.log('📊 总联系人数:', contactsResult.contacts.length)
console.log('📊 按类型统计:', {
friends: contactsResult.contacts.filter((c: ContactInfo) => c.type === 'friend').length,
groups: contactsResult.contacts.filter((c: ContactInfo) => c.type === 'group').length,
officials: contactsResult.contacts.filter((c: ContactInfo) => c.type === 'official').length,
other: contactsResult.contacts.filter((c: ContactInfo) => c.type === 'other').length
})
// 获取头像URL
const usernames = contactsResult.contacts.map((c: ContactInfo) => c.username)

View File

@@ -149,7 +149,7 @@ export default function SnsPage() {
const currentPosts = postsRef.current
if (currentPosts.length > 0) {
const topTs = currentPosts[0].createTime
console.log('[SnsPage] Fetching newer posts starts from:', topTs + 1);
const result = await window.electronAPI.sns.getTimeline(
limit,
@@ -281,10 +281,10 @@ export default function SnsPage() {
const checkSchema = async () => {
try {
const schema = await window.electronAPI.chat.execQuery('sns', null, "PRAGMA table_info(SnsTimeLine)");
console.log('[SnsPage] SnsTimeLine Schema:', schema);
if (schema.success && schema.rows) {
const columns = schema.rows.map((r: any) => r.name);
console.log('[SnsPage] Available columns:', columns);
}
} catch (e) {
console.error('[SnsPage] Failed to check schema:', e);
@@ -335,7 +335,7 @@ export default function SnsPage() {
// deltaY < 0 表示向上滚scrollTop === 0 表示已经在最顶端
if (e.deltaY < -20 && container.scrollTop <= 0 && hasNewer && !loading && !loadingNewer) {
console.log('[SnsPage] Wheel-up detected at top, loading newer posts...');
loadPosts({ direction: 'newer' })
}
}