Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
530e1a3d5d
commit
195069450f
480 files changed
+15507
-24650
No files matched your search
@@ -1,17 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import type { ContestClassView, ContestScoreCell, ContestScoreRow } from "@oj2/contract"
|
||||
import { useThemeVars } from "naive-ui"
|
||||
import { updateACMHelperChecked } from "admin/api"
|
||||
import { getContestClassView, getSubmission, getSubmissions } from "oj/api"
|
||||
import { useContestStore } from "oj/store/contest"
|
||||
import UserName from "shared/components/UserName.vue"
|
||||
import { useTone } from "shared/composables/tone"
|
||||
import { classLabel } from "utils/classes"
|
||||
import { useLeaveStudents } from "shared/composables/leaveStudents"
|
||||
import { ContestStatus, LANGUAGE_FORMAT_VALUE } from "utils/constants"
|
||||
import { parseTime, secondsToDuration } from "utils/functions"
|
||||
import type { Submission } from "utils/types"
|
||||
import { useVisibleInterval } from "shared/composables/visibleInterval"
|
||||
import type { ContestClassView, ContestScoreCell, ContestScoreRow } from '@oj2/contract'
|
||||
import { useThemeVars } from 'naive-ui'
|
||||
import { updateACMHelperChecked } from 'admin/api'
|
||||
import { getContestClassView, getSubmission, getSubmissions } from 'oj/api'
|
||||
import { useContestStore } from 'oj/store/contest'
|
||||
import UserName from 'shared/components/UserName.vue'
|
||||
import { useTone } from 'shared/composables/tone'
|
||||
import { classLabel } from 'utils/classes'
|
||||
import { useLeaveStudents } from 'shared/composables/leaveStudents'
|
||||
import { ContestStatus, LANGUAGE_FORMAT_VALUE } from 'utils/constants'
|
||||
import { parseTime, secondsToDuration } from 'utils/functions'
|
||||
import type { Submission } from 'utils/types'
|
||||
import { useVisibleInterval } from 'shared/composables/visibleInterval'
|
||||
|
||||
/**
|
||||
* 老师的「全班情况」,考完叫「成绩」(设计稿「比赛重设计」老师两块)。学生看不到这一页。
|
||||
@@ -28,7 +28,7 @@ const contestStore = useContestStore()
|
||||
const theme = useThemeVars()
|
||||
const tone = useTone()
|
||||
const message = useMessage()
|
||||
const { onLeave } = useLeaveStudents("oj_leave_students")
|
||||
const { onLeave } = useLeaveStudents('oj_leave_students')
|
||||
|
||||
const view = ref<ContestClassView | null>(null)
|
||||
const failed = ref(false)
|
||||
@@ -78,17 +78,12 @@ function flagOf(row: ContestScoreRow) {
|
||||
for (const p of problems.value) {
|
||||
const cell = cellOf(row, p.id)
|
||||
if (cell && !cell.isAc && cell.errors >= STUCK_ERRORS) {
|
||||
return { kind: "stuck" as const, text: `卡在第 ${p._id} 题,错 ${cell.errors} 次` }
|
||||
return { kind: 'stuck' as const, text: `卡在第 ${p._id} 题,错 ${cell.errors} 次` }
|
||||
}
|
||||
}
|
||||
const idle = minutesSinceLast(row)
|
||||
if (
|
||||
running.value &&
|
||||
idle !== null &&
|
||||
idle >= IDLE_MINUTES &&
|
||||
row.solved < problems.value.length
|
||||
) {
|
||||
return { kind: "idle" as const, text: `${idle} 分钟没交` }
|
||||
if (running.value && idle !== null && idle >= IDLE_MINUTES && row.solved < problems.value.length) {
|
||||
return { kind: 'idle' as const, text: `${idle} 分钟没交` }
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -97,60 +92,39 @@ const attention = computed(() =>
|
||||
rows.value
|
||||
.map((row) => ({ row, flag: flagOf(row) }))
|
||||
.filter((item) => item.flag !== null)
|
||||
.sort((a, b) =>
|
||||
a.flag!.kind === b.flag!.kind
|
||||
? a.row.solved - b.row.solved
|
||||
: a.flag!.kind === "stuck"
|
||||
? -1
|
||||
: 1,
|
||||
),
|
||||
.sort((a, b) => (a.flag!.kind === b.flag!.kind ? a.row.solved - b.row.solved : a.flag!.kind === 'stuck' ? -1 : 1)),
|
||||
)
|
||||
const others = computed(() => {
|
||||
const flagged = new Set(attention.value.map((item) => item.row.userId))
|
||||
return rows.value
|
||||
.filter((row) => !flagged.has(row.userId))
|
||||
.sort((a, b) => a.solved - b.solved || (minutesSinceLast(b) ?? 0) - (minutesSinceLast(a) ?? 0))
|
||||
return rows.value.filter((row) => !flagged.has(row.userId)).sort((a, b) => a.solved - b.solved || (minutesSinceLast(b) ?? 0) - (minutesSinceLast(a) ?? 0))
|
||||
})
|
||||
const absent = computed(() =>
|
||||
(view.value?.absent ?? []).map((user) => ({ ...user, leave: onLeave(user.username) })),
|
||||
)
|
||||
const absent = computed(() => (view.value?.absent ?? []).map((user) => ({ ...user, leave: onLeave(user.username) })))
|
||||
const leaveCount = computed(() => absent.value.filter((user) => user.leave).length)
|
||||
const rosterSize = computed(() => {
|
||||
const classes = new Set(view.value?.classes ?? [])
|
||||
return (
|
||||
rows.value.filter((row) => row.className && classes.has(row.className)).length +
|
||||
absent.value.length
|
||||
)
|
||||
return rows.value.filter((row) => row.className && classes.has(row.className)).length + absent.value.length
|
||||
})
|
||||
const average = computed(() =>
|
||||
rows.value.length
|
||||
? (rows.value.reduce((sum, row) => sum + row.solved, 0) / rows.value.length).toFixed(1)
|
||||
: "0",
|
||||
)
|
||||
const stuckCount = computed(
|
||||
() => attention.value.filter((item) => item.flag!.kind === "stuck").length,
|
||||
)
|
||||
const idleCount = computed(
|
||||
() => attention.value.filter((item) => item.flag!.kind === "idle").length,
|
||||
)
|
||||
const average = computed(() => (rows.value.length ? (rows.value.reduce((sum, row) => sum + row.solved, 0) / rows.value.length).toFixed(1) : '0'))
|
||||
const stuckCount = computed(() => attention.value.filter((item) => item.flag!.kind === 'stuck').length)
|
||||
const idleCount = computed(() => attention.value.filter((item) => item.flag!.kind === 'idle').length)
|
||||
|
||||
function lastText(row: ContestScoreRow) {
|
||||
const minutes = minutesSinceLast(row)
|
||||
if (minutes === null) return ""
|
||||
return minutes === 0 ? "刚刚" : `${minutes} 分钟前`
|
||||
if (minutes === null) return ''
|
||||
return minutes === 0 ? '刚刚' : `${minutes} 分钟前`
|
||||
}
|
||||
|
||||
const classNames = computed(() => (view.value?.classes ?? []).map(classLabel).join("、"))
|
||||
const classNames = computed(() => (view.value?.classes ?? []).map(classLabel).join('、'))
|
||||
|
||||
// ---------------------------------------------------------------- 考完:成绩
|
||||
|
||||
/** 按姓名(用户名,中文按拼音)/ 名次 / 做对少的在前 */
|
||||
type Sort = "username" | "rank" | "weak"
|
||||
const sort = ref<Sort>("username")
|
||||
type Sort = 'username' | 'rank' | 'weak'
|
||||
const sort = ref<Sort>('username')
|
||||
const gradeRows = computed(() => {
|
||||
const list = [...rows.value]
|
||||
if (sort.value === "username") list.sort((a, b) => a.username.localeCompare(b.username, "zh-CN"))
|
||||
else if (sort.value === "weak") list.sort((a, b) => a.solved - b.solved || b.rank! - a.rank!)
|
||||
if (sort.value === 'username') list.sort((a, b) => a.username.localeCompare(b.username, 'zh-CN'))
|
||||
else if (sort.value === 'weak') list.sort((a, b) => a.solved - b.solved || b.rank! - a.rank!)
|
||||
return list
|
||||
})
|
||||
|
||||
@@ -159,51 +133,36 @@ const distribution = computed(() => {
|
||||
for (const row of rows.value) count.set(row.solved, (count.get(row.solved) ?? 0) + 1)
|
||||
return [...count.entries()].sort(([a], [b]) => b - a)
|
||||
})
|
||||
const acTotal = computed(() =>
|
||||
rows.value.reduce((sum, row) => sum + Object.values(row.cells).filter((c) => c.isAc).length, 0),
|
||||
)
|
||||
const checkedTotal = computed(() =>
|
||||
rows.value.reduce(
|
||||
(sum, row) => sum + Object.values(row.cells).filter((c) => c.isAc && c.checked).length,
|
||||
0,
|
||||
),
|
||||
)
|
||||
const acTotal = computed(() => rows.value.reduce((sum, row) => sum + Object.values(row.cells).filter((c) => c.isAc).length, 0))
|
||||
const checkedTotal = computed(() => rows.value.reduce((sum, row) => sum + Object.values(row.cells).filter((c) => c.isAc && c.checked).length, 0))
|
||||
|
||||
function barColor(solved: number) {
|
||||
const n = problems.value.length || 1
|
||||
const ratio = solved / n
|
||||
if (ratio >= 1) return theme.value.successColor
|
||||
if (ratio >= 0.7) return tone("success").solid + "b3"
|
||||
if (ratio >= 0.5) return tone("success").solid + "66"
|
||||
if (ratio >= 0.7) return tone('success').solid + 'b3'
|
||||
if (ratio >= 0.5) return tone('success').solid + '66'
|
||||
if (ratio >= 0.3) return theme.value.warningColor
|
||||
return theme.value.errorColor + "aa"
|
||||
return theme.value.errorColor + 'aa'
|
||||
}
|
||||
|
||||
const selected = ref<{ userId: number; problemId: number } | null>(null)
|
||||
const code = ref<Submission | null>(null)
|
||||
const codeState = ref<"idle" | "loading" | "none" | "failed">("idle")
|
||||
const codeState = ref<'idle' | 'loading' | 'none' | 'failed'>('idle')
|
||||
|
||||
const selectedRow = computed(() =>
|
||||
selected.value ? rows.value.find((row) => row.userId === selected.value!.userId) : undefined,
|
||||
)
|
||||
const selectedProblem = computed(() =>
|
||||
selected.value ? problems.value.find((p) => p.id === selected.value!.problemId) : undefined,
|
||||
)
|
||||
const selectedCell = computed(() =>
|
||||
selectedRow.value && selectedProblem.value
|
||||
? cellOf(selectedRow.value, selectedProblem.value.id)
|
||||
: undefined,
|
||||
)
|
||||
const selectedRow = computed(() => (selected.value ? rows.value.find((row) => row.userId === selected.value!.userId) : undefined))
|
||||
const selectedProblem = computed(() => (selected.value ? problems.value.find((p) => p.id === selected.value!.problemId) : undefined))
|
||||
const selectedCell = computed(() => (selectedRow.value && selectedProblem.value ? cellOf(selectedRow.value, selectedProblem.value.id) : undefined))
|
||||
|
||||
async function pick(row: ContestScoreRow, problemId: number) {
|
||||
selected.value = { userId: row.userId, problemId }
|
||||
code.value = null
|
||||
const problem = problems.value.find((p) => p.id === problemId)
|
||||
if (!problem || !cellOf(row, problemId)) {
|
||||
codeState.value = "none"
|
||||
codeState.value = 'none'
|
||||
return
|
||||
}
|
||||
codeState.value = "loading"
|
||||
codeState.value = 'loading'
|
||||
try {
|
||||
const list = await getSubmissions({
|
||||
contestId: props.contestID,
|
||||
@@ -215,15 +174,15 @@ async function pick(row: ContestScoreRow, problemId: number) {
|
||||
const mine = list.results.filter((item) => item.username === row.username)
|
||||
const target = mine.find((item) => item.result === 0) ?? mine[0]
|
||||
if (!target) {
|
||||
codeState.value = "none"
|
||||
codeState.value = 'none'
|
||||
return
|
||||
}
|
||||
const detail = await getSubmission(target.id)
|
||||
if (selected.value?.userId !== row.userId || selected.value.problemId !== problemId) return
|
||||
code.value = detail
|
||||
codeState.value = "idle"
|
||||
codeState.value = 'idle'
|
||||
} catch {
|
||||
codeState.value = "failed"
|
||||
codeState.value = 'failed'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,15 +191,10 @@ async function toggleChecked(value: boolean) {
|
||||
const cell = selectedCell.value
|
||||
if (!row || !cell || !selected.value) return
|
||||
try {
|
||||
await updateACMHelperChecked(
|
||||
Number(props.contestID),
|
||||
row.rankId,
|
||||
String(selected.value.problemId),
|
||||
value,
|
||||
)
|
||||
await updateACMHelperChecked(Number(props.contestID), row.rankId, String(selected.value.problemId), value)
|
||||
cell.checked = value
|
||||
} catch {
|
||||
message.error("没标上,只有出这场比赛的老师能标")
|
||||
message.error('没标上,只有出这场比赛的老师能标')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,10 +208,10 @@ function step(dx: number, dy: number) {
|
||||
if (list[nr] && problems.value[nc]) pick(list[nr], problems.value[nc].id)
|
||||
}
|
||||
|
||||
onKeyStroke(["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"], (event) => {
|
||||
onKeyStroke(['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'], (event) => {
|
||||
if (!ended.value || !selected.value) return
|
||||
const target = event.target as HTMLElement | null
|
||||
if (target && ["INPUT", "TEXTAREA"].includes(target.tagName)) return
|
||||
if (target && ['INPUT', 'TEXTAREA'].includes(target.tagName)) return
|
||||
event.preventDefault()
|
||||
const map = {
|
||||
ArrowLeft: [-1, 0],
|
||||
@@ -270,59 +224,41 @@ onKeyStroke(["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"], (event) => {
|
||||
})
|
||||
|
||||
function exportGrades() {
|
||||
const title = contestStore.contest?.title ?? "比赛"
|
||||
const head = [
|
||||
"用户名",
|
||||
"班级",
|
||||
"做对",
|
||||
"名次",
|
||||
"用时",
|
||||
...problems.value.map((p) => `第${p._id}题 ${p.title}`),
|
||||
]
|
||||
const title = contestStore.contest?.title ?? '比赛'
|
||||
const head = ['用户名', '班级', '做对', '名次', '用时', ...problems.value.map((p) => `第${p._id}题 ${p.title}`)]
|
||||
const lines = [head]
|
||||
const byUsername = [...rows.value].sort((a, b) => a.username.localeCompare(b.username, "zh-CN"))
|
||||
const byUsername = [...rows.value].sort((a, b) => a.username.localeCompare(b.username, 'zh-CN'))
|
||||
for (const row of byUsername) {
|
||||
lines.push([
|
||||
row.username,
|
||||
row.className ? classLabel(row.className) : "",
|
||||
row.className ? classLabel(row.className) : '',
|
||||
String(row.solved),
|
||||
String(row.rank ?? ""),
|
||||
String(row.rank ?? ''),
|
||||
secondsToDuration(row.totalTime),
|
||||
...problems.value.map((p) => {
|
||||
const cell = cellOf(row, p.id)
|
||||
return !cell ? "没交" : cell.isAc ? "对" : "错"
|
||||
return !cell ? '没交' : cell.isAc ? '对' : '错'
|
||||
}),
|
||||
])
|
||||
}
|
||||
for (const user of absent.value) {
|
||||
lines.push([
|
||||
user.username,
|
||||
user.className ? classLabel(user.className) : "",
|
||||
user.leave ? "请假" : "没考",
|
||||
"",
|
||||
"",
|
||||
...problems.value.map(() => ""),
|
||||
])
|
||||
lines.push([user.username, user.className ? classLabel(user.className) : '', user.leave ? '请假' : '没考', '', '', ...problems.value.map(() => '')])
|
||||
}
|
||||
const csv = lines
|
||||
.map((line) => line.map((value) => `"${value.replaceAll('"', '""')}"`).join(","))
|
||||
.join("\r\n")
|
||||
const csv = lines.map((line) => line.map((value) => `"${value.replaceAll('"', '""')}"`).join(',')).join('\r\n')
|
||||
// 带 BOM,Excel 双击打开中文才不乱码
|
||||
const blob = new Blob(["" + csv], { type: "text/csv;charset=utf-8" })
|
||||
const blob = new Blob(['' + csv], { type: 'text/csv;charset=utf-8' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement("a")
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `${title} 成绩.csv`
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
const success = computed(() => tone("success"))
|
||||
const danger = computed(() => tone("error"))
|
||||
const warning = computed(() => tone("warning"))
|
||||
const liveColumns = computed(
|
||||
() => `130px 48px repeat(${problems.value.length}, minmax(70px, 96px)) 90px 1fr`,
|
||||
)
|
||||
const success = computed(() => tone('success'))
|
||||
const danger = computed(() => tone('error'))
|
||||
const warning = computed(() => tone('warning'))
|
||||
const liveColumns = computed(() => `130px 48px repeat(${problems.value.length}, minmax(70px, 96px)) 90px 1fr`)
|
||||
const gradeColumns = computed(() => `24px 124px 40px 36px repeat(${problems.value.length}, 50px)`)
|
||||
</script>
|
||||
|
||||
@@ -336,9 +272,7 @@ const gradeColumns = computed(() => `24px 124px 40px 36px repeat(${problems.valu
|
||||
<div class="stat">
|
||||
<b>{{ rows.length }}</b>
|
||||
<span
|
||||
>进来了<template v-if="classNames">
|
||||
· {{ classNames }} {{ rosterSize }} 人</template
|
||||
></span
|
||||
>进来了<template v-if="classNames"> · {{ classNames }} {{ rosterSize }} 人</template></span
|
||||
>
|
||||
</div>
|
||||
<div v-if="absent.length" class="stat">
|
||||
@@ -377,24 +311,16 @@ const gradeColumns = computed(() => `24px 124px 40px 36px repeat(${problems.valu
|
||||
|
||||
<template v-if="attention.length">
|
||||
<div class="group red-group">要看一眼的 {{ attention.length }} 人</div>
|
||||
<div
|
||||
v-for="{ row, flag } in attention"
|
||||
:key="row.userId"
|
||||
class="tr"
|
||||
:style="{ gridTemplateColumns: liveColumns }"
|
||||
>
|
||||
<div v-for="{ row, flag } in attention" :key="row.userId" class="tr" :style="{ gridTemplateColumns: liveColumns }">
|
||||
<UserName :username="row.username" />
|
||||
<span class="num solved">{{ row.solved }}</span>
|
||||
<span v-for="p in problems" :key="p.id" class="lc">
|
||||
<template v-if="cellOf(row, p.id)">
|
||||
<span v-if="cellOf(row, p.id)!.isAc" class="cell ac"
|
||||
>✓ {{ Math.floor(cellOf(row, p.id)!.acTime / 60) }}′<small
|
||||
v-if="cellOf(row, p.id)!.errors"
|
||||
>-{{ cellOf(row, p.id)!.errors }}</small
|
||||
></span
|
||||
>✓ {{ Math.floor(cellOf(row, p.id)!.acTime / 60) }}′<small v-if="cellOf(row, p.id)!.errors">-{{ cellOf(row, p.id)!.errors }}</small></span
|
||||
>
|
||||
<span v-else class="cell no" :class="{ hot: cellOf(row, p.id)!.errors >= 3 }">{{
|
||||
cellOf(row, p.id)!.errors ? `错 ${cellOf(row, p.id)!.errors}` : "交过"
|
||||
cellOf(row, p.id)!.errors ? `错 ${cellOf(row, p.id)!.errors}` : '交过'
|
||||
}}</span>
|
||||
</template>
|
||||
<span v-else class="dot">·</span>
|
||||
@@ -407,25 +333,15 @@ const gradeColumns = computed(() => `24px 124px 40px 36px repeat(${problems.valu
|
||||
</template>
|
||||
|
||||
<div class="group">其余 {{ others.length }} 人 · 做对少的在前</div>
|
||||
<div
|
||||
v-for="row in others"
|
||||
:key="row.userId"
|
||||
class="tr"
|
||||
:style="{ gridTemplateColumns: liveColumns }"
|
||||
>
|
||||
<div v-for="row in others" :key="row.userId" class="tr" :style="{ gridTemplateColumns: liveColumns }">
|
||||
<UserName :username="row.username" />
|
||||
<span class="num solved">{{ row.solved }}</span>
|
||||
<span v-for="p in problems" :key="p.id" class="lc">
|
||||
<template v-if="cellOf(row, p.id)">
|
||||
<span v-if="cellOf(row, p.id)!.isAc" class="cell ac"
|
||||
>✓ {{ Math.floor(cellOf(row, p.id)!.acTime / 60) }}′<small
|
||||
v-if="cellOf(row, p.id)!.errors"
|
||||
>-{{ cellOf(row, p.id)!.errors }}</small
|
||||
></span
|
||||
>✓ {{ Math.floor(cellOf(row, p.id)!.acTime / 60) }}′<small v-if="cellOf(row, p.id)!.errors">-{{ cellOf(row, p.id)!.errors }}</small></span
|
||||
>
|
||||
<span v-else class="cell no">{{
|
||||
cellOf(row, p.id)!.errors ? `错 ${cellOf(row, p.id)!.errors}` : "交过"
|
||||
}}</span>
|
||||
<span v-else class="cell no">{{ cellOf(row, p.id)!.errors ? `错 ${cellOf(row, p.id)!.errors}` : '交过' }}</span>
|
||||
</template>
|
||||
<span v-else class="dot">·</span>
|
||||
</span>
|
||||
@@ -435,18 +351,13 @@ const gradeColumns = computed(() => `24px 124px 40px 36px repeat(${problems.valu
|
||||
|
||||
<template v-if="absent.length">
|
||||
<div class="group">没进来 {{ absent.length }} 人</div>
|
||||
<div
|
||||
v-for="user in absent"
|
||||
:key="user.userId"
|
||||
class="tr absent"
|
||||
:style="{ gridTemplateColumns: liveColumns }"
|
||||
>
|
||||
<div v-for="user in absent" :key="user.userId" class="tr absent" :style="{ gridTemplateColumns: liveColumns }">
|
||||
<UserName :username="user.username" muted />
|
||||
<span></span>
|
||||
<span v-for="p in problems" :key="p.id"></span>
|
||||
<span></span>
|
||||
<span class="flag-wrap">
|
||||
<span class="flag grey">{{ user.leave ? "请假(看板里标的)" : "没进来" }}</span>
|
||||
<span class="flag grey">{{ user.leave ? '请假(看板里标的)' : '没进来' }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
@@ -466,16 +377,12 @@ const gradeColumns = computed(() => `24px 124px 40px 36px repeat(${problems.valu
|
||||
></span>
|
||||
</div>
|
||||
<div class="dist-legend">
|
||||
<span v-for="[solved, n] in distribution" :key="solved">
|
||||
<i :style="{ background: barColor(solved) }"></i>{{ solved }} 道 {{ n }} 人
|
||||
</span>
|
||||
<span v-for="[solved, n] in distribution" :key="solved"> <i :style="{ background: barColor(solved) }"></i>{{ solved }} 道 {{ n }} 人 </span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="sec small">
|
||||
{{ rows.length }} 人考了<template v-if="absent.length">
|
||||
· {{ absent.length }} 人没考<template v-if="leaveCount"
|
||||
>({{ leaveCount }} 人请假)</template
|
||||
></template
|
||||
· {{ absent.length }} 人没考<template v-if="leaveCount">({{ leaveCount }} 人请假)</template></template
|
||||
>
|
||||
· 平均做对 {{ average }} 道
|
||||
</span>
|
||||
@@ -488,13 +395,9 @@ const gradeColumns = computed(() => `24px 124px 40px 36px repeat(${problems.valu
|
||||
<section class="sheet">
|
||||
<div class="sort">
|
||||
<span class="muted small">排序</span>
|
||||
<button class="chip" :class="{ on: sort === 'username' }" @click="sort = 'username'">
|
||||
姓名
|
||||
</button>
|
||||
<button class="chip" :class="{ on: sort === 'username' }" @click="sort = 'username'">姓名</button>
|
||||
<button class="chip" :class="{ on: sort === 'rank' }" @click="sort = 'rank'">名次</button>
|
||||
<button class="chip" :class="{ on: sort === 'weak' }" @click="sort = 'weak'">
|
||||
做对少的在前
|
||||
</button>
|
||||
<button class="chip" :class="{ on: sort === 'weak' }" @click="sort = 'weak'">做对少的在前</button>
|
||||
</div>
|
||||
<div class="sheet-scroll">
|
||||
<div class="tr th" :style="{ gridTemplateColumns: gradeColumns }">
|
||||
@@ -536,15 +439,10 @@ const gradeColumns = computed(() => `24px 124px 40px 36px repeat(${problems.valu
|
||||
<i v-if="cellOf(row, p.id)?.checked" class="seen"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-for="user in absent"
|
||||
:key="user.userId"
|
||||
class="tr absent"
|
||||
:style="{ gridTemplateColumns: gradeColumns }"
|
||||
>
|
||||
<div v-for="user in absent" :key="user.userId" class="tr absent" :style="{ gridTemplateColumns: gradeColumns }">
|
||||
<span></span>
|
||||
<UserName :username="user.username" muted />
|
||||
<span class="tiny">{{ user.leave ? "请假" : "没考" }}</span>
|
||||
<span class="tiny">{{ user.leave ? '请假' : '没考' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -555,49 +453,29 @@ const gradeColumns = computed(() => `24px 124px 40px 36px repeat(${problems.valu
|
||||
<UserName :username="selectedRow.username" />
|
||||
<span class="muted">·</span>
|
||||
<span
|
||||
><span class="muted">{{ selectedProblem._id }}</span>
|
||||
{{ selectedProblem.title }}</span
|
||||
><span class="muted">{{ selectedProblem._id }}</span> {{ selectedProblem.title }}</span
|
||||
>
|
||||
<span v-if="selectedCell?.isAc" class="pill ok">
|
||||
{{
|
||||
Math.floor(selectedCell.acTime / 60) === 0
|
||||
? "1 分钟内做对"
|
||||
: `第 ${Math.floor(selectedCell.acTime / 60)} 分钟做对`
|
||||
}}
|
||||
{{ Math.floor(selectedCell.acTime / 60) === 0 ? '1 分钟内做对' : `第 ${Math.floor(selectedCell.acTime / 60)} 分钟做对` }}
|
||||
</span>
|
||||
<span v-else-if="selectedCell" class="pill bad">没做对</span>
|
||||
<span v-if="selectedCell?.errors" class="muted small"
|
||||
>错过 {{ selectedCell.errors }} 次</span
|
||||
>
|
||||
<span v-if="selectedCell?.errors" class="muted small">错过 {{ selectedCell.errors }} 次</span>
|
||||
<span v-if="selectedCell?.firstAc" class="muted small">· 全班最先做对</span>
|
||||
</div>
|
||||
<div class="code-box">
|
||||
<n-code
|
||||
v-if="code"
|
||||
:code="code.code"
|
||||
:language="LANGUAGE_FORMAT_VALUE[code.language]"
|
||||
show-line-numbers
|
||||
/>
|
||||
<n-code v-if="code" :code="code.code" :language="LANGUAGE_FORMAT_VALUE[code.language]" show-line-numbers />
|
||||
<span v-else-if="codeState === 'loading'" class="muted">代码加载中…</span>
|
||||
<span v-else-if="codeState === 'none'" class="muted">这题没交</span>
|
||||
<span v-else-if="codeState === 'failed'" class="muted">代码没拉下来,再点一次</span>
|
||||
</div>
|
||||
<div class="code-foot">
|
||||
<n-checkbox
|
||||
v-if="selectedCell?.isAc"
|
||||
:checked="selectedCell.checked"
|
||||
:disabled="!contestStore.isContestAdmin"
|
||||
@update:checked="toggleChecked"
|
||||
<n-checkbox v-if="selectedCell?.isAc" :checked="selectedCell.checked" :disabled="!contestStore.isContestAdmin" @update:checked="toggleChecked"
|
||||
>看过了,没问题</n-checkbox
|
||||
>
|
||||
<span v-if="code" class="muted tiny">
|
||||
{{ code.language }} · {{ parseTime(code.createTime, "M月D日 HH:mm:ss") }}
|
||||
</span>
|
||||
<span v-if="code" class="muted tiny"> {{ code.language }} · {{ parseTime(code.createTime, 'M月D日 HH:mm:ss') }} </span>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="code-empty muted">
|
||||
点左边任意一格,这里看那个人那道题的代码。<br />做对的格子看完可以标「看过了」,查抄袭用。
|
||||
</div>
|
||||
<div v-else class="code-empty muted">点左边任意一格,这里看那个人那道题的代码。<br />做对的格子看完可以标「看过了」,查抄袭用。</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
@@ -607,15 +485,15 @@ const gradeColumns = computed(() => `24px 124px 40px 36px repeat(${problems.valu
|
||||
.empty {
|
||||
padding: 60px 20px;
|
||||
text-align: center;
|
||||
color: v-bind("theme.textColor3");
|
||||
color: v-bind('theme.textColor3');
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: v-bind("theme.textColor3");
|
||||
color: v-bind('theme.textColor3');
|
||||
}
|
||||
|
||||
.sec {
|
||||
color: v-bind("theme.textColor2");
|
||||
color: v-bind('theme.textColor2');
|
||||
}
|
||||
|
||||
.small {
|
||||
@@ -649,7 +527,7 @@ const gradeColumns = computed(() => `24px 124px 40px 36px repeat(${problems.valu
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px 36px;
|
||||
border-bottom: 1px solid v-bind("theme.dividerColor");
|
||||
border-bottom: 1px solid v-bind('theme.dividerColor');
|
||||
}
|
||||
|
||||
.stat {
|
||||
@@ -666,19 +544,19 @@ const gradeColumns = computed(() => `24px 124px 40px 36px repeat(${problems.valu
|
||||
|
||||
.stat span {
|
||||
font-size: var(--oj-fs-meta);
|
||||
color: v-bind("theme.textColor3");
|
||||
color: v-bind('theme.textColor3');
|
||||
}
|
||||
|
||||
.stat .grey {
|
||||
color: v-bind("theme.textColor3");
|
||||
color: v-bind('theme.textColor3');
|
||||
}
|
||||
|
||||
.stat .red {
|
||||
color: v-bind("danger.color");
|
||||
color: v-bind('danger.color');
|
||||
}
|
||||
|
||||
.stat .amber {
|
||||
color: v-bind("warning.color");
|
||||
color: v-bind('warning.color');
|
||||
}
|
||||
|
||||
.grid-wrap {
|
||||
@@ -694,14 +572,14 @@ const gradeColumns = computed(() => `24px 124px 40px 36px repeat(${problems.valu
|
||||
padding: 0 var(--oj-pad-x);
|
||||
font-size: var(--oj-fs-body);
|
||||
box-sizing: border-box;
|
||||
border-bottom: 1px solid v-bind("theme.dividerColor");
|
||||
border-bottom: 1px solid v-bind('theme.dividerColor');
|
||||
}
|
||||
|
||||
.th {
|
||||
min-height: 50px;
|
||||
font-size: var(--oj-fs-meta);
|
||||
color: v-bind("theme.textColor3");
|
||||
background: v-bind("theme.actionColor");
|
||||
color: v-bind('theme.textColor3');
|
||||
background: v-bind('theme.actionColor');
|
||||
}
|
||||
|
||||
.ph {
|
||||
@@ -712,7 +590,7 @@ const gradeColumns = computed(() => `24px 124px 40px 36px repeat(${problems.valu
|
||||
}
|
||||
|
||||
.ph b {
|
||||
color: v-bind("theme.textColor2");
|
||||
color: v-bind('theme.textColor2');
|
||||
}
|
||||
|
||||
.ph.center {
|
||||
@@ -751,24 +629,24 @@ const gradeColumns = computed(() => `24px 124px 40px 36px repeat(${problems.valu
|
||||
|
||||
.cell.ac {
|
||||
font-weight: 600;
|
||||
background: v-bind("success.background");
|
||||
color: v-bind("success.color");
|
||||
background: v-bind('success.background');
|
||||
color: v-bind('success.color');
|
||||
}
|
||||
|
||||
.cell.no {
|
||||
background: v-bind("danger.background");
|
||||
color: v-bind("danger.color");
|
||||
background: v-bind('danger.background');
|
||||
color: v-bind('danger.color');
|
||||
}
|
||||
|
||||
.cell.no.hot {
|
||||
font-weight: 700;
|
||||
box-shadow: inset 0 0 0 1px v-bind("danger.color");
|
||||
box-shadow: inset 0 0 0 1px v-bind('danger.color');
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
color: v-bind("theme.textColor3");
|
||||
color: v-bind('theme.textColor3');
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
@@ -779,14 +657,14 @@ const gradeColumns = computed(() => `24px 124px 40px 36px repeat(${problems.valu
|
||||
align-items: center;
|
||||
font-size: var(--oj-fs-meta);
|
||||
font-weight: 600;
|
||||
color: v-bind("theme.textColor3");
|
||||
background: v-bind("theme.actionColor");
|
||||
border-bottom: 1px solid v-bind("theme.dividerColor");
|
||||
color: v-bind('theme.textColor3');
|
||||
background: v-bind('theme.actionColor');
|
||||
border-bottom: 1px solid v-bind('theme.dividerColor');
|
||||
}
|
||||
|
||||
.red-group {
|
||||
color: v-bind("danger.color");
|
||||
background: v-bind("danger.background");
|
||||
color: v-bind('danger.color');
|
||||
background: v-bind('danger.background');
|
||||
}
|
||||
|
||||
.flag-wrap {
|
||||
@@ -805,23 +683,23 @@ const gradeColumns = computed(() => `24px 124px 40px 36px repeat(${problems.valu
|
||||
}
|
||||
|
||||
.flag.stuck {
|
||||
background: v-bind("danger.background");
|
||||
color: v-bind("danger.color");
|
||||
background: v-bind('danger.background');
|
||||
color: v-bind('danger.color');
|
||||
}
|
||||
|
||||
.flag.idle {
|
||||
background: v-bind("warning.background");
|
||||
color: v-bind("warning.color");
|
||||
background: v-bind('warning.background');
|
||||
color: v-bind('warning.color');
|
||||
}
|
||||
|
||||
.flag.grey {
|
||||
background: v-bind("theme.actionColor");
|
||||
color: v-bind("theme.textColor3");
|
||||
background: v-bind('theme.actionColor');
|
||||
color: v-bind('theme.textColor3');
|
||||
}
|
||||
|
||||
.tr.absent {
|
||||
color: v-bind("theme.textColor3");
|
||||
background: v-bind("theme.actionColor");
|
||||
color: v-bind('theme.textColor3');
|
||||
background: v-bind('theme.actionColor');
|
||||
}
|
||||
|
||||
/* ---------- 成绩 ---------- */
|
||||
@@ -834,7 +712,7 @@ const gradeColumns = computed(() => `24px 124px 40px 36px repeat(${problems.valu
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px 24px;
|
||||
border-bottom: 1px solid v-bind("theme.dividerColor");
|
||||
border-bottom: 1px solid v-bind('theme.dividerColor');
|
||||
}
|
||||
|
||||
.dist {
|
||||
@@ -857,7 +735,7 @@ const gradeColumns = computed(() => `24px 124px 40px 36px repeat(${problems.valu
|
||||
flex-wrap: wrap;
|
||||
gap: 4px 14px;
|
||||
font-size: var(--oj-fs-meta);
|
||||
color: v-bind("theme.textColor2");
|
||||
color: v-bind('theme.textColor2');
|
||||
}
|
||||
|
||||
.dist-legend span {
|
||||
@@ -880,7 +758,7 @@ const gradeColumns = computed(() => `24px 124px 40px 36px repeat(${problems.valu
|
||||
.sheet {
|
||||
flex: 1 1 560px;
|
||||
min-width: 0;
|
||||
border-right: 1px solid v-bind("theme.dividerColor");
|
||||
border-right: 1px solid v-bind('theme.dividerColor');
|
||||
}
|
||||
|
||||
.sort {
|
||||
@@ -889,25 +767,25 @@ const gradeColumns = computed(() => `24px 124px 40px 36px repeat(${problems.valu
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
border-bottom: 1px solid v-bind("theme.dividerColor");
|
||||
border-bottom: 1px solid v-bind('theme.dividerColor');
|
||||
}
|
||||
|
||||
.chip {
|
||||
height: 30px;
|
||||
padding: 0 12px;
|
||||
border-radius: 15px;
|
||||
border: 1px solid v-bind("theme.borderColor");
|
||||
border: 1px solid v-bind('theme.borderColor');
|
||||
background: transparent;
|
||||
font: inherit;
|
||||
font-size: var(--oj-fs-sec);
|
||||
color: v-bind("theme.textColor2");
|
||||
color: v-bind('theme.textColor2');
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.chip.on {
|
||||
border-color: transparent;
|
||||
background: v-bind("success.background");
|
||||
color: v-bind("success.color");
|
||||
background: v-bind('success.background');
|
||||
color: v-bind('success.color');
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@@ -929,7 +807,7 @@ const gradeColumns = computed(() => `24px 124px 40px 36px repeat(${problems.valu
|
||||
}
|
||||
|
||||
.tr.picked {
|
||||
background: v-bind("theme.hoverColor");
|
||||
background: v-bind('theme.hoverColor');
|
||||
}
|
||||
|
||||
.gc {
|
||||
@@ -940,22 +818,22 @@ const gradeColumns = computed(() => `24px 124px 40px 36px repeat(${problems.valu
|
||||
background: transparent;
|
||||
font: inherit;
|
||||
font-size: var(--oj-fs-sec);
|
||||
color: v-bind("theme.textColor3");
|
||||
color: v-bind('theme.textColor3');
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.gc.ac {
|
||||
background: v-bind("success.background");
|
||||
color: v-bind("success.color");
|
||||
background: v-bind('success.background');
|
||||
color: v-bind('success.color');
|
||||
}
|
||||
|
||||
.gc.no {
|
||||
background: v-bind("danger.background");
|
||||
color: v-bind("danger.color");
|
||||
background: v-bind('danger.background');
|
||||
color: v-bind('danger.color');
|
||||
}
|
||||
|
||||
.gc.sel {
|
||||
box-shadow: 0 0 0 2px v-bind("theme.textColor1");
|
||||
box-shadow: 0 0 0 2px v-bind('theme.textColor1');
|
||||
}
|
||||
|
||||
.seen {
|
||||
@@ -965,7 +843,7 @@ const gradeColumns = computed(() => `24px 124px 40px 36px repeat(${problems.valu
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: 50%;
|
||||
background: v-bind("success.color");
|
||||
background: v-bind('success.color');
|
||||
}
|
||||
|
||||
.code-panel {
|
||||
@@ -976,7 +854,7 @@ const gradeColumns = computed(() => `24px 124px 40px 36px repeat(${problems.valu
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
background: v-bind("theme.actionColor");
|
||||
background: v-bind('theme.actionColor');
|
||||
}
|
||||
|
||||
.code-head {
|
||||
@@ -997,13 +875,13 @@ const gradeColumns = computed(() => `24px 124px 40px 36px repeat(${problems.valu
|
||||
}
|
||||
|
||||
.pill.ok {
|
||||
background: v-bind("success.background");
|
||||
color: v-bind("success.color");
|
||||
background: v-bind('success.background');
|
||||
color: v-bind('success.color');
|
||||
}
|
||||
|
||||
.pill.bad {
|
||||
background: v-bind("danger.background");
|
||||
color: v-bind("danger.color");
|
||||
background: v-bind('danger.background');
|
||||
color: v-bind('danger.color');
|
||||
}
|
||||
|
||||
.code-box {
|
||||
@@ -1012,9 +890,9 @@ const gradeColumns = computed(() => `24px 124px 40px 36px repeat(${problems.valu
|
||||
max-height: calc(100vh - 360px);
|
||||
overflow: auto;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid v-bind("theme.dividerColor");
|
||||
border: 1px solid v-bind('theme.dividerColor');
|
||||
border-radius: var(--oj-radius);
|
||||
background: v-bind("theme.cardColor");
|
||||
background: v-bind('theme.cardColor');
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user