Files
OJ2/apps/web/src/oj/user/components/KnowledgeMap.vue
T
xuyueandClaude Opus 5.5 a7f6ebaf49
Deploy / deploy (push) Canceled after 0s
feat(激励): 知识点地图——每个知识点做对几道、在哪一档、再做几道升级,首页一句话提示本周进步
现有 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>
2026-09-27 23:17:43 +08:00

136 lines
3.6 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { getKnowledgeMap } from "oj/api"
import { levelName, MAX_LEVEL, remainingToNext, tagLink } from "oj/user/knowledge"
import type { KnowledgeLevel, KnowledgeMap } from "utils/types"
/**
* 个人主页上的知识点地图:「我学会了什么」,只和自己比。
*
* 只在看**自己的**主页时挂(index.vue 判断),不开放给别人看 —— 开放了它就成了
* 另一张排行榜,基础弱的学生最不想被看到的就是这个。
*/
const map = ref<KnowledgeMap | null>(null)
onMounted(async () => {
map.value = await getKnowledgeMap().catch(() => null)
})
const learned = computed(() =>
(map.value?.tags ?? [])
.filter((tag) => tag.level >= 1)
.sort((a, b) => b.level - a.level || b.solved - a.solved),
)
/** 班里同学在学、自己还没碰过的。别的(比如 Python 班的「C 语言」)不列 */
const untouched = computed(() => {
const touched = new Set(map.value?.classTouched ?? [])
return (map.value?.tags ?? []).filter((tag) => tag.level === 0 && touched.has(tag.name))
})
function progress(tag: KnowledgeLevel) {
if (tag.nextAt === null) return 100
return Math.min(100, Math.round((tag.solved / tag.nextAt) * 100))
}
function hint(tag: KnowledgeLevel) {
const left = remainingToNext(tag)
if (left === null) return `做对了 ${tag.solved} 道,已经精通了`
return `做对 ${tag.solved} 道 · 再做 ${left} 道到「${levelName(tag.level + 1)}」`
}
</script>
<template>
<n-card v-if="map && (learned.length || untouched.length)" class="knowledge" title="我的知识点">
<template #header-extra>
<n-text depth="3" class="meta">入门 → 会了 → 熟练 → 精通</n-text>
</template>
<n-flex vertical :size="14">
<router-link v-for="tag in learned" :key="tag.name" :to="tagLink(tag.name)" class="row">
<div class="head">
<span class="name">{{ tag.name }}</span>
<span class="dots" :title="levelName(tag.level)">
<span v-for="i in MAX_LEVEL" :key="i" class="dot" :class="{ on: i <= tag.level }" />
</span>
<span class="level">{{ levelName(tag.level) }}</span>
<n-tag
v-if="tag.level > tag.levelAtWeekStart"
size="small"
type="success"
:bordered="false"
>
本周升级
</n-tag>
</div>
<n-progress
type="line"
:percentage="progress(tag)"
:show-indicator="false"
:height="6"
status="success"
/>
<n-text depth="3" class="meta">{{ hint(tag) }}</n-text>
</router-link>
<div v-if="untouched.length">
<n-text depth="3" class="meta">班里同学在学、你还没碰过:</n-text>
<n-flex :size="8" style="margin-top: 6px">
<router-link v-for="tag in untouched" :key="tag.name" :to="tagLink(tag.name)">
<n-tag size="small">{{ tag.name }}</n-tag>
</router-link>
</n-flex>
</div>
</n-flex>
</n-card>
</template>
<style scoped>
.knowledge {
max-width: 610px;
margin: 16px auto 0;
}
.row {
display: flex;
flex-direction: column;
gap: 4px;
color: inherit;
text-decoration: none;
}
.head {
display: flex;
align-items: center;
gap: 8px;
}
.name {
font-weight: 500;
min-width: 5em;
}
.dots {
display: inline-flex;
gap: 3px;
}
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: rgba(128, 128, 128, 0.25);
}
.dot.on {
background-color: #18a058;
}
.level {
font-size: 13px;
color: #18a058;
}
.meta {
font-size: 12px;
}
</style>