feat(激励): 知识点地图——每个知识点做对几道、在哪一档、再做几道升级,首页一句话提示本周进步
Deploy / deploy (push) Canceled after 0s

现有 38 个成就全是累计量(通过几题、提交几次、连续几天),起步的人人都有、往上的够不着,
而且没有一个在说「你学会了什么」。连续打卡在这里结构上做不到:2025 秋学生最长连续交题
天数的中位数是 1 天,连续 7 天的只有 3 人。

- GET /me/knowledge:题数 ≥10 的知识点标签(10 个),每个给做对几道、档位、周一时的档位、
  下一档门槛;四档 入门 1 / 会了 3 / 熟练 题数 40%(≤10)/ 精通 75%(≤20)。
  回测 2025 秋:中位数学生一学期升 5 档;「入门」一档是给只做对一两道的 71 人加的
- 同班 ≥30% 点亮过的知识点才会出现在「还没碰过」里,免得 Python 班看到「C 语言」
- 个人主页加「我的知识点」,只在看自己时显示;首页一句话:本周升级 > 离下一档最近的 >
  去点亮一个班里在学的,点了去按知识点筛的题目列表

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-09-27 23:17:43 +08:00
1 parent a63bf6ea07
commit a7f6ebaf49
8 files changed
+446

No files matched your search

+72
View File
@@ -0,0 +1,72 @@
import type { KnowledgeLevel, KnowledgeMap } from "utils/types"
/**
* 知识点地图的档位名(后端 routes/account.ts 的 levelThresholds 定门槛)。
* 用「会了」「熟练」这种学生听得懂的词,不用「Lv.3」—— 这张图要说的是「我会了什么」,
* 不是又一套积分。
*/
export const LEVEL_NAMES = ["没碰过", "入门", "会了", "熟练", "精通"] as const
export const MAX_LEVEL = LEVEL_NAMES.length - 1
export function levelName(level: number) {
return LEVEL_NAMES[Math.max(0, Math.min(MAX_LEVEL, level))]!
}
/** 离下一档还差几道;已经最高档为 null */
export function remainingToNext(tag: KnowledgeLevel) {
return tag.nextAt === null ? null : Math.max(1, tag.nextAt - tag.solved)
}
export function tagLink(name: string) {
return { path: "/problem", query: { tag: name } }
}
export interface Headline {
text: string
/** 点了去哪:按这个知识点筛的题目列表 */
to?: ReturnType<typeof tagLink>
}
/**
* 首页「你好」下面那一行:一次只说一件事,挑最能让人想再做一道的那件。
*
* 1. 本周升级了 —— 刚发生的进步,最值得说
* 2. 正在学的里面,离下一档最近的那个(班里在学的优先)—— 给一个很近的目标
* 3. 一个都没点亮:挑一个班里同学在学的,让他去点亮
*/
export function pickHeadline(map: KnowledgeMap): Headline | null {
const upgraded = map.tags
.filter((tag) => tag.level > tag.levelAtWeekStart)
.sort((a, b) => b.level - a.level)
if (upgraded.length === 1) {
const tag = upgraded[0]!
return { text: `本周升级:${tag.name} → ${levelName(tag.level)}`, to: tagLink(tag.name) }
}
if (upgraded.length > 1) {
const list = upgraded.map((tag) => `${tag.name}「${levelName(tag.level)}」`).join("、")
return { text: `本周升级了 ${upgraded.length} 个知识点:${list}` }
}
const touched = new Set(map.classTouched)
const learning = map.tags
.filter((tag) => tag.level >= 1 && tag.level < MAX_LEVEL)
.sort(
(a, b) =>
Number(touched.has(b.name)) - Number(touched.has(a.name)) ||
remainingToNext(a)! - remainingToNext(b)!,
)
const next = learning[0]
if (next) {
return {
text: `正在学${next.name},再做 ${remainingToNext(next)} 道就到「${levelName(next.level + 1)}」`,
to: tagLink(next.name),
}
}
const fresh = map.tags.find((tag) => tag.level === 0 && touched.has(tag.name))
if (fresh) {
return { text: `班里同学在学${fresh.name},去做一道点亮它`, to: tagLink(fresh.name) }
}
return null
}