diff --git a/electron/services/wcdbService.ts b/electron/services/wcdbService.ts index 2e2a95b..be0ecb3 100644 --- a/electron/services/wcdbService.ts +++ b/electron/services/wcdbService.ts @@ -49,6 +49,8 @@ export class WcdbService { private wcdbGetEmoticonCdnUrl: any = null private avatarUrlCache: Map = new Map() private readonly avatarCacheTtlMs = 10 * 60 * 1000 + private logTimer: NodeJS.Timeout | null = null + private lastLogTail: string | null = null setPaths(resourcesPath: string, userDataPath: string): void { this.resourcesPath = resourcesPath @@ -57,6 +59,11 @@ export class WcdbService { setLogEnabled(enabled: boolean): void { this.logEnabled = enabled + if (this.isLogEnabled() && this.initialized) { + this.startLogPolling() + } else { + this.stopLogPolling() + } } /** @@ -433,6 +440,49 @@ export class WcdbService { } } + private startLogPolling(): void { + if (this.logTimer || !this.isLogEnabled()) return + this.logTimer = setInterval(() => { + void this.pollLogs() + }, 2000) + } + + private stopLogPolling(): void { + if (this.logTimer) { + clearInterval(this.logTimer) + this.logTimer = null + } + this.lastLogTail = null + } + + private async pollLogs(): Promise { + try { + if (!this.wcdbGetLogs || !this.isLogEnabled()) return + const outPtr = [null as any] + const result = this.wcdbGetLogs(outPtr) + if (result !== 0 || !outPtr[0]) return + let jsonStr = '' + try { + jsonStr = this.koffi.decode(outPtr[0], 'char', -1) + } finally { + try { this.wcdbFreeString(outPtr[0]) } catch { } + } + const logs = JSON.parse(jsonStr) as string[] + if (!Array.isArray(logs) || logs.length === 0) return + let startIdx = 0 + if (this.lastLogTail) { + const idx = logs.lastIndexOf(this.lastLogTail) + if (idx >= 0) startIdx = idx + 1 + } + for (let i = startIdx; i < logs.length; i += 1) { + this.writeLog(`wcdb: ${logs[i]}`) + } + this.lastLogTail = logs[logs.length - 1] + } catch (e) { + // ignore polling errors + } + } + private decodeJsonPtr(outPtr: any): string | null { if (!outPtr) return null try { @@ -545,6 +595,9 @@ export class WcdbService { console.warn('设置 wxid 失败:', e) } } + if (this.isLogEnabled()) { + this.startLogPolling() + } this.writeLog(`open ok handle=${handle}`) return true } catch (e) { @@ -571,6 +624,7 @@ export class WcdbService { this.currentKey = null this.currentWxid = null this.initialized = false + this.stopLogPolling() } } diff --git a/package.json b/package.json index 6d2cec2..dff0f8e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "weflow", - "version": "1.0.3", + "version": "1.0.4", "description": "WeFlow - 微信聊天记录查看工具", "main": "dist-electron/main.js", "author": "cc", diff --git a/resources/wcdb_api.dll b/resources/wcdb_api.dll index 324246b..70e4eb7 100644 Binary files a/resources/wcdb_api.dll and b/resources/wcdb_api.dll differ diff --git a/src/pages/ChatPage.scss b/src/pages/ChatPage.scss index 9d36dbf..e53857c 100644 --- a/src/pages/ChatPage.scss +++ b/src/pages/ChatPage.scss @@ -883,6 +883,23 @@ min-height: 0; overflow: hidden; -webkit-app-region: no-drag; + position: relative; + + &.loading .message-list { + opacity: 0; + transform: translateY(8px); + pointer-events: none; + } + + &.loaded .message-list { + opacity: 1; + transform: translateY(0); + } + + &.loaded .loading-overlay { + opacity: 0; + pointer-events: none; + } } .message-list { @@ -898,6 +915,7 @@ background-color: var(--bg-tertiary); position: relative; -webkit-app-region: no-drag !important; + transition: opacity 240ms ease, transform 240ms ease; // 滚动条样式 &::-webkit-scrollbar { @@ -918,6 +936,19 @@ } } +.loading-messages.loading-overlay { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + gap: 10px; + background: rgba(10, 10, 10, 0.28); + backdrop-filter: blur(6px); + transition: opacity 200ms ease; + z-index: 2; +} + .message-list * { -webkit-app-region: no-drag !important; } @@ -1108,6 +1139,7 @@ font-size: 14px; line-height: 1.6; word-break: break-word; + white-space: pre-wrap; } // 表情包消息 @@ -1432,6 +1464,7 @@ .quoted-text { color: var(--text-secondary); + white-space: pre-wrap; } } diff --git a/src/pages/ChatPage.tsx b/src/pages/ChatPage.tsx index 0004cd5..3314201 100644 --- a/src/pages/ChatPage.tsx +++ b/src/pages/ChatPage.tsx @@ -108,6 +108,7 @@ function ChatPage(_props: ChatPageProps) { const messageListRef = useRef(null) const searchInputRef = useRef(null) const sidebarRef = useRef(null) + const initialRevealTimerRef = useRef(null) const [currentOffset, setCurrentOffset] = useState(0) const [myAvatarUrl, setMyAvatarUrl] = useState(undefined) const [showScrollToBottom, setShowScrollToBottom] = useState(false) @@ -118,6 +119,7 @@ function ChatPage(_props: ChatPageProps) { const [isLoadingDetail, setIsLoadingDetail] = useState(false) const [highlightedMessageKeys, setHighlightedMessageKeys] = useState([]) const [isRefreshingSessions, setIsRefreshingSessions] = useState(false) + const [hasInitialMessages, setHasInitialMessages] = useState(false) const highlightedMessageSet = useMemo(() => new Set(highlightedMessageKeys), [highlightedMessageKeys]) @@ -126,6 +128,7 @@ function ChatPage(_props: ChatPageProps) { const sessionMapRef = useRef>(new Map()) const sessionsRef = useRef([]) const currentSessionRef = useRef(null) + const prevSessionRef = useRef(null) const isLoadingMessagesRef = useRef(false) const isLoadingMoreRef = useRef(false) const isConnectedRef = useRef(false) @@ -511,6 +514,53 @@ function ChatPage(_props: ChatPageProps) { isLoadingMoreRef.current = isLoadingMore }, [isLoadingMessages, isLoadingMore]) + useEffect(() => { + if (initialRevealTimerRef.current !== null) { + window.clearTimeout(initialRevealTimerRef.current) + initialRevealTimerRef.current = null + } + if (!isLoadingMessages) { + if (messages.length === 0) { + setHasInitialMessages(true) + } else { + initialRevealTimerRef.current = window.setTimeout(() => { + setHasInitialMessages(true) + initialRevealTimerRef.current = null + }, 120) + } + } + }, [isLoadingMessages, messages.length]) + + useEffect(() => { + if (currentSessionId !== prevSessionRef.current) { + prevSessionRef.current = currentSessionId + if (initialRevealTimerRef.current !== null) { + window.clearTimeout(initialRevealTimerRef.current) + initialRevealTimerRef.current = null + } + if (messages.length === 0) { + setHasInitialMessages(false) + } else if (!isLoadingMessages) { + setHasInitialMessages(true) + } + } + }, [currentSessionId, messages.length, isLoadingMessages]) + + useEffect(() => { + if (currentSessionId && messages.length === 0 && !isLoadingMessages && !isLoadingMore) { + loadMessages(currentSessionId, 0) + } + }, [currentSessionId, messages.length, isLoadingMessages, isLoadingMore]) + + useEffect(() => { + return () => { + if (initialRevealTimerRef.current !== null) { + window.clearTimeout(initialRevealTimerRef.current) + initialRevealTimerRef.current = null + } + } + }, []) + useEffect(() => { isConnectedRef.current = isConnected }, [isConnected]) @@ -710,18 +760,18 @@ function ChatPage(_props: ChatPageProps) { -
- {isLoadingMessages ? ( -
+
+ {isLoadingMessages && !hasInitialMessages && ( +
加载消息中...
- ) : ( -
+ )} +
{hasMoreMessages && (
{isLoadingMore ? ( @@ -772,7 +822,6 @@ function ChatPage(_props: ChatPageProps) { 回到底部
- )} {/* 会话详情面板 */} {showDetailPanel && (