diff --git a/electron/main/ipc/chat.ts b/electron/main/ipc/chat.ts index eb30380..6d300fa 100644 --- a/electron/main/ipc/chat.ts +++ b/electron/main/ipc/chat.ts @@ -323,6 +323,21 @@ export function registerChatHandlers(ctx: IpcContext): void { } ) + /** + * 获取年份活跃度分布 + */ + ipcMain.handle( + 'chat:getYearlyActivity', + async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => { + try { + return await worker.getYearlyActivity(sessionId, filter) + } catch (error) { + console.error('获取年份活跃度失败:', error) + return [] + } + } + ) + /** * 获取消息类型分布 */ diff --git a/electron/main/worker/dbWorker.ts b/electron/main/worker/dbWorker.ts index 41c716d..e875d0f 100644 --- a/electron/main/worker/dbWorker.ts +++ b/electron/main/worker/dbWorker.ts @@ -18,6 +18,7 @@ import { getDailyActivity, getWeekdayActivity, getMonthlyActivity, + getYearlyActivity, getMessageTypeDistribution, getTimeRange, getMemberNameHistory, @@ -82,6 +83,7 @@ const syncHandlers: Record any> = { getDailyActivity: (p) => getDailyActivity(p.sessionId, p.filter), getWeekdayActivity: (p) => getWeekdayActivity(p.sessionId, p.filter), getMonthlyActivity: (p) => getMonthlyActivity(p.sessionId, p.filter), + getYearlyActivity: (p) => getYearlyActivity(p.sessionId, p.filter), getMessageTypeDistribution: (p) => getMessageTypeDistribution(p.sessionId, p.filter), getTimeRange: (p) => getTimeRange(p.sessionId), getMemberNameHistory: (p) => getMemberNameHistory(p.sessionId, p.memberId), diff --git a/electron/main/worker/query/basic.ts b/electron/main/worker/query/basic.ts index ad07aae..b54847a 100644 --- a/electron/main/worker/query/basic.ts +++ b/electron/main/worker/query/basic.ts @@ -242,6 +242,37 @@ export function getMonthlyActivity(sessionId: string, filter?: TimeFilter): any[ return result } +/** + * 获取年份活跃度分布 + */ +export function getYearlyActivity(sessionId: string, filter?: TimeFilter): any[] { + const db = openDatabase(sessionId) + if (!db) return [] + + const { clause, params } = buildTimeFilter(filter) + const clauseWithSystem = buildSystemMessageFilter(clause) + + const rows = db + .prepare( + ` + SELECT + CAST(strftime('%Y', msg.ts, 'unixepoch', 'localtime') AS INTEGER) as year, + COUNT(*) as messageCount + FROM message msg + JOIN member m ON msg.sender_id = m.id + ${clauseWithSystem} + GROUP BY year + ORDER BY year + ` + ) + .all(...params) as Array<{ year: number; messageCount: number }> + + return rows.map((r) => ({ + year: r.year, + messageCount: r.messageCount, + })) +} + /** * 获取消息类型分布 */ diff --git a/electron/main/worker/query/index.ts b/electron/main/worker/query/index.ts index 7a8f0f4..4382650 100644 --- a/electron/main/worker/query/index.ts +++ b/electron/main/worker/query/index.ts @@ -11,6 +11,7 @@ export { getDailyActivity, getWeekdayActivity, getMonthlyActivity, + getYearlyActivity, getMessageTypeDistribution, getTimeRange, getMemberNameHistory, diff --git a/electron/main/worker/workerManager.ts b/electron/main/worker/workerManager.ts index f52c8e5..30dbed3 100644 --- a/electron/main/worker/workerManager.ts +++ b/electron/main/worker/workerManager.ts @@ -225,6 +225,10 @@ export async function getMonthlyActivity(sessionId: string, filter?: any): Promi return sendToWorker('getMonthlyActivity', { sessionId, filter }) } +export async function getYearlyActivity(sessionId: string, filter?: any): Promise { + return sendToWorker('getYearlyActivity', { sessionId, filter }) +} + export async function getMessageTypeDistribution(sessionId: string, filter?: any): Promise { return sendToWorker('getMessageTypeDistribution', { sessionId, filter }) } diff --git a/electron/preload/index.d.ts b/electron/preload/index.d.ts index 03e6a80..c602bd7 100644 --- a/electron/preload/index.d.ts +++ b/electron/preload/index.d.ts @@ -79,6 +79,10 @@ interface ChatApi { getDailyActivity: (sessionId: string, filter?: TimeFilter) => Promise getWeekdayActivity: (sessionId: string, filter?: TimeFilter) => Promise getMonthlyActivity: (sessionId: string, filter?: TimeFilter) => Promise + getYearlyActivity: ( + sessionId: string, + filter?: TimeFilter + ) => Promise> getMessageTypeDistribution: ( sessionId: string, filter?: TimeFilter diff --git a/electron/preload/index.ts b/electron/preload/index.ts index bf7817a..f443420 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -183,6 +183,16 @@ const chatApi = { return ipcRenderer.invoke('chat:getMonthlyActivity', sessionId, filter) }, + /** + * 获取年份活跃度分布 + */ + getYearlyActivity: ( + sessionId: string, + filter?: { startTs?: number; endTs?: number } + ): Promise> => { + return ipcRenderer.invoke('chat:getYearlyActivity', sessionId, filter) + }, + /** * 获取消息类型分布 */ diff --git a/src/i18n/locales/en-US/analysis.json b/src/i18n/locales/en-US/analysis.json index ca05460..716ecd5 100644 --- a/src/i18n/locales/en-US/analysis.json +++ b/src/i18n/locales/en-US/analysis.json @@ -13,6 +13,7 @@ }, "tabs": { "overview": "Overview", + "view": "View", "ranking": "Rankings", "groupQuotes": "Quotes", "quotes": "Quotes", diff --git a/src/i18n/locales/zh-CN/analysis.json b/src/i18n/locales/zh-CN/analysis.json index 390a1d7..80e433c 100644 --- a/src/i18n/locales/zh-CN/analysis.json +++ b/src/i18n/locales/zh-CN/analysis.json @@ -13,6 +13,7 @@ }, "tabs": { "overview": "总览", + "view": "视图", "ranking": "群榜单", "groupQuotes": "群语录", "quotes": "语录", diff --git a/src/pages/private-chat/components/ViewTab.vue b/src/pages/private-chat/components/ViewTab.vue new file mode 100644 index 0000000..334119e --- /dev/null +++ b/src/pages/private-chat/components/ViewTab.vue @@ -0,0 +1,91 @@ + + + + + + + +{ + "zh-CN": { + "message": "消息", + "wordcloud": "词云", + "portrait": "对话画像" + }, + "en-US": { + "message": "Messages", + "wordcloud": "Word Cloud", + "portrait": "Chat Portrait" + } +} + + diff --git a/src/pages/private-chat/components/view/MessageView.vue b/src/pages/private-chat/components/view/MessageView.vue new file mode 100644 index 0000000..7ec3900 --- /dev/null +++ b/src/pages/private-chat/components/view/MessageView.vue @@ -0,0 +1,420 @@ + + + + + +{ + "zh-CN": { + "typeDistribution": "消息类型分布", + "hourlyDistribution": "小时分布", + "weekdayDistribution": "星期分布", + "monthlyDistribution": "月份分布", + "yearlyDistribution": "年份分布", + "timeHeatmap": "时间热力图", + "heatmapHint": "展示聊天时间规律", + "lengthDistribution": "消息长度分布", + "memberTypeComparison": "双方类型对比", + "noData": "暂无数据", + "comingSoon": "功能开发中...", + "less": "少", + "more": "多", + "weekdays": { + "sun": "周日", + "mon": "周一", + "tue": "周二", + "wed": "周三", + "thu": "周四", + "fri": "周五", + "sat": "周六" + }, + "months": { + "jan": "1月", + "feb": "2月", + "mar": "3月", + "apr": "4月", + "may": "5月", + "jun": "6月", + "jul": "7月", + "aug": "8月", + "sep": "9月", + "oct": "10月", + "nov": "11月", + "dec": "12月" + } + }, + "en-US": { + "typeDistribution": "Message Type Distribution", + "hourlyDistribution": "Hourly Distribution", + "weekdayDistribution": "Weekday Distribution", + "monthlyDistribution": "Monthly Distribution", + "yearlyDistribution": "Yearly Distribution", + "timeHeatmap": "Time Heatmap", + "heatmapHint": "Shows chat time patterns", + "lengthDistribution": "Message Length Distribution", + "memberTypeComparison": "Member Type Comparison", + "noData": "No data", + "comingSoon": "Coming soon...", + "less": "Less", + "more": "More", + "weekdays": { + "sun": "Sun", + "mon": "Mon", + "tue": "Tue", + "wed": "Wed", + "thu": "Thu", + "fri": "Fri", + "sat": "Sat" + }, + "months": { + "jan": "Jan", + "feb": "Feb", + "mar": "Mar", + "apr": "Apr", + "may": "May", + "jun": "Jun", + "jul": "Jul", + "aug": "Aug", + "sep": "Sep", + "oct": "Oct", + "nov": "Nov", + "dec": "Dec" + } + } +} + diff --git a/src/pages/private-chat/components/view/PortraitView.vue b/src/pages/private-chat/components/view/PortraitView.vue new file mode 100644 index 0000000..4b92137 --- /dev/null +++ b/src/pages/private-chat/components/view/PortraitView.vue @@ -0,0 +1,48 @@ + + + + + +{ + "zh-CN": { + "title": "对话画像", + "description": "双方发言特征对比(消息长度、表情使用、消息类型等)" + }, + "en-US": { + "title": "Chat Portrait", + "description": "Comparison of chat characteristics (message length, emoji usage, message types, etc.)" + } +} + + diff --git a/src/pages/private-chat/components/view/TimelineView.vue b/src/pages/private-chat/components/view/TimelineView.vue new file mode 100644 index 0000000..b1205b3 --- /dev/null +++ b/src/pages/private-chat/components/view/TimelineView.vue @@ -0,0 +1,48 @@ + + + + + +{ + "zh-CN": { + "title": "时间线视图", + "description": "消息时间分布、活跃周期变化趋势等可视化内容" + }, + "en-US": { + "title": "Timeline View", + "description": "Message time distribution, active period trends and other visualizations" + } +} + + diff --git a/src/pages/private-chat/components/view/WordcloudView.vue b/src/pages/private-chat/components/view/WordcloudView.vue new file mode 100644 index 0000000..6c7a044 --- /dev/null +++ b/src/pages/private-chat/components/view/WordcloudView.vue @@ -0,0 +1,48 @@ + + + + + +{ + "zh-CN": { + "title": "词云视图", + "description": "双方高频词汇对比可视化" + }, + "en-US": { + "title": "Word Cloud View", + "description": "Visual comparison of frequently used words between both parties" + } +} + + diff --git a/src/pages/private-chat/index.vue b/src/pages/private-chat/index.vue index f3d05f6..f836b2a 100644 --- a/src/pages/private-chat/index.vue +++ b/src/pages/private-chat/index.vue @@ -10,6 +10,7 @@ import CaptureButton from '@/components/common/CaptureButton.vue' import UITabs from '@/components/UI/Tabs.vue' import AITab from '@/components/analysis/AITab.vue' import OverviewTab from './components/OverviewTab.vue' +import ViewTab from './components/ViewTab.vue' import QuotesTab from './components/QuotesTab.vue' import MemberTab from './components/MemberTab.vue' import PageHeader from '@/components/layout/PageHeader.vue' @@ -52,9 +53,10 @@ const availableYears = ref([]) const selectedYear = ref(0) // 0 表示全部 const isInitialLoad = ref(true) // 用于跳过初始加载时的 watch 触发,并控制首屏加载状态 -// Tab 配置 - 私聊有总览、语录、成员、AI实验室(趋势已合并到总览) +// Tab 配置 - 私聊有总览、视图、语录、成员、AI实验室 const tabs = [ { id: 'overview', labelKey: 'analysis.tabs.overview', icon: 'i-heroicons-chart-pie' }, + { id: 'view', labelKey: 'analysis.tabs.view', icon: 'i-heroicons-presentation-chart-bar' }, { id: 'quotes', labelKey: 'analysis.tabs.quotes', icon: 'i-heroicons-chat-bubble-left-right' }, { id: 'member', labelKey: 'analysis.tabs.member', icon: 'i-heroicons-user-group' }, { id: 'ai', labelKey: 'analysis.tabs.ai', icon: 'i-heroicons-sparkles' }, @@ -350,6 +352,12 @@ onMounted(() => { :filtered-member-count="filteredMemberCount" :time-filter="timeFilter" /> +