@@ -8,6 +8,11 @@ const toISO = (d: Date) => `${d.getFullYear()}-${String(d.getMonth() + 1).padSta
88const recent = < T > ( arr : T [ ] , dt : ( i : T ) => Date , days : number ) => { const c = Date . now ( ) - days * DAY_MS ; return arr . filter ( ( i ) => dt ( i ) . getTime ( ) >= c ) ; } ;
99const norm = ( v : number , m : number ) => ( m === 0 ? 0 : Math . min ( 100 , Math . round ( ( v / m ) * 100 ) ) ) ;
1010
11+ // ── 非番茄钟学习分钟折算(学习脉搏反映全部学习行为,内测反馈)──
12+ const NOTE_EDIT_MINUTES = 10 ; // 每篇笔记当天更新折算 10 分钟(按 id+日期去重防自动保存虚高)
13+ const REVIEW_MINUTES = 2 ; // 每次闪卡复习折算 2 分钟
14+ const FEYNMAN_MINUTES = 15 ; // 每篇费曼输出折算 15 分钟
15+
1116/** 聚合输入类型 */
1217export interface AggregateInput {
1318 sessions : PomodoroSession [ ] ; notes : Note [ ] ; flashcards : Flashcard [ ] ;
@@ -93,15 +98,43 @@ export function computeHeatmap(sessions: PomodoroSession[], days = 30): HeatmapC
9398 return cells ;
9499}
95100
96- /** 每日学习时长趋势(含 7 天滑动均值 label) */
97- export function computeTrend ( sessions : PomodoroSession [ ] , days = 30 ) : TrendPoint [ ] {
101+ /** 每日学习时长趋势(含 7 天滑动均值 label)
102+ * 数据源:番茄钟 sessions(实际分钟)+ 可选折算(笔记/复习/费曼)
103+ */
104+ export function computeTrend (
105+ sessions : PomodoroSession [ ] , days = 30 ,
106+ extras ?: { notes ?: Note [ ] ; reviews ?: FlashcardReview [ ] ; feynmanNotes ?: FeynmanNote [ ] } ,
107+ ) : TrendPoint [ ] {
98108 const cutoff = Date . now ( ) - days * DAY_MS ;
99109 const dm : Record < string , number > = { } ;
100110 sessions . forEach ( ( s ) => {
101111 const d = new Date ( s . completedAt ) ;
102112 if ( d . getTime ( ) < cutoff ) return ;
103113 const k = toISO ( d ) ; dm [ k ] = ( dm [ k ] ?? 0 ) + Math . round ( s . actualDuration / 60 ) ;
104114 } ) ;
115+
116+ // 非番茄钟学习折算(笔记按 (id, 日期) 去重:自动保存会频繁更新 updatedAt)
117+ const seenNotes = new Set < string > ( ) ;
118+ extras ?. notes ?. forEach ( ( n ) => {
119+ const d = new Date ( n . updatedAt ) ;
120+ if ( d . getTime ( ) < cutoff ) return ;
121+ const k = toISO ( d ) ;
122+ const key = `${ n . id } -${ k } ` ;
123+ if ( seenNotes . has ( key ) ) return ;
124+ seenNotes . add ( key ) ;
125+ dm [ k ] = ( dm [ k ] ?? 0 ) + NOTE_EDIT_MINUTES ;
126+ } ) ;
127+ extras ?. reviews ?. forEach ( ( r ) => {
128+ const d = new Date ( r . reviewedAt ) ;
129+ if ( d . getTime ( ) < cutoff ) return ;
130+ const k = toISO ( d ) ; dm [ k ] = ( dm [ k ] ?? 0 ) + REVIEW_MINUTES ;
131+ } ) ;
132+ extras ?. feynmanNotes ?. forEach ( ( f ) => {
133+ const d = new Date ( f . updatedAt ) ;
134+ if ( d . getTime ( ) < cutoff ) return ;
135+ const k = toISO ( d ) ; dm [ k ] = ( dm [ k ] ?? 0 ) + FEYNMAN_MINUTES ;
136+ } ) ;
137+
105138 const dates : string [ ] = [ ] ;
106139 for ( let i = days - 1 ; i >= 0 ; i -- ) {
107140 const k = toISO ( new Date ( Date . now ( ) - i * DAY_MS ) ) ;
@@ -154,7 +187,9 @@ export function aggregateAnalytics(data: AggregateInput, days = 30): AnalyticsAg
154187 return {
155188 radar : computeRadarData ( data . sessions , data . notes , data . flashcards , data . feynmanNotes , data . reviews , days ) ,
156189 heatmap,
157- trend : computeTrend ( data . sessions , days ) ,
190+ trend : computeTrend ( data . sessions , days , {
191+ notes : data . notes , reviews : data . reviews , feynmanNotes : data . feynmanNotes ,
192+ } ) ,
158193 recommendations : computeRecommendations ( heatmap ) ,
159194 period : { start : toISO ( new Date ( Date . now ( ) - days * DAY_MS ) ) , end : toISO ( new Date ( ) ) } ,
160195 goals : computeGoalProgress ( data . sessions , data . notes , data . reviews ) ,
0 commit comments