feat: 优化足迹总结与自身消息强度分析

This commit is contained in:
clearyss
2026-05-27 19:59:48 +08:00
parent ca6c479496
commit a67959dc2a
8 changed files with 503 additions and 53 deletions

View File

@@ -184,6 +184,31 @@
color: var(--text-primary);
margin: 0 0 12px;
}
.chart-title-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin: 0 0 12px;
h3 {
margin: 0;
}
span {
font-size: 12px;
color: var(--text-tertiary);
white-space: nowrap;
}
}
.chart-note {
margin: -4px 0 10px;
font-size: 12px;
line-height: 1.6;
color: var(--text-tertiary);
}
}
// Rankings

View File

@@ -37,7 +37,19 @@ function AnalyticsPage() {
const [draftExcluded, setDraftExcluded] = useState<Set<string>>(new Set())
const themeMode = useThemeStore((state) => state.themeMode)
const { statistics, rankings, timeDistribution, isLoaded, setStatistics, setRankings, setTimeDistribution, markLoaded, clearCache } = useAnalyticsStore()
const {
statistics,
rankings,
timeDistribution,
selfSentDailyDistribution,
isLoaded,
setStatistics,
setRankings,
setTimeDistribution,
setSelfSentDailyDistribution,
markLoaded,
clearCache
} = useAnalyticsStore()
const loadExcludedUsernames = useCallback(async () => {
try {
@@ -54,7 +66,14 @@ function AnalyticsPage() {
}, [])
const loadData = useCallback(async (forceRefresh = false) => {
if (isLoaded && !forceRefresh) return
const currentAnalyticsState = useAnalyticsStore.getState()
if (
currentAnalyticsState.isLoaded &&
!forceRefresh &&
currentAnalyticsState.statistics &&
currentAnalyticsState.timeDistribution &&
currentAnalyticsState.selfSentDailyDistribution
) return
const taskId = registerBackgroundTask({
sourcePage: 'analytics',
title: forceRefresh ? '刷新分析看板' : '加载分析看板',
@@ -128,6 +147,22 @@ function AnalyticsPage() {
if (timeResult.success && timeResult.data) {
setTimeDistribution(timeResult.data)
}
setLoadingStatus('正在统计每日发送分布...')
updateBackgroundTask(taskId, {
detail: '正在统计每日发送分布',
progressText: '每日发送'
})
const selfSentDailyResult = await window.electronAPI.analytics.getSelfSentDailyDistribution(0, 0, forceRefresh)
if (isBackgroundTaskCancelRequested(taskId)) {
finishBackgroundTask(taskId, 'canceled', {
detail: '已停止后续加载,每日发送分布结果未继续写入'
})
setIsLoading(false)
return
}
if (selfSentDailyResult.success && selfSentDailyResult.data) {
setSelfSentDailyDistribution(selfSentDailyResult.data)
}
markLoaded()
finishBackgroundTask(taskId, 'completed', {
detail: '分析看板数据加载完成',
@@ -142,7 +177,7 @@ function AnalyticsPage() {
setIsLoading(false)
if (removeListener) removeListener()
}
}, [isLoaded, markLoaded, setRankings, setStatistics, setTimeDistribution])
}, [markLoaded, setRankings, setSelfSentDailyDistribution, setStatistics, setTimeDistribution])
const location = useLocation()
@@ -417,6 +452,105 @@ function AnalyticsPage() {
}
}
const getSelfSentDailyRatioData = () => {
const entries = Object.entries(selfSentDailyDistribution?.dailyDistribution || {})
.sort(([a], [b]) => a.localeCompare(b))
const days = entries.map(([day]) => day)
const counts = entries.map(([, count]) => count)
const totalDays = Math.max(days.length, 1)
const total = counts.reduce((sum, count) => sum + count, 0)
const baseline = total > 0 ? total / totalDays : 0
const ratios = counts.map((count) => baseline > 0 ? Number((count / baseline * 100).toFixed(1)) : 0)
const movingAverage = ratios.map((_, index) => {
const start = Math.max(0, index - 6)
const windowValues = ratios.slice(start, index + 1)
const sum = windowValues.reduce((total, value) => total + value, 0)
return Number((sum / windowValues.length).toFixed(1))
})
return { days, counts, ratios, movingAverage, baseline, total }
}
const getSelfSentDailyRatioOption = () => {
if (!selfSentDailyDistribution) return {}
const { days, counts, ratios, movingAverage, baseline } = getSelfSentDailyRatioData()
const showZoom = days.length > 31
const zoomStart = showZoom ? Math.max(0, 100 - Math.min(100, 31 / days.length * 100)) : 0
return {
tooltip: {
trigger: 'axis',
formatter: (params: any) => {
const items = Array.isArray(params) ? params : [params]
const first = items[0]
const index = Number(first?.dataIndex || 0)
const lines = [
`${first?.axisValue || ''}`,
`当日发送:${formatNumber(counts[index] || 0)}`,
`相对日均:${formatNumber(ratios[index] || 0)}%`,
`7日均线${formatNumber(movingAverage[index] || 0)}%`,
`全期日均:${baseline.toFixed(1)} 条/天`
]
return lines.join('<br/>')
}
},
legend: { data: ['单日比例', '7日均线'], top: 0 },
grid: { left: 56, right: 24, top: 42, bottom: showZoom ? 58 : 32 },
xAxis: {
type: 'category',
data: days,
axisLabel: {
hideOverlap: true,
formatter: (value: string) => value.slice(5)
}
},
yAxis: {
type: 'value',
name: '相对日均',
axisLabel: {
formatter: '{value}%'
}
},
dataZoom: showZoom ? [
{ type: 'inside', start: zoomStart, end: 100 },
{ type: 'slider', height: 18, bottom: 16, start: zoomStart, end: 100 }
] : undefined,
series: [
{
name: '单日比例',
type: 'bar',
data: ratios,
itemStyle: {
color: (params: any) => {
const value = Number(params?.value || 0)
if (value >= 200) return '#ff4d4f'
if (value >= 150) return '#faad14'
return '#07c160'
},
borderRadius: [4, 4, 0, 0]
},
markLine: {
symbol: 'none',
data: [{ yAxis: 100, name: '日均基线' }],
label: { formatter: '日均基线' },
lineStyle: { type: 'dashed', color: '#8c8c8c' }
}
},
{
name: '7日均线',
type: 'line',
data: movingAverage,
smooth: true,
showSymbol: false,
lineStyle: { width: 2, color: '#1989fa' },
itemStyle: { color: '#1989fa' }
}
]
}
}
const selfSentDailyRatioData = getSelfSentDailyRatioData()
const renderPageShell = (content: ReactNode) => (
<div className="analytics-page-shell">
<ChatAnalysisHeader currentMode="private" />
@@ -521,6 +655,16 @@ function AnalyticsPage() {
<div className="chart-card"><h3></h3><ReactECharts option={getTypeChartOption()} style={{ height: 300 }} /></div>
<div className="chart-card"><h3>/</h3><ReactECharts option={getSendReceiveOption()} style={{ height: 300 }} /></div>
<div className="chart-card wide"><h3></h3><ReactECharts option={getHourlyOption()} style={{ height: 250 }} /></div>
<div className="chart-card wide">
<div className="chart-title-row">
<h3></h3>
<span> · 线{selfSentDailyRatioData.baseline.toFixed(1)} / · {formatNumber(selfSentDailyDistribution?.totalMessages || 0)} </span>
</div>
<div className="chart-note">
= ÷ 100% 线
</div>
<ReactECharts option={getSelfSentDailyRatioOption()} style={{ height: 320 }} />
</div>
</div>
</section>
<section className="page-section">

View File

@@ -32,11 +32,22 @@ interface TimeDistribution {
monthlyDistribution: Record<string, number>
}
interface SelfSentDailyDistribution {
unit: 'day'
dailyDistribution: Record<string, number>
totalMessages: number
firstMessageTime: number | null
lastMessageTime: number | null
beginTimestamp: number
endTimestamp: number
}
interface AnalyticsState {
// 数据
statistics: ChatStatistics | null
rankings: ContactRanking[]
timeDistribution: TimeDistribution | null
selfSentDailyDistribution: SelfSentDailyDistribution | null
// 状态
isLoaded: boolean
@@ -46,6 +57,7 @@ interface AnalyticsState {
setStatistics: (data: ChatStatistics) => void
setRankings: (data: ContactRanking[]) => void
setTimeDistribution: (data: TimeDistribution) => void
setSelfSentDailyDistribution: (data: SelfSentDailyDistribution) => void
markLoaded: () => void
clearCache: () => void
}
@@ -56,17 +68,20 @@ export const useAnalyticsStore = create<AnalyticsState>()(
statistics: null,
rankings: [],
timeDistribution: null,
selfSentDailyDistribution: null,
isLoaded: false,
lastLoadTime: null,
setStatistics: (data) => set({ statistics: data }),
setRankings: (data) => set({ rankings: data }),
setTimeDistribution: (data) => set({ timeDistribution: data }),
setSelfSentDailyDistribution: (data) => set({ selfSentDailyDistribution: data }),
markLoaded: () => set({ isLoaded: true, lastLoadTime: Date.now() }),
clearCache: () => set({
statistics: null,
rankings: [],
timeDistribution: null,
selfSentDailyDistribution: null,
isLoaded: false,
lastLoadTime: null
}),

View File

@@ -785,6 +785,19 @@ export interface ElectronAPI {
}
error?: string
}>
getSelfSentDailyDistribution: (beginTimestamp?: number, endTimestamp?: number, force?: boolean) => Promise<{
success: boolean
data?: {
unit: 'day'
dailyDistribution: Record<string, number>
totalMessages: number
firstMessageTime: number | null
lastMessageTime: number | null
beginTimestamp: number
endTimestamp: number
}
error?: string
}>
getExcludedUsernames: () => Promise<{
success: boolean
data?: string[]