feat(智能分析): 页面改成每节课一行,一眼看出哪道一次就对、哪道交了很多次、哪道还没做对
Deploy / deploy (push) Canceled after 0s

一年热力图、半年月度图、难度分布、时间活跃度这些对多数学生大半空白的图都去掉了;
右边是差一点的(接着做)、错在哪、知识点;老师看学生能 ‹ › 翻同班下一个人。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-12 23:20:34 +08:00
1 parent 890e731a1c
commit e4abbc1645
27 files changed
+1343 -1710

No files matched your search

@@ -0,0 +1,221 @@
<script setup lang="ts">
import type { LearningDay } from "@oj2/contract"
import { useThemeVars } from "naive-ui"
import { classLabel } from "oj/submission/utils"
import ProblemChip from "./ProblemChip.vue"
import { dayLabel, isDone } from "../utils"
/** 一天一行,新的在前:先是班上这节课的题,再是这天自己练的 */
const props = defineProps<{
days: LearningDay[]
className: string | null
username?: string
/** 手机:日期单独一行,题目块在下面折行 */
compact?: boolean
}>()
const theme = useThemeVars()
/** 一天自己练的超过这么多就先收起来;做得多的学生一天能自己刷二十几道 */
const OWN_CAP = 10
/** 「全部」能有上百天,先列这么多天 */
const DAY_CAP = 12
const openOwn = ref(new Set<string>())
const showAll = ref(false)
watch(
() => props.days,
() => {
openOwn.value = new Set()
showAll.value = false
},
)
const shown = computed(() => (showAll.value ? props.days : props.days.slice(0, DAY_CAP)))
const you = computed(() => (props.username ? "他" : "你"))
const subtitle = computed(() =>
props.className
? `${classLabel(props.className)}课上做的题,加上${you.value}自己练的`
: `${you.value}做过的题`,
)
function ownOf(day: LearningDay) {
return openOwn.value.has(day.day) ? day.own : day.own.slice(0, OWN_CAP)
}
function expand(day: string) {
openOwn.value = new Set([...openOwn.value, day])
}
</script>
<template>
<section class="card" :class="{ compact }">
<h3>
每节课<small>{{ subtitle }}</small>
</h3>
<div v-for="day in shown" :key="day.day" class="day">
<div class="when">
<b>{{ dayLabel(day.day).date }}</b>
<span>{{ dayLabel(day.day).weekday }}</span>
<span v-if="compact && day.lesson.length" class="done">
做对 {{ day.lesson.filter(isDone).length }}/{{ day.lesson.length }}
</span>
</div>
<div class="groups">
<div v-if="day.lesson.length" class="chips">
<ProblemChip
v-for="item in day.lesson"
:key="item.displayId"
:item="item"
in-lesson
:username="username"
/>
</div>
<div v-if="day.own.length" class="chips">
<span class="own">自己练</span>
<ProblemChip
v-for="item in ownOf(day)"
:key="item.displayId"
:item="item"
:in-lesson="false"
:username="username"
/>
<button
v-if="day.own.length > OWN_CAP && !openOwn.has(day.day)"
class="more"
@click="expand(day.day)"
>
还有 {{ day.own.length - OWN_CAP }} 道 ▾
</button>
</div>
</div>
<div v-if="!compact" class="done">
<template v-if="day.lesson.length">
做对 {{ day.lesson.filter(isDone).length }}/{{ day.lesson.length }}
</template>
</div>
</div>
<button v-if="!showAll && days.length > DAY_CAP" class="earlier" @click="showAll = true">
更早的 {{ days.length - DAY_CAP }} 天 ▾
</button>
</section>
</template>
<style scoped>
.card {
padding: 14px 20px 6px;
border: 1px solid v-bind("theme.borderColor");
border-radius: var(--oj-radius);
background: v-bind("theme.cardColor");
}
h3 {
margin: 0 0 8px;
font-size: var(--oj-fs-h2);
display: flex;
align-items: baseline;
gap: 10px;
}
h3 small {
font-size: var(--oj-fs-meta);
font-weight: 400;
color: v-bind("theme.textColor3");
}
.day {
display: flex;
gap: 12px;
padding: 10px 0;
border-top: 1px solid v-bind("theme.dividerColor");
}
.when {
width: 92px;
flex-shrink: 0;
line-height: 30px;
white-space: nowrap;
}
.when b {
font-size: var(--oj-fs-body);
margin-right: 4px;
}
.when span {
font-size: var(--oj-fs-meta);
color: v-bind("theme.textColor3");
}
.groups {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 8px;
}
.chips {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px;
}
.own {
font-size: var(--oj-fs-meta);
color: v-bind("theme.textColor3");
margin-right: 2px;
}
.done {
width: 72px;
flex-shrink: 0;
text-align: right;
line-height: 30px;
font-size: var(--oj-fs-meta);
color: v-bind("theme.textColor3");
font-variant-numeric: tabular-nums;
}
.more,
.earlier {
height: 30px;
padding: 0 12px;
border: 1px dashed v-bind("theme.borderColor");
border-radius: 6px;
background: transparent;
color: #18a058;
font: inherit;
font-size: var(--oj-fs-meta);
cursor: pointer;
}
.earlier {
width: 100%;
margin: 4px 0 10px;
}
.compact {
padding: 12px 14px 4px;
}
.compact .day {
flex-direction: column;
gap: 6px;
}
.compact .when {
width: auto;
display: flex;
align-items: baseline;
gap: 6px;
line-height: 22px;
}
.compact .when .done {
margin-left: auto;
width: auto;
line-height: 22px;
}
</style>