From 2f4fbeaae8506d3fe8ee12987b96fadd4afad2d8 Mon Sep 17 00:00:00 2001 From: digua Date: Fri, 28 Nov 2025 18:15:38 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E7=BB=9F=E8=AE=A1?= =?UTF-8?q?=E9=BE=99=E7=8E=8B=E5=92=8C=E6=BD=9C=E6=B0=B4=E6=8E=92=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- electron/main/database/analysis.ts | 139 ++++++++++++++++++++++++ electron/main/database/index.ts | 2 + electron/main/ipcMain.ts | 30 +++++ electron/preload/index.d.ts | 4 + electron/preload/index.ts | 22 ++++ src/components/AnalysisDashboard.vue | 8 +- src/components/analysis/MembersTab.vue | 49 ++++++++- src/components/analysis/TimelineTab.vue | 125 ++++++++++++++++++++- src/types/chat.ts | 37 +++++++ 9 files changed, 411 insertions(+), 5 deletions(-) diff --git a/electron/main/database/analysis.ts b/electron/main/database/analysis.ts index bcc3057..d622b0c 100644 --- a/electron/main/database/analysis.ts +++ b/electron/main/database/analysis.ts @@ -21,6 +21,10 @@ import type { TimeRankItem, ConsecutiveNightRecord, NightOwlChampion, + DragonKingAnalysis, + DragonKingRankItem, + DivingAnalysis, + DivingRankItem, } from '../../../src/types/chat' import { openDatabase } from './core' @@ -1012,3 +1016,138 @@ export function getNightOwlAnalysis(sessionId: string, filter?: TimeFilter): Nig db.close() } } + +/** + * 获取龙王排名 + * 每天发言最多的人+1,统计所有天数 + */ +export function getDragonKingAnalysis(sessionId: string, filter?: TimeFilter): DragonKingAnalysis { + const db = openDatabase(sessionId) + const emptyResult: DragonKingAnalysis = { + rank: [], + totalDays: 0, + } + + if (!db) return emptyResult + + try { + const { clause, params } = buildTimeFilter(filter) + const clauseWithSystem = buildSystemMessageFilter(clause) + + // 查询每天每个人的发言数,找出每天的龙王 + const dailyTopSpeakers = db + .prepare( + ` + WITH daily_counts AS ( + SELECT + strftime('%Y-%m-%d', msg.ts, 'unixepoch', 'localtime') as date, + msg.sender_id, + m.platform_id, + m.name, + COUNT(*) as msg_count + FROM message msg + JOIN member m ON msg.sender_id = m.id + ${clauseWithSystem} + GROUP BY date, msg.sender_id + ), + daily_max AS ( + SELECT date, MAX(msg_count) as max_count + FROM daily_counts + GROUP BY date + ) + SELECT dc.sender_id, dc.platform_id, dc.name, COUNT(*) as dragon_days + FROM daily_counts dc + JOIN daily_max dm ON dc.date = dm.date AND dc.msg_count = dm.max_count + GROUP BY dc.sender_id + ORDER BY dragon_days DESC + ` + ) + .all(...params) as Array<{ + sender_id: number + platform_id: string + name: string + dragon_days: number + }> + + // 获取总天数 + const totalDaysRow = db + .prepare( + ` + SELECT COUNT(DISTINCT strftime('%Y-%m-%d', msg.ts, 'unixepoch', 'localtime')) as total + FROM message msg + JOIN member m ON msg.sender_id = m.id + ${clauseWithSystem} + ` + ) + .get(...params) as { total: number } + + const totalDays = totalDaysRow.total + + const rank: DragonKingRankItem[] = dailyTopSpeakers.map((item) => ({ + memberId: item.sender_id, + platformId: item.platform_id, + name: item.name, + count: item.dragon_days, + percentage: totalDays > 0 ? Math.round((item.dragon_days / totalDays) * 10000) / 100 : 0, + })) + + return { rank, totalDays } + } finally { + db.close() + } +} + +/** + * 获取潜水排名 + * 所有人的最后一次发言记录,按时间倒序(最久没发言的在前面) + */ +export function getDivingAnalysis(sessionId: string, filter?: TimeFilter): DivingAnalysis { + const db = openDatabase(sessionId) + const emptyResult: DivingAnalysis = { + rank: [], + } + + if (!db) return emptyResult + + try { + const { clause, params } = buildTimeFilter(filter) + const clauseWithSystem = buildSystemMessageFilter(clause) + + // 查询每个成员的最后发言时间 + const lastMessages = db + .prepare( + ` + SELECT + m.id as member_id, + m.platform_id, + m.name, + MAX(msg.ts) as last_ts + FROM member m + JOIN message msg ON m.id = msg.sender_id + ${clauseWithSystem.replace('msg.', 'msg.')} + GROUP BY m.id + ORDER BY last_ts ASC + ` + ) + .all(...params) as Array<{ + member_id: number + platform_id: string + name: string + last_ts: number + }> + + const now = Math.floor(Date.now() / 1000) + + const rank: DivingRankItem[] = lastMessages.map((item) => ({ + memberId: item.member_id, + platformId: item.platform_id, + name: item.name, + lastMessageTs: item.last_ts, + daysSinceLastMessage: Math.floor((now - item.last_ts) / 86400), + })) + + return { rank } + } finally { + db.close() + } +} diff --git a/electron/main/database/index.ts b/electron/main/database/index.ts index 120a3a0..530c69d 100644 --- a/electron/main/database/index.ts +++ b/electron/main/database/index.ts @@ -19,6 +19,8 @@ export { getRepeatAnalysis, getCatchphraseAnalysis, getNightOwlAnalysis, + getDragonKingAnalysis, + getDivingAnalysis, } from './analysis' // 类型导出 diff --git a/electron/main/ipcMain.ts b/electron/main/ipcMain.ts index 9e8780e..2d6a619 100644 --- a/electron/main/ipcMain.ts +++ b/electron/main/ipcMain.ts @@ -427,6 +427,36 @@ const mainIpcMain = (win: BrowserWindow) => { } } ) + + /** + * 获取龙王分析数据 + */ + ipcMain.handle( + 'chat:getDragonKingAnalysis', + async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => { + try { + return database.getDragonKingAnalysis(sessionId, filter) + } catch (error) { + console.error('获取龙王分析失败:', error) + return { rank: [], totalDays: 0 } + } + } + ) + + /** + * 获取潜水分析数据 + */ + ipcMain.handle( + 'chat:getDivingAnalysis', + async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => { + try { + return database.getDivingAnalysis(sessionId, filter) + } catch (error) { + console.error('获取潜水分析失败:', error) + return { rank: [] } + } + } + ) } export default mainIpcMain diff --git a/electron/preload/index.d.ts b/electron/preload/index.d.ts index 40d6b6b..82a2c13 100644 --- a/electron/preload/index.d.ts +++ b/electron/preload/index.d.ts @@ -11,6 +11,8 @@ import type { RepeatAnalysis, CatchphraseAnalysis, NightOwlAnalysis, + DragonKingAnalysis, + DivingAnalysis, } from '../../src/types/chat' interface TimeFilter { @@ -41,6 +43,8 @@ interface ChatApi { getRepeatAnalysis: (sessionId: string, filter?: TimeFilter) => Promise getCatchphraseAnalysis: (sessionId: string, filter?: TimeFilter) => Promise getNightOwlAnalysis: (sessionId: string, filter?: TimeFilter) => Promise + getDragonKingAnalysis: (sessionId: string, filter?: TimeFilter) => Promise + getDivingAnalysis: (sessionId: string, filter?: TimeFilter) => Promise } interface Api { diff --git a/electron/preload/index.ts b/electron/preload/index.ts index 38a303b..6a6e373 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -12,6 +12,8 @@ import type { RepeatAnalysis, CatchphraseAnalysis, NightOwlAnalysis, + DragonKingAnalysis, + DivingAnalysis, } from '../../src/types/chat' // Custom APIs for renderer @@ -190,6 +192,26 @@ const chatApi = { ): Promise => { return ipcRenderer.invoke('chat:getNightOwlAnalysis', sessionId, filter) }, + + /** + * 获取龙王分析数据 + */ + getDragonKingAnalysis: ( + sessionId: string, + filter?: { startTs?: number; endTs?: number } + ): Promise => { + return ipcRenderer.invoke('chat:getDragonKingAnalysis', sessionId, filter) + }, + + /** + * 获取潜水分析数据 + */ + getDivingAnalysis: ( + sessionId: string, + filter?: { startTs?: number; endTs?: number } + ): Promise => { + return ipcRenderer.invoke('chat:getDivingAnalysis', sessionId, filter) + }, } // Use `contextBridge` APIs to expose Electron APIs to diff --git a/src/components/AnalysisDashboard.vue b/src/components/AnalysisDashboard.vue index e5409c5..f2ee7bb 100644 --- a/src/components/AnalysisDashboard.vue +++ b/src/components/AnalysisDashboard.vue @@ -249,7 +249,13 @@ onMounted(loadData) :hourly-activity="hourlyActivity" :time-filter="timeFilter" /> - + diff --git a/src/components/analysis/MembersTab.vue b/src/components/analysis/MembersTab.vue index 02f039c..c607729 100644 --- a/src/components/analysis/MembersTab.vue +++ b/src/components/analysis/MembersTab.vue @@ -1,6 +1,6 @@ diff --git a/src/types/chat.ts b/src/types/chat.ts index 9e03751..f19ca33 100644 --- a/src/types/chat.ts +++ b/src/types/chat.ts @@ -212,6 +212,43 @@ export interface NightOwlChampion { consecutiveDays: number // 最长连续天数 } +/** + * 龙王排名项(每天发言最多的人) + */ +export interface DragonKingRankItem { + memberId: number + platformId: string + name: string + count: number // 成为龙王的天数 + percentage: number // 占总天数的百分比 +} + +/** + * 龙王分析结果 + */ +export interface DragonKingAnalysis { + rank: DragonKingRankItem[] + totalDays: number // 统计的总天数 +} + +/** + * 潜水排名项(最后发言时间) + */ +export interface DivingRankItem { + memberId: number + platformId: string + name: string + lastMessageTs: number // 最后发言时间戳(秒) + daysSinceLastMessage: number // 距离最后发言的天数 +} + +/** + * 潜水分析结果 + */ +export interface DivingAnalysis { + rank: DivingRankItem[] +} + /** * 夜猫分析完整结果 */