From 3214c2804e1c23fa85ec5fde3ad485bffebd6053 Mon Sep 17 00:00:00 2001 From: Leoluis0705 Date: Wed, 25 Mar 2026 18:24:05 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat(group-analytics):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E5=B9=B6=E6=9E=81=E8=87=B4=E4=BC=98=E5=8C=96=E7=BE=A4=E6=88=90?= =?UTF-8?q?=E5=91=98=E8=AF=A6=E7=BB=86=E5=88=86=E6=9E=90=E4=B8=8E=E5=9B=BE?= =?UTF-8?q?=E8=A1=A8=E5=91=88=E7=8E=B0=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- electron/main.ts | 7 + electron/preload.ts | 1 + electron/services/groupAnalyticsService.ts | 176 +++++++++++++- src/pages/GroupAnalyticsPage.scss | 76 ++++-- src/pages/GroupAnalyticsPage.tsx | 265 ++++++++++++++++++++- src/types/electron.d.ts | 22 ++ 6 files changed, 529 insertions(+), 18 deletions(-) diff --git a/electron/main.ts b/electron/main.ts index 2ebe56b..bb9947d 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -2139,6 +2139,13 @@ function registerIpcHandlers() { return groupAnalyticsService.getGroupMediaStats(chatroomId, startTime, endTime) }) + ipcMain.handle( + 'groupAnalytics:getGroupMemberAnalytics', + async (_, chatroomId: string, memberUsername: string, startTime?: number, endTime?: number) => { + return groupAnalyticsService.getGroupMemberAnalytics(chatroomId, memberUsername, startTime, endTime) + } + ) + ipcMain.handle( 'groupAnalytics:getGroupMemberMessages', async ( diff --git a/electron/preload.ts b/electron/preload.ts index b1c9c30..924bfd5 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -297,6 +297,7 @@ contextBridge.exposeInMainWorld('electronAPI', { getGroupMessageRanking: (chatroomId: string, limit?: number, startTime?: number, endTime?: number) => ipcRenderer.invoke('groupAnalytics:getGroupMessageRanking', chatroomId, limit, startTime, endTime), getGroupActiveHours: (chatroomId: string, startTime?: number, endTime?: number) => ipcRenderer.invoke('groupAnalytics:getGroupActiveHours', chatroomId, startTime, endTime), getGroupMediaStats: (chatroomId: string, startTime?: number, endTime?: number) => ipcRenderer.invoke('groupAnalytics:getGroupMediaStats', chatroomId, startTime, endTime), + getGroupMemberAnalytics: (chatroomId: string, memberUsername: string, startTime?: number, endTime?: number) => ipcRenderer.invoke('groupAnalytics:getGroupMemberAnalytics', chatroomId, memberUsername, startTime, endTime), getGroupMemberMessages: ( chatroomId: string, memberUsername: string, diff --git a/electron/services/groupAnalyticsService.ts b/electron/services/groupAnalyticsService.ts index 8d66ce9..1d322ae 100644 --- a/electron/services/groupAnalyticsService.ts +++ b/electron/services/groupAnalyticsService.ts @@ -5,6 +5,7 @@ import { ConfigService } from './config' import { wcdbService } from './wcdbService' import { chatService } from './chatService' import type { Message } from './chatService' +import type { ChatStatistics } from './analyticsService' export interface GroupChatInfo { username: string @@ -49,6 +50,13 @@ export interface GroupMediaStats { total: number } +export interface GroupMemberAnalytics { + statistics: ChatStatistics + timeDistribution: Record + commonPhrases?: Array<{ phrase: string; count: number }> + commonEmojis?: Array<{ emoji: string; count: number }> +} + export interface GroupMemberMessagesPage { messages: Message[] hasMore: boolean @@ -791,13 +799,33 @@ class GroupAnalyticsService { if (normalizedValue) return normalizedValue } } + + // Fallback: fast extract from raw content to avoid full parse + const rawContent = String(row.StrContent || row.message_content || row.content || row.msg_content || '').trim() + if (rawContent) { + const match = /^\s*([a-zA-Z0-9_@-]{4,}):(?!\/\/)\s*(?:\r?\n|)/i.exec(rawContent) + if (match && match[1]) { + return match[1].trim() + } + } + return '' } private parseSingleMessageRow(row: Record): Message | null { try { const mapped = chatService.mapRowsToMessagesForApi([row]) - return Array.isArray(mapped) && mapped.length > 0 ? mapped[0] : null + if (Array.isArray(mapped) && mapped.length > 0) { + const msg = mapped[0] + if (!msg.localType) { + msg.localType = parseInt(row.Type || row.type || row.local_type || row.msg_type || '0', 10) + } + if (!msg.createTime) { + msg.createTime = parseInt(row.CreateTime || row.create_time || row.createTime || row.msg_time || '0', 10) + } + return msg + } + return null } catch { return null } @@ -1438,6 +1466,152 @@ class GroupAnalyticsService { } } + async getGroupMemberAnalytics( + chatroomId: string, + memberUsername: string, + startTime?: number, + endTime?: number + ): Promise<{ success: boolean; data?: GroupMemberAnalytics; error?: string }> { + try { + const conn = await this.ensureConnected() + if (!conn.success) return { success: false, error: conn.error } + + const normalizedChatroomId = String(chatroomId || '').trim() + const normalizedMemberUsername = String(memberUsername || '').trim() + + const batchSize = 10000 + const senderMatchCache = new Map() + const matchesTargetSender = (sender: string | null | undefined): boolean => { + const key = String(sender || '').trim().toLowerCase() + if (!key) return false + const cached = senderMatchCache.get(key) + if (typeof cached === 'boolean') return cached + const matched = this.isSameAccountIdentity(normalizedMemberUsername, sender) + senderMatchCache.set(key, matched) + return matched + } + + const cursorResult = await this.openMemberMessageCursor(normalizedChatroomId, batchSize, true, startTime || 0, endTime || 0) + if (!cursorResult.success || !cursorResult.cursor) { + return { success: false, error: cursorResult.error || '创建游标失败' } + } + + const cursor = cursorResult.cursor + const stats: ChatStatistics = { + totalMessages: 0, + textMessages: 0, + imageMessages: 0, + voiceMessages: 0, + videoMessages: 0, + emojiMessages: 0, + otherMessages: 0, + sentMessages: 0, // In group, we only fetch messages of this member, so sentMessages = totalMessages + receivedMessages: 0, // No meaning here + firstMessageTime: null, + lastMessageTime: null, + activeDays: 0, + messageTypeCounts: {} + } + + const hourlyDistribution: Record = {} + for (let i = 0; i < 24; i++) hourlyDistribution[i] = 0 + const dailySet = new Set() + const textTypes = [1, 244813135921] + + const phraseCounts = new Map() + const emojiCounts = new Map() + + const myWxid = String(this.configService.get('myWxid') || '').trim() + + try { + while (true) { + const batch = await wcdbService.fetchMessageBatch(cursor) + if (!batch.success) break + const rows = Array.isArray(batch.rows) ? batch.rows as Record[] : [] + if (rows.length === 0) break + + for (const row of rows) { + let senderFromRow = this.extractRowSenderUsername(row) + + const isSendRaw = row.computed_is_send ?? row.is_send ?? row.isSend ?? row.WCDB_CT_is_send + const isSend = isSendRaw != null ? parseInt(isSendRaw, 10) === 1 : false + + if (isSend) { + senderFromRow = myWxid + } + + if (!senderFromRow || !matchesTargetSender(senderFromRow)) { + continue + } + + const msgType = parseInt(row.Type || row.type || row.local_type || row.msg_type || '0', 10) + const createTime = parseInt(row.CreateTime || row.create_time || row.createTime || row.msg_time || '0', 10) + + let content = String(row.StrContent || row.message_content || row.content || row.msg_content || '') + if (content) { + content = content.replace(/^\s*([a-zA-Z0-9_@-]{4,}):(?!\/\/)\s*(?:\r?\n|)/i, '') + } + + stats.totalMessages++ + if (textTypes.includes(msgType)) { + stats.textMessages++ + if (content) { + const text = content.trim() + if (text && text.length <= 20) { + phraseCounts.set(text, (phraseCounts.get(text) || 0) + 1) + } + const emojiMatches = text.match(/\[.*?\]/g) + if (emojiMatches) { + for (const em of emojiMatches) { + emojiCounts.set(em, (emojiCounts.get(em) || 0) + 1) + } + } + } + } + else if (msgType === 3) stats.imageMessages++ + else if (msgType === 34) stats.voiceMessages++ + else if (msgType === 43) stats.videoMessages++ + else if (msgType === 47) stats.emojiMessages++ + else stats.otherMessages++ + + stats.sentMessages++ + + stats.messageTypeCounts[msgType] = (stats.messageTypeCounts[msgType] || 0) + 1 + + if (createTime > 0) { + if (stats.firstMessageTime === null || createTime < stats.firstMessageTime) stats.firstMessageTime = createTime + if (stats.lastMessageTime === null || createTime > stats.lastMessageTime) stats.lastMessageTime = createTime + + const d = new Date(createTime * 1000) + const hour = d.getHours() + hourlyDistribution[hour]++ + dailySet.add(`${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`) + } + } + if (!batch.hasMore) break + } + } finally { + await wcdbService.closeMessageCursor(cursor) + } + + stats.activeDays = dailySet.size + + const commonPhrases = Array.from(phraseCounts.entries()) + .sort((a, b) => b[1] - a[1]) + .slice(0, 10) + .map(([phrase, count]) => ({ phrase, count })) + + const commonEmojis = Array.from(emojiCounts.entries()) + .sort((a, b) => b[1] - a[1]) + .slice(0, 10) + .map(([emoji, count]) => ({ emoji, count })) + + return { success: true, data: { statistics: stats, timeDistribution: hourlyDistribution, commonPhrases, commonEmojis } } + } catch (e) { + return { success: false, error: String(e) } + } + } + async exportGroupMemberMessages( chatroomId: string, memberUsername: string, diff --git a/src/pages/GroupAnalyticsPage.scss b/src/pages/GroupAnalyticsPage.scss index b1b0eab..e55c30f 100644 --- a/src/pages/GroupAnalyticsPage.scss +++ b/src/pages/GroupAnalyticsPage.scss @@ -834,11 +834,13 @@ } .member-export-panel, -.member-messages-panel { +.member-messages-panel, +.member-analytics-panel { display: flex; flex-direction: column; gap: 16px; min-height: 0; + flex: 1; .member-export-empty { padding: 20px; @@ -1521,29 +1523,73 @@ } } - .stats-cards { + .stats-overview { display: grid; - grid-template-columns: repeat(5, 1fr); - gap: 12px; - margin-bottom: 20px; + grid-template-columns: repeat(4, 1fr); + gap: 16px; + margin-bottom: 24px; + padding-top: 10px; + } - .stat-card { - background: transparent; + .stat-card { + display: flex; + align-items: center; + gap: 16px; + padding: 20px; + background: var(--card-bg); + border-radius: 12px; + border: 1px solid var(--border-color); + + .stat-icon { + width: 48px; + height: 48px; + display: flex; + align-items: center; + justify-content: center; + background: var(--primary-light); border-radius: 12px; - padding: 16px; - text-align: center; + color: var(--primary); + } - .value { - display: block; + .stat-info { + display: flex; + flex-direction: column; + gap: 4px; + + .stat-value { font-size: 24px; font-weight: 600; - color: var(--primary); - margin-bottom: 4px; + color: var(--text-primary); } - .label { + .stat-label { font-size: 13px; - color: var(--text-secondary); + color: var(--text-tertiary); + } + } + } + + .charts-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 16px; + margin-bottom: 24px; + + .chart-card { + background: var(--card-bg); + border-radius: 12px; + border: 1px solid var(--border-color); + padding: 20px; + + &.wide { + grid-column: span 2; + } + + h3 { + font-size: 15px; + font-weight: 500; + color: var(--text-primary); + margin: 0 0 16px; } } } diff --git a/src/pages/GroupAnalyticsPage.tsx b/src/pages/GroupAnalyticsPage.tsx index db14c4d..a31dafc 100644 --- a/src/pages/GroupAnalyticsPage.tsx +++ b/src/pages/GroupAnalyticsPage.tsx @@ -1,6 +1,6 @@ import { useState, useEffect, useRef, useCallback, useMemo } from 'react' import { useLocation } from 'react-router-dom' -import { Users, BarChart3, Clock, Image, Loader2, RefreshCw, Medal, Search, X, ChevronLeft, Copy, Check, Download, ChevronDown, MessageSquare } from 'lucide-react' +import { Users, BarChart3, Clock, Image, Loader2, RefreshCw, Medal, Search, X, ChevronLeft, Copy, Check, Download, ChevronDown, MessageSquare, Calendar, PieChart, Hash, Smile } from 'lucide-react' import { Avatar } from '../components/Avatar' import ReactECharts from 'echarts-for-react' import DateRangePicker from '../components/DateRangePicker' @@ -37,7 +37,7 @@ interface GroupMessageRank { messageCount: number } -type AnalysisFunction = 'members' | 'memberMessages' | 'ranking' | 'activeHours' | 'mediaStats' +type AnalysisFunction = 'members' | 'memberMessages' | 'memberAnalytics' | 'ranking' | 'activeHours' | 'mediaStats' type MemberExportFormat = 'chatlab' | 'chatlab-jsonl' | 'json' | 'arkme-json' | 'html' | 'txt' | 'excel' | 'weclone' interface MemberMessageExportOptions { @@ -167,6 +167,7 @@ function GroupAnalyticsPage() { const [isExportingMembers, setIsExportingMembers] = useState(false) const [isExportingMemberMessages, setIsExportingMemberMessages] = useState(false) const [memberMessages, setMemberMessages] = useState([]) + const [memberAnalyticsData, setMemberAnalyticsData] = useState(null) const [memberMessagesHasMore, setMemberMessagesHasMore] = useState(false) const [memberMessagesCursor, setMemberMessagesCursor] = useState(0) const [memberMessagesLoadingMore, setMemberMessagesLoadingMore] = useState(false) @@ -524,6 +525,7 @@ function GroupAnalyticsPage() { break } case 'memberMessages': { + resetMemberMessageState(false) updateBackgroundTask(taskId, { detail: '正在读取成员列表与消息', progressText: '成员消息' @@ -566,7 +568,55 @@ function GroupAnalyticsPage() { }) break } + case 'memberAnalytics': { + setMemberAnalyticsData(null) + updateBackgroundTask(taskId, { + detail: '正在读取成员列表与消息分析', + progressText: '成员分析' + }) + const result = await window.electronAPI.groupAnalytics.getGroupMembers(targetGroup.username) + if (isBackgroundTaskCancelRequested(taskId)) { + finishBackgroundTask(taskId, 'canceled', { detail: '已停止后续加载,成员分析未继续写入' }) + return + } + if (!result.success || !result.data) { + finishBackgroundTask(taskId, 'failed', { detail: result.error || '获取成员列表失败' }) + return + } + setMembers(result.data) + let targetMember = preferredMemberUsername + ? result.data.find(m => m.username === preferredMemberUsername) + : result.data.find(m => m.username === selectedMessageMemberUsername) + if (!targetMember && result.data.length > 0) { + targetMember = result.data[0] + setSelectedMessageMemberUsername(targetMember.username) + } + if (!targetMember) { + finishBackgroundTask(taskId, 'failed', { detail: '找不到目标成员' }) + return + } + updateBackgroundTask(taskId, { + detail: `正在分析 ${targetMember.displayName || targetMember.username} 的发言记录`, + progressText: '统计分析' + }) + const analyticsResult = await window.electronAPI.groupAnalytics.getGroupMemberAnalytics(targetGroup.username, targetMember.username, startTime, endTime) + if (isBackgroundTaskCancelRequested(taskId)) { + finishBackgroundTask(taskId, 'canceled', { detail: '已停止后续加载,成员分析未继续写入' }) + return + } + if (analyticsResult.success && analyticsResult.data) { + setMemberAnalyticsData(analyticsResult.data) + finishBackgroundTask(taskId, 'completed', { + detail: `分析完成,共计 ${analyticsResult.data.statistics?.totalMessages || 0} 条消息`, + progressText: '已完成' + }) + } else { + finishBackgroundTask(taskId, 'failed', { detail: analyticsResult.error || '分析失败' }) + } + break + } case 'ranking': { + setRankings([]) updateBackgroundTask(taskId, { detail: '正在计算群消息排行', progressText: '消息排行' @@ -584,6 +634,7 @@ function GroupAnalyticsPage() { break } case 'activeHours': { + setActiveHours({}) updateBackgroundTask(taskId, { detail: '正在计算群活跃时段', progressText: '活跃时段' @@ -601,6 +652,7 @@ function GroupAnalyticsPage() { break } case 'mediaStats': { + setMediaStats(null) updateBackgroundTask(taskId, { detail: '正在统计群消息类型', progressText: '消息类型' @@ -633,6 +685,12 @@ function GroupAnalyticsPage() { return num.toLocaleString() } + const formatDate = (timestamp: number | null) => { + if (!timestamp) return '-' + const date = new Date(timestamp * 1000) + return `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}` + } + const sanitizeFileName = (name: string) => { return name.replace(/[<>:"/\\|?*]+/g, '_').trim() } @@ -764,6 +822,16 @@ function GroupAnalyticsPage() { await loadFunctionData('memberMessages', selectedGroup, member.username) } + const handleViewMemberAnalyticsFromModal = async (member: GroupMember) => { + if (!selectedGroup) return + setSelectedMember(null) + setSelectedFunction('memberAnalytics') + setSelectedMessageMemberUsername(member.username) + setMessageMemberSearchKeyword('') + setShowMessageMemberSelect(false) + await loadFunctionData('memberAnalytics', selectedGroup, member.username) + } + const handleOpenMemberExportModal = () => { setShowMessageMemberSelect(false) setShowFormatSelect(false) @@ -982,6 +1050,14 @@ function GroupAnalyticsPage() { + + {showMessageMemberSelect && ( +
+
+ + setMessageMemberSearchKeyword(e.target.value)} + placeholder="搜索 wxid / 昵称 / 备注 / 微信号" + onClick={e => e.stopPropagation()} + /> +
+
+ {filteredMessageMemberOptions.length === 0 ? ( +
无匹配成员
+ ) : ( + filteredMessageMemberOptions.map(member => ( + + )) + )} +
+
+ )} + + + {memberAnalyticsData ? ( +
+
+
+
+
+ {formatNumber(memberAnalyticsData.statistics.sentMessages)} + 发信数量 +
+
+
+
+
+ {memberAnalyticsData.statistics.activeDays} + 活跃天数 +
+
+
+
+
+ + {formatDate(memberAnalyticsData.statistics.firstMessageTime)} - {formatDate(memberAnalyticsData.statistics.lastMessageTime)} + + 活跃周期 +
+
+
+ +
+
+

活跃时段

+
+ `${i}时`) }, + yAxis: { type: 'value' }, + series: [{ type: 'bar', data: Array.from({ length: 24 }, (_, i) => memberAnalyticsData.timeDistribution[i] || 0), itemStyle: { color: '#07c160', borderRadius: [4, 4, 0, 0] } }] + }} + style={{ height: '300px', width: '100%' }} + /> +
+
+ +
+

消息类型分布

+
+ item.value > 0), + label: { show: true, formatter: '{b} {d}%' } + }] + }} + style={{ height: '300px', width: '100%' }} + /> +
+
+
+
+

