feat(统计): 数据统计从弹框改成独立页面,左边题目、右边学生,一道题看全班每人的每一次
- 课堂看板管这节课,统计管回头看:时间段默认「两节课」,还有这节课、今天、7 天、30 天、这学期、全部、自己选日子;不再自动刷新 - 选一道题看每人在这道题上的方块串,鼠标停上去看那次的代码;选「全部」每人一行、展开按题看 - 没选班级(从题目页进来)先按班级汇总;流程图统计有等级分布、四项雷达、各项平均分、词云和每人最好的一次 - 提交列表、题目页的「数据统计」改成新标签打开这个页面 - 「请假隐藏」挪到课堂看板:点名字旁的 × 藏两个小时,做完人数的分母一起减掉 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f09da88ff7
commit
ed55ee5af6
16 files changed
+2256
-76
No files matched your search
@@ -0,0 +1,163 @@
|
||||
<script lang="ts">
|
||||
import type { Submission } from "utils/types"
|
||||
|
||||
/**
|
||||
* 代码按提交 id 缓存,整页共用:交了就不会再变,老师在一排方块之间来回对比时
|
||||
* 不用每次都等网络。失败的不进缓存,下次停上去再试。
|
||||
*/
|
||||
const codeCache = new Map<string, Submission>()
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getSubmission } from "oj/api"
|
||||
import { JUDGE_STATUS, LANGUAGE_FORMAT_VALUE, SubmissionStatus } from "utils/constants"
|
||||
import { parseTime } from "utils/functions"
|
||||
import type { SUBMISSION_RESULT } from "utils/types"
|
||||
import { useTone, type Tone } from "oj/submission/composables/tone"
|
||||
|
||||
/**
|
||||
* 一个人在一道题上的每一次提交,一格一次、从早到晚(设计稿「统计重设计」)。
|
||||
* 鼠标停在一格上看那一次的代码,点一下新页面打开那条提交。
|
||||
*/
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
items: Array<{ id: string; result: SUBMISSION_RESULT; createTime: string }>
|
||||
size?: number
|
||||
}>(),
|
||||
{ size: 14 },
|
||||
)
|
||||
|
||||
const tone = useTone()
|
||||
|
||||
function toneOf(result: number): Tone {
|
||||
if (result === SubmissionStatus.accepted) return "success"
|
||||
if (
|
||||
result === SubmissionStatus.compile_error ||
|
||||
result === SubmissionStatus.runtime_error ||
|
||||
result === SubmissionStatus.ast_check_failed ||
|
||||
result === SubmissionStatus.partial_accepted
|
||||
) {
|
||||
return "warning"
|
||||
}
|
||||
if (result === SubmissionStatus.pending || result === SubmissionStatus.judging) return "info"
|
||||
return "error"
|
||||
}
|
||||
|
||||
const code = reactive<Record<string, Submission | "loading" | "failed">>({})
|
||||
|
||||
async function load(id: string) {
|
||||
const hit = codeCache.get(id)
|
||||
if (hit) {
|
||||
code[id] = hit
|
||||
return
|
||||
}
|
||||
if (code[id] === "loading") return
|
||||
code[id] = "loading"
|
||||
try {
|
||||
const res = await getSubmission(id)
|
||||
codeCache.set(id, res)
|
||||
code[id] = res
|
||||
} catch {
|
||||
code[id] = "failed"
|
||||
}
|
||||
}
|
||||
|
||||
function open(id: string) {
|
||||
window.open("/submission/" + id, "_blank", "noopener")
|
||||
}
|
||||
|
||||
const cells = computed(() =>
|
||||
props.items.map((item, index) => ({
|
||||
...item,
|
||||
index: index + 1,
|
||||
color: tone(toneOf(item.result)).solid,
|
||||
})),
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span class="squares">
|
||||
<n-popover
|
||||
v-for="cell in cells"
|
||||
:key="cell.id"
|
||||
:delay="200"
|
||||
:style="{ maxWidth: '560px' }"
|
||||
@update:show="(show: boolean) => show && load(cell.id)"
|
||||
>
|
||||
<template #trigger>
|
||||
<button
|
||||
class="square"
|
||||
:style="{ width: `${size}px`, height: `${size}px`, background: cell.color }"
|
||||
:aria-label="`第 ${cell.index} 次:${JUDGE_STATUS[cell.result]?.name ?? ''}`"
|
||||
@click.stop="open(cell.id)"
|
||||
></button>
|
||||
</template>
|
||||
<div class="pop">
|
||||
<div class="pop-head">
|
||||
<b>{{ JUDGE_STATUS[cell.result]?.name }}</b>
|
||||
<span class="muted"
|
||||
>{{ parseTime(cell.createTime, "M月D日 HH:mm") }} · 第 {{ cell.index }} 次</span
|
||||
>
|
||||
</div>
|
||||
<n-code
|
||||
v-if="typeof code[cell.id] === 'object'"
|
||||
class="pop-code"
|
||||
:code="(code[cell.id] as Submission).code"
|
||||
:language="LANGUAGE_FORMAT_VALUE[(code[cell.id] as Submission).language]"
|
||||
show-line-numbers
|
||||
/>
|
||||
<span v-else-if="code[cell.id] === 'failed'" class="muted">代码没拉下来,点方块打开看</span>
|
||||
<span v-else class="muted">代码加载中…</span>
|
||||
<span class="muted small">点方块在新页面打开这一条</span>
|
||||
</div>
|
||||
</n-popover>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.squares {
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.square {
|
||||
flex: none;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.square:hover {
|
||||
outline: 2px solid currentColor;
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
.pop {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.pop-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.pop-code {
|
||||
max-height: 360px;
|
||||
overflow: auto;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.muted {
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
.small {
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user