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-26 08:52:50 +08:00
1 parent 0faf78ad8f
commit 1a173f75d7
19 files changed
+1243 -24

No files matched your search

+56 -1
View File
@@ -256,6 +256,17 @@ const groupCount = computed(() => {
for (const student of students.value) count[groupOf(student)]++
return count
})
/**
* 全做完了的人里有几个在挑战:做对之后又交了、还没做到(见契约 classBoardCellSchema.challenge)。
* 做完了又没在挑战的,就是空着的
*/
const challengingCount = computed(
() =>
students.value.filter(
(student) =>
groupOf(student) === "done" && student.cells.some((cell) => cell.challenge === "doing"),
).length,
)
/** 每组第一个人,在他上面插一行组名 */
const groupStarts = computed(() => {
const starts = new Set<number>()
@@ -298,7 +309,11 @@ const problemStats = computed(() =>
const done = present.value.filter((student) => student.cells[i]?.status === "accepted").length
const wrong = present.value.filter((student) => student.cells[i]?.status === "tried").length
const total = present.value.length
return { problem, done, wrong, none: total - done - wrong, total }
// 挑战成功几人:只有配了挑战的题才写
const challenged = problem.hasChallenges
? present.value.filter((student) => student.cells[i]?.challenge === "done").length
: null
return { problem, done, wrong, none: total - done - wrong, total, challenged }
}),
)
@@ -532,6 +547,9 @@ function lastText(student: ClassBoardStudent) {
<span class="card-foot">
<span :class="stat.wrong ? 'wrong' : 'muted'">错着 {{ stat.wrong }}</span>
<span class="muted">没做 {{ stat.none }}</span>
<span v-if="stat.challenged !== null" class="challenged">
挑战成功 {{ stat.challenged }}
</span>
</span>
</button>
</div>
@@ -651,6 +669,9 @@ function lastText(student: ClassBoardStudent) {
<span class="group-label">
{{ GROUP_LABEL[groupOf(student)] }}
<span class="muted">{{ groupCount[groupOf(student)] }}</span>
<span v-if="groupOf(student) === 'done' && challengingCount" class="challenged">
在挑战 {{ challengingCount }}
</span>
</span>
</td>
</tr>
@@ -709,6 +730,7 @@ function lastText(student: ClassBoardStudent) {
v-if="cell.status === 'tried' && cell.latest"
type="button"
class="cell"
:data-challenge="cell.challenge ?? undefined"
:style="cellStyle(cell)"
:title="cellTitle(cell, student)"
@click="openExplain(i, student.userId)"
@@ -727,6 +749,7 @@ function lastText(student: ClassBoardStudent) {
<button
type="button"
class="cell"
:data-challenge="cell.challenge ?? undefined"
:class="{ picked: trailKey === `${student.userId}:${i}` }"
:style="cellStyle(cell)"
:title="cellTitle(cell, student)"
@@ -747,6 +770,7 @@ function lastText(student: ClassBoardStudent) {
<a
v-else-if="cell.attempts"
class="cell"
:data-challenge="cell.challenge ?? undefined"
:style="cellStyle(cell)"
:href="submissionsHref(student, board.problems[i], !cell.codeAttempts)"
target="_blank"
@@ -757,6 +781,7 @@ function lastText(student: ClassBoardStudent) {
<span
v-else
class="cell"
:data-challenge="cell.challenge ?? undefined"
:style="cellStyle(cell)"
:title="cellTitle(cell, student)"
>
@@ -1220,6 +1245,36 @@ a.cell {
cursor: pointer;
}
/* 挑战:做到了贴一枚紫色筹码,在挑战加一圈紫边(设计稿 StkBoard) */
.cell[data-challenge="done"] {
position: relative;
}
.cell[data-challenge="done"]::after {
content: "";
position: absolute;
right: -6px;
top: -6px;
width: 14px;
height: 14px;
border-radius: 50%;
box-sizing: border-box;
background: #5b34c4;
border: 3px solid #b49cf5;
box-shadow: 0 0 0 2px v-bind("theme.cardColor");
}
.cell[data-challenge="doing"] {
box-shadow:
0 0 0 2px v-bind("theme.cardColor"),
0 0 0 4px #8f72e3;
}
.challenged {
color: #6a45d6;
font-weight: 600;
}
.cell.picked {
box-shadow: 0 0 0 2px v-bind("theme.textColor1");
}