From 74d985acf0950229d959daef62ddf1b21eb52dd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E6=9D=B0?= <17793426456@163.com> Date: Tue, 8 Sep 2026 11:16:47 +0800 Subject: [PATCH 01/18] =?UTF-8?q?fix(coaching):=20=E7=82=B9=E5=87=BB?= =?UTF-8?q?=E7=A9=BA=E7=99=BD=E5=8C=BA=E5=9F=9F=E6=94=B6=E8=B5=B7=E5=B1=95?= =?UTF-8?q?=E5=BC=80=E8=AF=BE=E7=A8=8B=E5=8D=A1=E7=89=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 展开重叠课程后,点击课表空白区域恢复默认堆叠状态;教练模式下收起后再次点击才进入当天排课。 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- web/src/components/CoachingSchedulePanel.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/web/src/components/CoachingSchedulePanel.tsx b/web/src/components/CoachingSchedulePanel.tsx index 2f06df4..eb42f57 100644 --- a/web/src/components/CoachingSchedulePanel.tsx +++ b/web/src/components/CoachingSchedulePanel.tsx @@ -964,6 +964,12 @@ export function CoachingSchedulePanel({ nowTs, mode = "coach" }: Props) { dIdx < 6 ? "border-r border-border/30" : "" }`} style={{ height: axisHeightPx }} + onClick={() => { + if (expandedOverlapGroup) { + setExpandedOverlapGroup(null); + setRaisedOverlapId(null); + } + }} > {isPastDay ? (
{ dismissCellTip(); + if (expandedOverlapGroup) { + setExpandedOverlapGroup(null); + setRaisedOverlapId(null); + return; + } openScheduleForDay(d); }} /> From eec90328411cd2fcd0f3bacd9cd65999c003f45a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E6=9D=B0?= <17793426456@163.com> Date: Tue, 8 Sep 2026 11:24:39 +0800 Subject: [PATCH 02/18] =?UTF-8?q?fix(coaching):=20=E7=82=B9=E5=87=BB?= =?UTF-8?q?=E5=8D=A1=E7=89=87=E5=A4=96=E5=8C=BA=E5=9F=9F=E6=81=A2=E5=A4=8D?= =?UTF-8?q?=E8=AF=BE=E7=A8=8B=E5=A0=86=E5=8F=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 展开重叠课程后,点击课程卡片以外的任意区域自动收起,恢复默认堆叠状态。 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- web/src/components/CoachingSchedulePanel.tsx | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/web/src/components/CoachingSchedulePanel.tsx b/web/src/components/CoachingSchedulePanel.tsx index eb42f57..3baa1ff 100644 --- a/web/src/components/CoachingSchedulePanel.tsx +++ b/web/src/components/CoachingSchedulePanel.tsx @@ -302,6 +302,7 @@ function TimetableBlock({ return (
Date: Wed, 9 Sep 2026 15:12:31 +0800 Subject: [PATCH 05/18] =?UTF-8?q?fix(pre-training):=20=E5=88=A0=E9=99=A4?= =?UTF-8?q?=E5=BA=95=E6=A0=8F=E3=80=8C=E5=85=A8=E9=80=89=E3=80=8D=E6=8C=89?= =?UTF-8?q?=E9=92=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 只保留「✔5个」和「❌5个」两个按钮,移除「全选」按钮及相关未使用代码。 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- web/src/pages/PreTrainingCheck.tsx | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/web/src/pages/PreTrainingCheck.tsx b/web/src/pages/PreTrainingCheck.tsx index 08cab70..f41ae3f 100644 --- a/web/src/pages/PreTrainingCheck.tsx +++ b/web/src/pages/PreTrainingCheck.tsx @@ -303,16 +303,6 @@ export default function PreTrainingCheck() { void loadWords(1, true); }, [loadWords]); - const handleSelectAll = useCallback(() => { - setWords((prev) => { - const allSelected = prev.every((word) => word.status !== null); - if (allSelected) { - return prev.map((word) => ({ ...word, status: null as WordItem["status"] })); - } - return prev.map((word) => ({ ...word, status: "wrong" as WordItem["status"] })); - }); - }, []); - const handleSelect5 = useCallback((status: "correct" | "wrong") => { setWords((prev) => { const unselected = prev.filter((word) => word.status === null); @@ -680,14 +670,6 @@ export default function PreTrainingCheck() { {t("practice.shuffle")} )} - - {t("practice.select_all")} - Date: Wed, 9 Sep 2026 15:30:16 +0800 Subject: [PATCH 06/18] =?UTF-8?q?fix(anti-forgetting):=20=E5=90=8C?= =?UTF-8?q?=E4=B8=80=E8=8A=82=E8=AF=BE=E5=90=88=E5=B9=B6=E6=98=BE=E7=A4=BA?= =?UTF-8?q?=E4=B8=94=E8=AE=AD=E7=BB=83=E6=97=B6=E9=97=B4=E6=98=BE=E7=A4=BA?= =?UTF-8?q?=E4=B8=BA=E6=97=B6=E9=97=B4=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 后端 review.go: - aggKey 去掉 SessionID,按 StudentID+WordBookID 分组, 同一学员同词库的多节课次合并为一条 - 合并后取最早 startedAt 和最晚 endedAt 前端 AntiForgetting.tsx: - clockParts 接收 practiceEndedAt,trainingAt 显示为 "2026-09-06 17:05~17:20" 时间段格式 - 无 endedAt 时显示 "17:05~17:05" Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- internal/handlers/review.go | 15 ++++++++++++--- web/src/pages/AntiForgetting.tsx | 16 +++++++++++++--- 2 files changed, 25 insertions(+), 6 deletions(-) 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/pages/AntiForgetting.tsx b/web/src/pages/AntiForgetting.tsx index ac21c00..6620110 100644 --- a/web/src/pages/AntiForgetting.tsx +++ b/web/src/pages/AntiForgetting.tsx @@ -21,6 +21,7 @@ type ReviewTask = { timeSort: number; trainingAt: string; practiceStartedAt?: string; + practiceEndedAt?: string | null; }; type TimeSlotGroup = { @@ -36,7 +37,7 @@ function studentDisplayName(row: ReviewBookStatRow): string { return id ? i18n.t("student_detail.student_fallback", { id }) : i18n.t("anti_forgetting.current_user"); } -function clockParts(iso: string | null | undefined, tz: string) { +function clockParts(iso: string | null | undefined, endIso: string | null | undefined, tz: string) { if (!iso) return null; const d = new Date(iso); if (Number.isNaN(d.getTime())) return null; @@ -58,10 +59,18 @@ function clockParts(iso: string | null | undefined, tz: string) { const day = parts.find((p) => p.type === "day")?.value ?? ""; const slot = timeFmt.format(d); const [hh, mm] = slot.split(":").map((x) => Number(x)); + // 结束时间:有 endedAt 则用,否则同开始时间(显示 17:05~17:05) + let endSlot = slot; + if (endIso) { + const endD = new Date(endIso); + if (!Number.isNaN(endD.getTime())) { + endSlot = timeFmt.format(endD); + } + } return { slot, sort: (Number.isFinite(hh) ? hh : 0) * 60 + (Number.isFinite(mm) ? mm : 0), - trainingAt: `${y}-${mo}-${day} ${slot}`, + trainingAt: `${y}-${mo}-${day} ${slot}~${endSlot}`, }; } @@ -118,7 +127,7 @@ export default function AntiForgetting() { const studentId = normalizeSnowflakeId(b.studentId) || "self"; const wordBookId = normalizeSnowflakeId(b.wordBookId); const sessionId = normalizeSnowflakeId(b.sessionId); - const clock = clockParts(b.practiceStartedAt, timeZone); + const clock = clockParts(b.practiceStartedAt, b.practiceEndedAt, timeZone); return { id: `${studentId}-${wordBookId}-${sessionId || "0"}`, studentId, @@ -132,6 +141,7 @@ export default function AntiForgetting() { timeSort: clock?.sort ?? 9999, trainingAt: clock?.trainingAt || "—", practiceStartedAt: b.practiceStartedAt, + practiceEndedAt: b.practiceEndedAt, }; }); }, [bookStats, timeZone]); From f691379ee8714308fa020e93305d451af1b106ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E6=9D=B0?= <17793426456@163.com> Date: Wed, 9 Sep 2026 15:33:35 +0800 Subject: [PATCH 07/18] =?UTF-8?q?fix(pre-training):=20=E6=8C=89=E5=8F=82?= =?UTF-8?q?=E8=80=83=E6=A0=B7=E5=BC=8F=E8=B0=83=E6=95=B4=E6=89=B9=E9=87=8F?= =?UTF-8?q?=E6=A0=87=E8=AE=B0=E6=8C=89=E9=92=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将「5个正确」和「5个错误」按钮改为薄荷绿/红色实心圆角样式, 分别使用勾号和叉号图标,并同步更新中英文文案。 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- web/src/i18n/locales/en.json | 4 ++-- web/src/i18n/locales/zh-CN.json | 4 ++-- web/src/pages/PreTrainingCheck.tsx | 10 ++++++---- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index 66d9d18..44ed7d2 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -1514,8 +1514,8 @@ "practice.simple_tip_on": "Simple mode: tap for full expand", "practice.simple_tip_off": "Full mode: tap for simple", "practice.select_five": "Select 5", - "practice.mark_correct_5": "✔5", - "practice.mark_wrong_5": "❌5", + "practice.mark_correct_5": "5 correct", + "practice.mark_wrong_5": "5 incorrect", "practice.start_learning": "Start learning", "practice.start_learning_count": "Learn {{count}} words", "practice.screened_count": "{{count}} screened", diff --git a/web/src/i18n/locales/zh-CN.json b/web/src/i18n/locales/zh-CN.json index b711b95..9ad1d09 100644 --- a/web/src/i18n/locales/zh-CN.json +++ b/web/src/i18n/locales/zh-CN.json @@ -1226,8 +1226,8 @@ "practice.simple_tip_on": "当前简易:点击查看全部拓展", "practice.simple_tip_off": "当前全部:点击切回简易", "practice.select_five": "选择5个", - "practice.mark_correct_5": "✔5个", - "practice.mark_wrong_5": "❌5个", + "practice.mark_correct_5": "5个正确", + "practice.mark_wrong_5": "5个错误", "practice.start_learning": "开始识记", "practice.start_learning_count": "记 {{count}} 个", "practice.screened_count": "已筛 {{count}} 个", diff --git a/web/src/pages/PreTrainingCheck.tsx b/web/src/pages/PreTrainingCheck.tsx index f41ae3f..cc35db8 100644 --- a/web/src/pages/PreTrainingCheck.tsx +++ b/web/src/pages/PreTrainingCheck.tsx @@ -671,19 +671,21 @@ export default function PreTrainingCheck() { )} handleSelect5("correct")} > + {t("practice.mark_correct_5")} handleSelect5("wrong")} > + {t("practice.mark_wrong_5")} From 6b781ffe0c821f2a5c9c01aec4ad1fc40744d8c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E6=9D=B0?= <17793426456@163.com> Date: Wed, 9 Sep 2026 15:47:27 +0800 Subject: [PATCH 08/18] =?UTF-8?q?fix(word-practice):=20=E4=BA=BA=E5=B7=A5?= =?UTF-8?q?=E5=B8=A6=E8=AF=BB=E6=97=B6=E9=AB=98=E4=BA=AE=E5=BD=93=E5=89=8D?= =?UTF-8?q?=E6=9C=97=E8=AF=BB=E5=8D=95=E8=AF=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 取消人工带读模式下对当前单词选中框的隐藏,点击朗读单词后使用主题色边框和浅色背景标记当前单词。 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- web/src/pages/WordPractice.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/web/src/pages/WordPractice.tsx b/web/src/pages/WordPractice.tsx index ab538fd..f7de99d 100644 --- a/web/src/pages/WordPractice.tsx +++ b/web/src/pages/WordPractice.tsx @@ -340,9 +340,9 @@ export default function WordPractice() {
w.id === cardWord.id) === selectedIndex - ? "border-[#4ECDC4] bg-[#4ECDC4]/10" - : "border-[#E2E8F0]" + words.findIndex((w) => w.id === cardWord.id) === selectedIndex + ? "border-primary bg-primary-soft" + : "border-border" }`} style={{ minHeight: "min(62vh, calc(100dvh - 13.5rem))" }} > @@ -426,8 +426,8 @@ export default function WordPractice() {
From fc17e7663a5cf8d6f76e439ca83cea83bcc6de1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E6=9D=B0?= <17793426456@163.com> Date: Wed, 9 Sep 2026 15:57:43 +0800 Subject: [PATCH 09/18] =?UTF-8?q?fix(word-practice):=20=E4=BA=BA=E5=B7=A5?= =?UTF-8?q?=E5=B8=A6=E8=AF=BB=E6=94=AF=E6=8C=81=E6=95=B4=E5=BC=A0=E5=8D=95?= =?UTF-8?q?=E8=AF=8D=E5=8D=A1=E9=80=89=E4=B8=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 人工带读时点击整张单词卡即可标记当前朗读单词, 并阻止单词卡内的翻页和笔记操作误触发朗读标记。 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- web/src/pages/WordPractice.tsx | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/web/src/pages/WordPractice.tsx b/web/src/pages/WordPractice.tsx index f7de99d..12991e4 100644 --- a/web/src/pages/WordPractice.tsx +++ b/web/src/pages/WordPractice.tsx @@ -343,7 +343,8 @@ export default function WordPractice() { words.findIndex((w) => w.id === cardWord.id) === selectedIndex ? "border-primary bg-primary-soft" : "border-border" - }`} + } ${manualReadMode ? "cursor-pointer" : ""}`} + onClick={manualReadMode ? () => handleWordTap(cardWord) : undefined} style={{ minHeight: "min(62vh, calc(100dvh - 13.5rem))" }} >

@@ -355,7 +356,10 @@ export default function WordPractice() { variant="ghost" size="iconRound" disabled={cardIndex <= 0} - onClick={() => setCardIndex((i) => Math.max(0, i - 1))} + onClick={(event) => { + event.stopPropagation(); + setCardIndex((i) => Math.max(0, i - 1)); + }} className="absolute left-2 top-1/2 z-10 size-11 -translate-y-1/2 bg-muted/90 shadow-sm disabled:opacity-35" > @@ -363,7 +367,10 @@ export default function WordPractice() {