Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
1f5390e
feat(web): 新增学习通式课程课表页面
17793426456 Sep 3, 2026
4cd0b7d
refactor(web): 备课页排课替换为学习通式课表
17793426456 Sep 3, 2026
87cb052
refactor(web): 备课页课表改为节次网格展示
17793426456 Sep 3, 2026
19ce9ea
fix(web): 修复备课课表节次网格错位
17793426456 Sep 3, 2026
bd973cd
fix(web): 压缩手机端课表布局
17793426456 Sep 3, 2026
99df6e3
fix(web): 放宽备课课表桌面布局
17793426456 Sep 3, 2026
176fbd4
fix(web): 优化复习单词页手机底栏适配
17793426456 Sep 3, 2026
15b7594
Merge remote-tracking branch 'upstream/main'
17793426456 Sep 6, 2026
5264541
feat(web): 优化排课面板交互与空状态提示
17793426456 Sep 6, 2026
a2e12df
feat(web): 抗遗忘任务右侧添加开始复习按钮
17793426456 Sep 6, 2026
8b589b3
style(web): 抗遗忘时间方块垂直居中
17793426456 Sep 6, 2026
ae93cd8
style(web): 抗遗忘时间方块居中且训前检测修复移动端按钮和桌面端间距
17793426456 Sep 6, 2026
e990282
style(web): 抗遗忘时间方块内容严格水平居中
17793426456 Sep 6, 2026
7d3e735
fix(web): 将抗遗忘时间方块中心对齐时间轴
17793426456 Sep 6, 2026
eed7649
fix(web): 修正拼写模式点击逻辑
17793426456 Sep 6, 2026
224cd31
style(web): 为抗遗忘复习项添加淡色分隔线
17793426456 Sep 6, 2026
db213fc
style(web): 增强抗遗忘复习项分隔线
17793426456 Sep 6, 2026
9ec1014
style(web): 延伸抗遗忘复习项分隔线至内容边界
17793426456 Sep 6, 2026
3ec1589
fix(web): 优化课表重叠课程布局并移除灰色卡片
17793426456 Sep 6, 2026
ec7048f
style(web): 优化抗遗忘复习按钮布局与交互反馈
17793426456 Sep 6, 2026
31a5fc9
style(web): 优化抗遗忘时间轴布局并跟随主题色
17793426456 Sep 6, 2026
37d462c
fix(anti-forgetting): 精简抗遗忘设置页标题与说明
17793426456 Sep 7, 2026
0d2aa5b
fix(anti-forgetting): 删除抗遗忘界面的曲线标签
17793426456 Sep 7, 2026
000e693
feat(billing): 学员管理改为按课时计费,完成训后检测消耗1课时
17793426456 Sep 7, 2026
a6bd238
fix(review): 修复抗遗忘列表词数与点开词数不一致
17793426456 Sep 7, 2026
ba4b535
fix(coaching): 排课进行中不显示默认结束时间,下课后显示真实时间
17793426456 Sep 7, 2026
38f31ca
fix(pre-training): 选词计数只统计❌未记住的单词
17793426456 Sep 7, 2026
5533f52
refactor(billing): 恢复按分钟计费,下课扣学员时长
17793426456 Sep 7, 2026
8abe7a7
Merge upstream/main into main
17793426456 Sep 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 20 additions & 6 deletions internal/handlers/study_session_report.go
Original file line number Diff line number Diff line change
Expand Up @@ -280,10 +280,12 @@ func loadSessionWordLabels(db *gorm.DB, session *models.StudySession, filter ses
}

