diff --git a/internal/handlers/review.go b/internal/handlers/review.go index 6bf8754..119a983 100644 --- a/internal/handlers/review.go +++ b/internal/handlers/review.go @@ -174,7 +174,6 @@ func reviewBooksByDateForUsers(db *gorm.DB, userIDs []uint, dayStart, dayEnd tim type aggKey struct { StudentID uint - SessionID uint WordBookID uint } type aggVal struct { @@ -209,7 +208,7 @@ func reviewBooksByDateForUsers(db *gorm.DB, userIDs []uint, dayStart, dayEnd tim continue } - key := aggKey{StudentID: r.StudentID, SessionID: r.SessionID, WordBookID: r.WordBookID} + key := aggKey{StudentID: r.StudentID, WordBookID: r.WordBookID} cur, ok := aggs[key] if !ok { cur = &aggVal{ @@ -227,6 +226,17 @@ func reviewBooksByDateForUsers(db *gorm.DB, userIDs []uint, dayStart, dayEnd tim } aggs[key] = cur } + // 合并同一学员同词库的多节课:取最早开始、最晚结束 + if r.PracticeStartedAt != nil { + if cur.stat.PracticeStartedAt == nil || r.PracticeStartedAt.Before(*cur.stat.PracticeStartedAt) { + cur.stat.PracticeStartedAt = r.PracticeStartedAt + } + } + if r.PracticeEndedAt != nil { + if cur.stat.PracticeEndedAt == nil || r.PracticeEndedAt.After(*cur.stat.PracticeEndedAt) { + cur.stat.PracticeEndedAt = r.PracticeEndedAt + } + } cur.words[r.WordID] = struct{}{} } @@ -259,7 +269,6 @@ func reviewBooksByDateForUsers(db *gorm.DB, userIDs []uint, dayStart, dayEnd tim return stats, nil } - // handleReviewToday GET /review/today?wordBookId=1&date=YYYY-MM-DD&timeZone=Asia/Shanghai&all=true // 取词口径与 /review/books-by-date 对齐:今日含逾期至本地明日 0 点前;其它日仅该日 due。 // all=true 时忽略日期,返回所有已学待复习单词(不限 due_at)。 diff --git a/web/src/components/CoachingSchedulePanel.tsx b/web/src/components/CoachingSchedulePanel.tsx index 1c7ec7d..49ca116 100644 --- a/web/src/components/CoachingSchedulePanel.tsx +++ b/web/src/components/CoachingSchedulePanel.tsx @@ -214,6 +214,7 @@ function layoutDayEvents( zIndex: number; }> { const span = Math.max(1, axisEnd - axisStart); + const STACK_OFFSET_PX = 8; const raw = items.map((schedule) => { const range = scheduleVisualRange(schedule); let s = parseHmToMinutes(range.start); @@ -258,30 +259,30 @@ function layoutDayEvents( const normalHeightPx = Math.max(EVENT_MIN_H, ((ev.end - ev.start) / span) * axisHeightPx); const group = groupById.get(ev.schedule.id) || { key: String(ev.schedule.id), index: 0, count: 1 }; const expanded = group.count > 1 && group.key === expandedGroupKey; - const collapsedHeightPx = Math.max(EVENT_MIN_H, Math.min(normalHeightPx, 72)); + const collapsedHeightPx = Math.max(EVENT_MIN_H, Math.min(normalHeightPx, 48)); const heightPx = group.count === 1 ? normalHeightPx : expanded - ? Math.max(52, Math.min(normalHeightPx, 88)) + ? Math.max(56, Math.min(normalHeightPx, 64)) : collapsedHeightPx; const offsetPx = group.count === 1 ? 0 : expanded - ? group.index * Math.min(48, Math.max(32, heightPx * 0.55)) - : group.index * 8; + ? group.index * 44 + : group.index * STACK_OFFSET_PX; const collapsedTopPx = ((groupStartByKey.get(group.key) ?? ev.start) - axisStart) / span * axisHeightPx; return { schedule: ev.schedule, - topPx: (!expanded && group.count > 1 ? collapsedTopPx : topPx) + offsetPx, + topPx: (group.count > 1 ? collapsedTopPx : topPx) + offsetPx, heightPx, - showDetail: group.count === 1 ? normalHeightPx >= 40 : expanded || group.index === 0, + showDetail: group.count === 1 ? normalHeightPx >= 40 : expanded || group.index === group.count - 1, col: 0, colCount: 1, overlapGroupKey: group.key, overlapIndex: group.index, overlapCount: group.count, - zIndex: raisedId === ev.schedule.id ? 30 : 10 + (group.count - group.index), + zIndex: raisedId === ev.schedule.id ? 30 : 10 + group.index, }; }); } @@ -331,11 +332,12 @@ function TimetableBlock({ return (