常用语

+
+ {memberAnalyticsData.commonPhrases && memberAnalyticsData.commonPhrases.length > 0 ? ( + memberAnalyticsData.commonPhrases.map((item: any, idx: number) => ( +
+ {item.phrase} + {item.count}次 +
+ )) + ) : ( + 暂无常用语数据 + )} +
+
+
+

常用表情

+
+ {memberAnalyticsData.commonEmojis && memberAnalyticsData.commonEmojis.length > 0 ? ( + memberAnalyticsData.commonEmojis.map((item: any, idx: number) => ( +
+ {item.emoji} + {item.count}次 +
+ )) + ) : ( + 暂无表情包数据 + )} +
+
+
+
+
+ ) : ( +
+ )} + + )} + + )} {selectedFunction === 'ranking' && (
{rankings.map((item, index) => ( diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts index 8d96abe..41a578c 100644 --- a/src/types/electron.d.ts +++ b/src/types/electron.d.ts @@ -496,6 +496,28 @@ export interface ElectronAPI { } error?: string }> + getGroupMemberAnalytics: (chatroomId: string, memberUsername: string, startTime?: number, endTime?: number) => Promise<{ + success: boolean + data?: { + statistics: { + totalMessages: number + textMessages: number + imageMessages: number + voiceMessages: number + videoMessages: number + emojiMessages: number + otherMessages: number + sentMessages: number + receivedMessages: number + firstMessageTime: number | null + lastMessageTime: number | null + activeDays: number + messageTypeCounts: Record + } + timeDistribution: Record + } + error?: string + }> getGroupMemberMessages: ( chatroomId: string, memberUsername: string, From 3c0683b9f8aa0372af3aa5b4e24785e4f1b7ce7f Mon Sep 17 00:00:00 2001 From: Leoluis0705 Date: Wed, 25 Mar 2026 18:30:24 +0800 Subject: [PATCH 2/3] =?UTF-8?q?perf(core):=20=E4=B8=BA=E5=BA=95=E5=B1=82?= =?UTF-8?q?=E6=8F=90=E5=8F=96=E5=99=A8=E5=BC=95=E5=85=A5=20isSend=20?= =?UTF-8?q?=E6=A0=87=E8=AF=86=E6=99=BA=E8=83=BD=E5=88=A4=E6=96=AD=EF=BC=8C?= =?UTF-8?q?=E8=A7=A3=E5=86=B3=E5=A4=A7=E9=87=8F=E6=9C=AC=E5=9C=B0=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E5=8F=8A=E5=AF=8C=E6=96=87=E6=9C=AC=E6=B6=88=E6=81=AF?= =?UTF-8?q?=E5=BC=95=E5=8F=91=E7=9A=84=E6=80=A7=E8=83=BD=E9=80=80=E5=8C=96?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- electron/services/groupAnalyticsService.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/electron/services/groupAnalyticsService.ts b/electron/services/groupAnalyticsService.ts index 1d322ae..79477ac 100644 --- a/electron/services/groupAnalyticsService.ts +++ b/electron/services/groupAnalyticsService.ts @@ -776,7 +776,12 @@ class GroupAnalyticsService { return normalized > 10000000000 ? Math.floor(normalized / 1000) : normalized } - private extractRowSenderUsername(row: Record): string { + private extractRowSenderUsername(row: Record, myWxid?: string): string { + const isSendRaw = row.computed_is_send ?? row.is_send ?? row.isSend ?? row.WCDB_CT_is_send + if (isSendRaw != null && parseInt(isSendRaw, 10) === 1 && myWxid) { + return myWxid + } + const candidates = [ row.sender_username, row.senderUsername, @@ -880,7 +885,7 @@ class GroupAnalyticsService { if (rows.length === 0) break for (const row of rows) { - const senderFromRow = this.extractRowSenderUsername(row) + const senderFromRow = this.extractRowSenderUsername(row, String(this.configService.get('myWxid') || '').trim()) if (senderFromRow && !matchesTargetSender(senderFromRow)) { continue } @@ -986,7 +991,7 @@ class GroupAnalyticsService { const row = rows[index] consumedRows += 1 - const senderFromRow = this.extractRowSenderUsername(row) + const senderFromRow = this.extractRowSenderUsername(row, String(this.configService.get('myWxid') || '').trim()) if (senderFromRow && !matchesTargetSender(senderFromRow)) { continue } @@ -1531,7 +1536,7 @@ class GroupAnalyticsService { if (rows.length === 0) break for (const row of rows) { - let senderFromRow = this.extractRowSenderUsername(row) + let senderFromRow = this.extractRowSenderUsername(row, myWxid) const isSendRaw = row.computed_is_send ?? row.is_send ?? row.isSend ?? row.WCDB_CT_is_send const isSend = isSendRaw != null ? parseInt(isSendRaw, 10) === 1 : false From a46b52e603341eae197f141aaef0c71c853e2dfc Mon Sep 17 00:00:00 2001 From: Leoluis0705 Date: Wed, 25 Mar 2026 19:15:12 +0800 Subject: [PATCH 3/3] =?UTF-8?q?fix(analytics):=20=E5=AE=8C=E5=96=84?= =?UTF-8?q?=E7=BE=A4=E6=88=90=E5=91=98=E5=88=86=E6=9E=90=E5=A4=B1=E8=B4=A5?= =?UTF-8?q?=E6=97=B6=E7=9A=84=E9=94=99=E8=AF=AF=E8=BE=B9=E7=95=8C=E5=A4=84?= =?UTF-8?q?=E7=90=86=E4=B8=8EUI=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- electron/services/groupAnalyticsService.ts | 4 +++- src/pages/GroupAnalyticsPage.tsx | 7 ++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/electron/services/groupAnalyticsService.ts b/electron/services/groupAnalyticsService.ts index 79477ac..148f6c1 100644 --- a/electron/services/groupAnalyticsService.ts +++ b/electron/services/groupAnalyticsService.ts @@ -1531,7 +1531,9 @@ class GroupAnalyticsService { try { while (true) { const batch = await wcdbService.fetchMessageBatch(cursor) - if (!batch.success) break + if (!batch.success) { + return { success: false, error: batch.error || '获取分析数据失败' } + } const rows = Array.isArray(batch.rows) ? batch.rows as Record[] : [] if (rows.length === 0) break diff --git a/src/pages/GroupAnalyticsPage.tsx b/src/pages/GroupAnalyticsPage.tsx index a31dafc..7a6470f 100644 --- a/src/pages/GroupAnalyticsPage.tsx +++ b/src/pages/GroupAnalyticsPage.tsx @@ -168,6 +168,7 @@ function GroupAnalyticsPage() { const [isExportingMemberMessages, setIsExportingMemberMessages] = useState(false) const [memberMessages, setMemberMessages] = useState([]) const [memberAnalyticsData, setMemberAnalyticsData] = useState(null) + const [analyticsError, setAnalyticsError] = useState(null) const [memberMessagesHasMore, setMemberMessagesHasMore] = useState(false) const [memberMessagesCursor, setMemberMessagesCursor] = useState(0) const [memberMessagesLoadingMore, setMemberMessagesLoadingMore] = useState(false) @@ -570,6 +571,7 @@ function GroupAnalyticsPage() { } case 'memberAnalytics': { setMemberAnalyticsData(null) + setAnalyticsError(null) updateBackgroundTask(taskId, { detail: '正在读取成员列表与消息分析', progressText: '成员分析' @@ -611,6 +613,7 @@ function GroupAnalyticsPage() { progressText: '已完成' }) } else { + setAnalyticsError(analyticsResult.error || '分析失败') finishBackgroundTask(taskId, 'failed', { detail: analyticsResult.error || '分析失败' }) } break @@ -1434,7 +1437,9 @@ function GroupAnalyticsPage() { )}
- {memberAnalyticsData ? ( + {analyticsError ? ( +
{analyticsError}
+ ) : memberAnalyticsData ? (