func studySessionReportPrompts(report studySessionReportDTO) (systemPrompt, userPrompt string) {
systemPrompt = "你是英语陪练老师的课堂助教,根据本节课数据写一段简短「教练点评」。" +
"硬性要求:中文;严格 2-3 句;总长不超过 100 字;语气克制,像老师随手记的教学笔记;" +
"禁止使用 emoji;禁止 Markdown;禁止复述用时、正确率、筛词数、识记数、剩余待学等界面已有量化数据;" +
"只写表现判断与下一步建议;不要编造未提供的信息。"
systemPrompt = "你是英语陪练老师,写一段简短的课堂评价。" +
"硬性要求:中文;只写 2-3 句自然连贯的话;总长不超过 80 字;" +
"只聚焦本节正确率、课堂状态和表现是否有进步;表现好就直接表扬,表现下降或正确率偏低就简短鼓励并说下次加油;" +
"禁止使用序号、列表、括号、Markdown、emoji;禁止写下节课安排、学习计划、下一步建议或扩展分析;" +
"不要编造没有提供的课堂表现或进步信息;若没有历史对比,不要虚构具体进步幅度;" +
"如需出现单词,只能从“未记住单词”中选择,禁止出现其他学过的单词。"
forgotLine := "无"
if len(report.ForgotWords) > 0 {
forgotLine = strings.Join(report.ForgotWords, "、")
Expand All @@ -295,7 +297,7 @@ func studySessionReportPrompts(report studySessionReportDTO) (systemPrompt, user
screenTotal := report.ScreenedKnownCount + report.ScreenedUnknownCount
name := fallbackDash(report.StudentName)
userPrompt = fmt.Sprintf(
"学员:%s\n词库:%s\n用时:约 %d 分钟\n筛词:合计 %d(认识 %d / 新学 %d)\n本课识记:%d\n训后记住:%d / 未记住:%d\n正确率:%.0f%%\n词书剩余待学:%d\n本节学习词:%s\n需巩固词:%s\n请只输出教练点评正文。",
"学员:%s\n词库:%s\n用时:约 %d 分钟\n筛词:合计 %d(认识 %d / 新学 %d)\n本课识记:%d\n训后记住:%d / 未记住:%d\n正确率:%.0f%%\n词书剩余待学:%d\n本节学习词:%s\n需巩固词:%s\n请只输出课堂评价正文,不要加标题、序号或括号。",
name,
fallbackDash(report.WordBookName),
report.DurationMinutes,
Expand Down Expand Up @@ -368,7 +370,19 @@ func isCurrentSessionReportFormat(text string) bool {
strings.Contains(trimmed, "全程用时") ||
strings.Contains(trimmed, "系统记录") ||
strings.Contains(trimmed, "未触发熟词") ||
(strings.Contains(trimmed, "正确率") && strings.Contains(trimmed, "剩余")) {
strings.Contains(trimmed, "下节课") ||
strings.Contains(trimmed, "下一节") ||
strings.Contains(trimmed, "学习计划") ||
strings.Contains(trimmed, "建议") ||
strings.Contains(trimmed, "(") ||
strings.Contains(trimmed, ")") ||
strings.Contains(trimmed, "(") ||
strings.Contains(trimmed, ")") ||
(strings.Contains(trimmed, "正确率") && strings.Contains(trimmed, "剩余")) ||
strings.Contains(trimmed, "1、") || strings.Contains(trimmed, "2、") ||
strings.Contains(trimmed, "3、") || strings.Contains(trimmed, "4、") ||
strings.Contains(trimmed, "1.") || strings.Contains(trimmed, "2.") ||
strings.Contains(trimmed, "3.") || strings.Contains(trimmed, "4.") {
return false
}
return true
Expand Down
157 changes: 157 additions & 0 deletions web/src/api/timetable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/**
* 学习通式课程课表 — 数据访问层
*
* 当前实现:浏览器 localStorage 持久化(方案 A)。
* 预留后端:本文件导出 `TimetableRepository` 接口与 `timetableRepo` 单例,
* 未来切换到方案 B 时,只需新增一个基于 `get/post/put/del` 的 HTTP 实现并替换单例即可,
* 上层 store / 组件无需改动。
*
* 预期的 RESTful 契约(方案 B):
* GET /api/timetable/config -> TimetableConfig
* PUT /api/timetable/config -> TimetableConfig
* GET /api/timetable/course -> Course[]
* POST /api/timetable/course -> Course
* PUT /api/timetable/course/:id -> Course
* DELETE /api/timetable/course/:id -> void
*/

/** 单双周:0=每周,1=单周,2=双周 */
export type WeekType = 0 | 1 | 2;

/** 节次时间配置项 */
export interface Section {
no: number;
start: string; // "08:00"
end: string; // "08:45"
}

/** 课表全局配置 */
export interface TimetableConfig {
/** 当前是第几周(1-based) */
currentWeek: number;
/** 学期总周数 */
totalWeek: number;
/** 节次时间表 */
sections: Section[];
}

/** 课程对象 */
export interface Course {
id: string;
name: string;
teacher: string;
room: string;
/** 星期 1~7(周一~周日) */
weekDay: number;
/** 开始节次(1-based,包含) */
startSection: number;
/** 结束节次(1-based,包含) */
endSection: number;
/** 起始周(1-based,包含) */
startWeek: number;
/** 结束周(1-based,包含) */
endWeek: number;
/** 单双周 */
weekType: WeekType;
/** 色块背景色(hex) */
color: string;
}

/** 仓储接口:localStorage 与未来 HTTP 实现共同遵守 */
export interface TimetableRepository {
listCourses(): Promise<Course[]>;
saveCourse(course: Course): Promise<Course>;
deleteCourse(id: string): Promise<void>;
getConfig(): Promise<TimetableConfig>;
saveConfig(config: TimetableConfig): Promise<TimetableConfig>;
}

const COURSES_KEY = "cloudsteps_timetable_courses";
const CONFIG_KEY = "cloudsteps_timetable_config";

/** 默认节次时间(学习通常见 12 节) */
export const DEFAULT_SECTIONS: Section[] = [
{ no: 1, start: "08:00", end: "08:45" },
{ no: 2, start: "08:55", end: "09:40" },
{ no: 3, start: "10:00", end: "10:45" },
{ no: 4, start: "10:55", end: "11:40" },
{ no: 5, start: "14:00", end: "14:45" },
{ no: 6, start: "14:55", end: "15:40" },
{ no: 7, start: "16:00", end: "16:45" },
{ no: 8, start: "16:55", end: "17:40" },
{ no: 9, start: "19:00", end: "19:45" },
{ no: 10, start: "19:55", end: "20:40" },
{ no: 11, start: "20:50", end: "21:35" },
{ no: 12, start: "21:45", end: "22:30" },
];

/** 课程色块预设(学习通风格,避免 AI 紫色审美) */
export const COURSE_COLORS: string[] = [
"#4ECDC4", // mint
"#55A3FF", // sky
"#FF8C5A", // coral
"#F6B26B", // amber
"#8B7FD8", // violet
"#E85555", // red
"#3DB8B0", // deep mint
"#F4A6C7", // pink
"#67B279", // green
"#7A8A99", // slate
];

function defaultConfig(): TimetableConfig {
return { currentWeek: 1, totalWeek: 20, sections: DEFAULT_SECTIONS };
}

function safeParse<T>(raw: string | null, fallback: T): T {
if (!raw) return fallback;
try {
return JSON.parse(raw) as T;
} catch {
return fallback;
}
}

/** 生成唯一 id(无依赖) */
export function genCourseId(): string {
return `c_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
}

/**
* localStorage 实现。所有方法返回 Promise,签名与未来 HTTP 实现一致,
* 便于无缝替换。
*/
class LocalTimetableRepository implements TimetableRepository {
async listCourses(): Promise<Course[]> {
return safeParse<Course[]>(localStorage.getItem(COURSES_KEY), []);
}

async saveCourse(course: Course): Promise<Course> {
const list = await this.listCourses();
const idx = list.findIndex((c) => c.id === course.id);
if (idx >= 0) list[idx] = course;
else list.push(course);
localStorage.setItem(COURSES_KEY, JSON.stringify(list));
return course;
}

async deleteCourse(id: string): Promise<void> {
const list = (await this.listCourses()).filter((c) => c.id !== id);
localStorage.setItem(COURSES_KEY, JSON.stringify(list));
}

async getConfig(): Promise<TimetableConfig> {
return safeParse<TimetableConfig>(localStorage.getItem(CONFIG_KEY), defaultConfig());
}

async saveConfig(config: TimetableConfig): Promise<TimetableConfig> {
localStorage.setItem(CONFIG_KEY, JSON.stringify(config));
return config;
}
}

/**
* 仓储单例。当前指向 localStorage 实现;
* 切换后端时把这里换成 `new HttpTimetableRepository()` 即可。
*/
export const timetableRepo: TimetableRepository = new LocalTimetableRepository();
Loading
Loading