feat: 重新设计成绩册
左右固定列从 11 列减到 6 列,1366 宽一屏能看到全部任务;任务按教程 / 挑战分组, 表头显示题目名和交的人数;单元格只区分缺交、未评和分数;考核等级和分数合成一列, 教程 / 挑战合计和平均放到总分的悬停提示里;名次、考核、总分、已交可以排序; 加“怎么算的”说明;默认打开批改页当前的班级。计算规则和 CSV 不变。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
06267fb0ff
commit
726906e3f6
1 file changed
+379
-247
+379
-247
@@ -1,95 +1,116 @@
|
|||||||
<template>
|
<template>
|
||||||
<n-flex
|
<div class="gradebook">
|
||||||
vertical
|
<div class="toolbar">
|
||||||
:size="12"
|
<n-select
|
||||||
style="height: 100%; min-width: 0; box-sizing: border-box; padding: 10px 10px 10px 0; overflow: hidden;"
|
v-model:value="query.classname"
|
||||||
>
|
class="class-select"
|
||||||
<n-flex class="toolbar" align="center" justify="space-between" style="flex-shrink: 0;">
|
:options="classOptions"
|
||||||
<n-flex align="center" :size="8" wrap style="min-width: 0;">
|
placeholder="班级"
|
||||||
<n-select
|
size="small"
|
||||||
v-model:value="query.classname"
|
:loading="classesLoading"
|
||||||
class="class-select"
|
/>
|
||||||
:options="classOptions"
|
<n-radio-group v-model:value="query.task_type" size="small">
|
||||||
placeholder="班级"
|
<n-radio-button value="">全部</n-radio-button>
|
||||||
:loading="classesLoading"
|
<n-radio-button value="tutorial">教程</n-radio-button>
|
||||||
/>
|
<n-radio-button value="challenge">挑战</n-radio-button>
|
||||||
<n-select
|
</n-radio-group>
|
||||||
v-model:value="query.task_type"
|
<n-input
|
||||||
class="type-select"
|
v-model:value="query.username"
|
||||||
:options="taskTypeOptions"
|
class="search-input"
|
||||||
/>
|
size="small"
|
||||||
<n-input
|
clearable
|
||||||
v-model:value="query.username"
|
placeholder="搜索学生"
|
||||||
class="search-input"
|
>
|
||||||
clearable
|
<template #prefix><Icon icon="lucide:search" :width="14" /></template>
|
||||||
placeholder="学生搜索"
|
</n-input>
|
||||||
/>
|
<n-checkbox v-model:checked="query.include_all_tasks" size="small">
|
||||||
<n-switch v-model:value="query.include_all_tasks">
|
显示不计入的任务
|
||||||
<template #checked>全部有提交任务</template>
|
</n-checkbox>
|
||||||
<template #unchecked>只看计入任务</template>
|
|
||||||
</n-switch>
|
<span class="spacer"></span>
|
||||||
</n-flex>
|
|
||||||
<n-flex align="center" :size="8">
|
<span v-if="gradebook" class="summary">
|
||||||
<n-button
|
{{ gradebook.student_count }} 人 · 计入 {{ gradebook.included_task_count }} 个任务
|
||||||
secondary
|
</span>
|
||||||
title="刷新"
|
<n-popover trigger="click" placement="bottom-end" style="max-width: 340px">
|
||||||
:disabled="!query.classname"
|
<template #trigger>
|
||||||
:loading="loading"
|
<n-button size="small" quaternary>
|
||||||
@click="loadGradebook"
|
<template #icon><Icon icon="lucide:info" /></template>
|
||||||
>
|
怎么算的
|
||||||
<template #icon>
|
</n-button>
|
||||||
<Icon icon="lucide:refresh-cw" :width="15" />
|
</template>
|
||||||
</template>
|
<div class="rules">
|
||||||
</n-button>
|
<p>
|
||||||
<n-button
|
<b>计入的任务</b>:班里至少一半人交过的任务才计入(现在是
|
||||||
type="primary"
|
{{ gradebook?.coverage_threshold_count ?? "—" }} 人),灰色的列不计入。
|
||||||
secondary
|
</p>
|
||||||
:disabled="!query.classname || !gradebook"
|
<p><b>每格分数</b>:这个学生在这个任务里分数最高的一次提交,点开可以批改。</p>
|
||||||
:loading="exporting"
|
<p>
|
||||||
@click="exportCsv"
|
<b>总分 / 平时等级</b>:计入任务的分数相加后排名,A 30%、B 40%、C 20%、D 5%、E 5%。
|
||||||
>
|
</p>
|
||||||
<template #icon>
|
<p>
|
||||||
<Icon icon="lucide:download" :width="15" />
|
<b>考核</b>:计入任务里最高三次的平均分另外排名,A 10%、B 20%、C 40%、D 20%、E 10%;
|
||||||
</template>
|
分数在等级的分数段内按名次铺开(A 92–99、B 84–91、C 76–83、D 68–75、E 60–67)。
|
||||||
导出 CSV
|
</p>
|
||||||
</n-button>
|
</div>
|
||||||
</n-flex>
|
</n-popover>
|
||||||
</n-flex>
|
<n-tooltip>
|
||||||
|
<template #trigger>
|
||||||
|
<n-button
|
||||||
|
size="small"
|
||||||
|
quaternary
|
||||||
|
:disabled="!query.classname"
|
||||||
|
:loading="loading"
|
||||||
|
@click="loadGradebook"
|
||||||
|
>
|
||||||
|
<template #icon><Icon icon="lucide:refresh-cw" /></template>
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
|
刷新
|
||||||
|
</n-tooltip>
|
||||||
|
<n-button
|
||||||
|
size="small"
|
||||||
|
secondary
|
||||||
|
type="primary"
|
||||||
|
:disabled="!gradebook"
|
||||||
|
:loading="exporting"
|
||||||
|
@click="exportCsv"
|
||||||
|
>
|
||||||
|
<template #icon><Icon icon="lucide:download" /></template>
|
||||||
|
导出 CSV
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<n-alert v-if="loadError" type="error" closable @close="loadError = ''">
|
<n-alert v-if="loadError" type="error" closable @close="loadError = ''">
|
||||||
{{ loadError }}
|
{{ loadError }}
|
||||||
</n-alert>
|
</n-alert>
|
||||||
|
|
||||||
<n-flex v-if="gradebook" align="center" :size="8" style="flex-shrink: 0;">
|
|
||||||
<n-tag size="small">学生 {{ gradebook.student_count }}</n-tag>
|
|
||||||
<n-tag size="small">任务 {{ gradebook.task_count }}</n-tag>
|
|
||||||
<n-tag size="small" type="success">
|
|
||||||
计入 {{ gradebook.included_task_count }}
|
|
||||||
</n-tag>
|
|
||||||
<n-tag size="small">
|
|
||||||
覆盖门槛 {{ gradebook.coverage_threshold_count }} 人
|
|
||||||
</n-tag>
|
|
||||||
</n-flex>
|
|
||||||
|
|
||||||
<n-data-table
|
<n-data-table
|
||||||
|
class="table gradebook-table"
|
||||||
size="small"
|
size="small"
|
||||||
striped
|
|
||||||
flex-height
|
flex-height
|
||||||
|
:single-line="false"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
:data="rows"
|
:data="rows"
|
||||||
:row-key="(row: GradebookRow) => row.user_id"
|
:row-key="(row: GradebookRow) => row.user_id"
|
||||||
:scroll-x="scrollX"
|
:scroll-x="scrollX"
|
||||||
style="flex: 1; min-height: 0;"
|
|
||||||
/>
|
/>
|
||||||
</n-flex>
|
</div>
|
||||||
|
|
||||||
<n-drawer
|
<n-drawer
|
||||||
:show="!!reviewId"
|
:show="!!reviewId"
|
||||||
width="min(1180px, 92vw)"
|
width="min(1180px, 92vw)"
|
||||||
placement="right"
|
placement="right"
|
||||||
@update:show="(v: boolean) => !v && (reviewId = null)"
|
@update:show="(v: boolean) => !v && (reviewId = null)"
|
||||||
>
|
>
|
||||||
<ReviewPanel v-if="reviewId" :submission-id="reviewId" wide @close="reviewId = null" />
|
<ReviewPanel
|
||||||
|
v-if="reviewId"
|
||||||
|
:submission-id="reviewId"
|
||||||
|
wide
|
||||||
|
@close="reviewId = null"
|
||||||
|
@changed="loadGradebook"
|
||||||
|
/>
|
||||||
</n-drawer>
|
</n-drawer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -98,13 +119,7 @@ import ReviewPanel from "../components/review/ReviewPanel.vue"
|
|||||||
import { computed, h, onMounted, reactive, ref, watch } from "vue"
|
import { computed, h, onMounted, reactive, ref, watch } from "vue"
|
||||||
import { Icon } from "../icons"
|
import { Icon } from "../icons"
|
||||||
import { watchDebounced } from "@vueuse/core"
|
import { watchDebounced } from "@vueuse/core"
|
||||||
import {
|
import { useMessage, type DataTableColumn } from "naive-ui"
|
||||||
NButton,
|
|
||||||
NTag,
|
|
||||||
NText,
|
|
||||||
useMessage,
|
|
||||||
type DataTableColumn,
|
|
||||||
} from "naive-ui"
|
|
||||||
import { Account, Gradebook } from "../api"
|
import { Account, Gradebook } from "../api"
|
||||||
import { displayGradebookStudentName } from "../utils/gradebook"
|
import { displayGradebookStudentName } from "../utils/gradebook"
|
||||||
import type {
|
import type {
|
||||||
@@ -113,9 +128,11 @@ import type {
|
|||||||
GradebookQuery,
|
GradebookQuery,
|
||||||
GradebookRow,
|
GradebookRow,
|
||||||
GradebookTask,
|
GradebookTask,
|
||||||
GradebookTaskType,
|
|
||||||
} from "../utils/type"
|
} from "../utils/type"
|
||||||
|
|
||||||
|
// 和批改页共用“当前班级”(那边的“全部班级”在这里不适用)
|
||||||
|
const CLASS_KEY = "review-class"
|
||||||
|
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
const classesLoading = ref(false)
|
const classesLoading = ref(false)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
@@ -131,28 +148,19 @@ const query = reactive<GradebookQuery>({
|
|||||||
include_all_tasks: false,
|
include_all_tasks: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
const taskTypeOptions: { label: string; value: GradebookTaskType | "" }[] = [
|
|
||||||
{ label: "全部", value: "" },
|
|
||||||
{ label: "教程", value: "tutorial" },
|
|
||||||
{ label: "挑战", value: "challenge" },
|
|
||||||
]
|
|
||||||
|
|
||||||
const classOptions = computed(() =>
|
const classOptions = computed(() =>
|
||||||
classes.value.map((classname) => ({ label: classname, value: classname })),
|
classes.value.map((classname) => ({ label: `${classname} 班`, value: classname })),
|
||||||
)
|
)
|
||||||
const rows = computed(() => gradebook.value?.rows ?? [])
|
const rows = computed(() => gradebook.value?.rows ?? [])
|
||||||
const scrollX = computed(() => 1040 + (gradebook.value?.tasks.length ?? 0) * 96)
|
|
||||||
|
const TASK_WIDTH = 66
|
||||||
|
const scrollX = computed(() => 380 + (gradebook.value?.tasks.length ?? 0) * TASK_WIDTH)
|
||||||
|
|
||||||
function formatScore(value: number | null) {
|
function formatScore(value: number | null) {
|
||||||
if (value === null) return "-"
|
if (value === null) return "—"
|
||||||
return Number.isInteger(value) ? String(value) : value.toFixed(2)
|
return Number.isInteger(value) ? String(value) : value.toFixed(2)
|
||||||
}
|
}
|
||||||
|
|
||||||
function taskTitle(task: GradebookTask) {
|
|
||||||
const typeLabel = task.task_type === "tutorial" ? "教程" : "挑战"
|
|
||||||
return `${typeLabel}${task.display}`
|
|
||||||
}
|
|
||||||
|
|
||||||
// 点分数:在右侧打开和批改页一样的详情,能直接打分、看提示词
|
// 点分数:在右侧打开和批改页一样的详情,能直接打分、看提示词
|
||||||
const reviewId = ref<string | null>(null)
|
const reviewId = ref<string | null>(null)
|
||||||
function openSubmission(cell: GradebookCell) {
|
function openSubmission(cell: GradebookCell) {
|
||||||
@@ -160,152 +168,135 @@ function openSubmission(cell: GradebookCell) {
|
|||||||
reviewId.value = cell.submission_id
|
reviewId.value = cell.submission_id
|
||||||
}
|
}
|
||||||
|
|
||||||
function gradeTagType(grade: string) {
|
function renderGrade(grade: string, extra?: string | number) {
|
||||||
if (grade === "A") return "success"
|
return h("span", { class: ["grade", `g-${grade}`] }, [
|
||||||
if (grade === "B") return "info"
|
grade,
|
||||||
if (grade === "C") return "default"
|
extra === undefined ? null : h("small", String(extra)),
|
||||||
return "warning"
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderTaskHeader(task: GradebookTask) {
|
function renderTaskHeader(task: GradebookTask) {
|
||||||
return h("div", { class: ["task-header", { muted: !task.included }] }, [
|
const pct = Math.round(task.coverage * 100)
|
||||||
h("div", { class: "task-title", title: task.title }, taskTitle(task)),
|
const studentCount = gradebook.value?.student_count ?? 0
|
||||||
h("div", { class: "task-meta" }, [
|
return h(
|
||||||
h("span", `${Math.round(task.coverage * 100)}%`),
|
"div",
|
||||||
task.included
|
{
|
||||||
? null
|
class: ["task-head", { excluded: !task.included }],
|
||||||
: h(NTag, { size: "tiny", round: false }, { default: () => "未计入" }),
|
title: `${task.display}. ${task.title}\n交了 ${task.submitted_count} / ${studentCount} 人(${pct}%)${task.included ? "" : ",不计入"}`,
|
||||||
]),
|
},
|
||||||
])
|
[
|
||||||
|
h("div", { class: "task-name" }, task.title),
|
||||||
|
h("div", { class: "task-cov" }, [
|
||||||
|
h("span", { class: "cov-bar" }, [h("span", { style: { width: pct + "%" } })]),
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderScore(row: GradebookRow, task: GradebookTask) {
|
function renderScore(row: GradebookRow, task: GradebookTask) {
|
||||||
const cell = row.scores[task.id]
|
const cell = row.scores[task.id]
|
||||||
if (!cell || !cell.submitted) {
|
if (!cell?.submitted)
|
||||||
return h(
|
return h("span", { class: ["cell", "missing", { excluded: !task.included }] }, "缺")
|
||||||
NText,
|
|
||||||
{ class: "missing-cell", type: "error" },
|
|
||||||
{ default: () => "缺交" },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return h(
|
return h(
|
||||||
NButton,
|
"button",
|
||||||
{
|
{
|
||||||
text: true,
|
class: ["cell", "score", { unrated: cell.score === 0, excluded: !task.included }],
|
||||||
type: task.included ? "primary" : "default",
|
title: cell.score === 0 ? "交了,还没人评分" : `${formatScore(cell.score)},点开批改`,
|
||||||
class: ["score-link", { muted: !task.included }],
|
onClick: () => openSubmission(cell),
|
||||||
onClick: (event: MouseEvent) => {
|
|
||||||
event.stopPropagation()
|
|
||||||
openSubmission(cell)
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{ default: () => formatScore(cell.score) },
|
cell.score === 0 ? "未评" : cell.score.toFixed(1),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderMissingCount(value: number) {
|
function taskColumn(task: GradebookTask): DataTableColumn<GradebookRow> {
|
||||||
if (value <= 0) return "0"
|
return {
|
||||||
return h(NText, { type: "error" }, { default: () => String(value) })
|
title: () => renderTaskHeader(task),
|
||||||
|
key: `task-${task.id}`,
|
||||||
|
width: TASK_WIDTH,
|
||||||
|
align: "center",
|
||||||
|
className: task.included ? "" : "excluded-col",
|
||||||
|
render: (row) => renderScore(row, task),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const columns = computed<DataTableColumn<GradebookRow>[]>(() => {
|
const columns = computed<DataTableColumn<GradebookRow>[]>(() => {
|
||||||
const tasks = gradebook.value?.tasks ?? []
|
const tasks = gradebook.value?.tasks ?? []
|
||||||
|
const groups = (["tutorial", "challenge"] as const)
|
||||||
|
.map((type) => ({ type, items: tasks.filter((t) => t.task_type === type) }))
|
||||||
|
.filter((g) => g.items.length)
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
title: "排名",
|
title: "#",
|
||||||
key: "rank",
|
key: "rank",
|
||||||
width: 60,
|
width: 40,
|
||||||
fixed: "left",
|
fixed: "left",
|
||||||
},
|
align: "right",
|
||||||
{
|
sorter: (a, b) => a.rank - b.rank,
|
||||||
title: "平时等级",
|
render: (row) => h("span", { class: "rank" }, row.rank),
|
||||||
key: "grade",
|
|
||||||
width: 80,
|
|
||||||
fixed: "left",
|
|
||||||
render: (row) =>
|
|
||||||
h(
|
|
||||||
NTag,
|
|
||||||
{ size: "small", type: gradeTagType(row.grade) },
|
|
||||||
{ default: () => row.grade },
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "考核等级",
|
|
||||||
key: "assessment_grade",
|
|
||||||
width: 80,
|
|
||||||
fixed: "left",
|
|
||||||
render: (row) =>
|
|
||||||
h(
|
|
||||||
NTag,
|
|
||||||
{ size: "small", type: gradeTagType(row.assessment_grade) },
|
|
||||||
{ default: () => row.assessment_grade },
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "考核分数",
|
|
||||||
key: "assessment_score",
|
|
||||||
width: 80,
|
|
||||||
fixed: "left",
|
|
||||||
render: (row) => row.assessment_score,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "学生",
|
title: "学生",
|
||||||
key: "username",
|
key: "username",
|
||||||
width: 80,
|
width: 92,
|
||||||
fixed: "left",
|
fixed: "left",
|
||||||
render: (row) => {
|
ellipsis: { tooltip: true },
|
||||||
const studentName = displayGradebookStudentName(row)
|
render: (row) => displayGradebookStudentName(row),
|
||||||
return h(NText, { title: studentName }, { default: () => studentName })
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
...tasks.map((task) => ({
|
{
|
||||||
title: () => renderTaskHeader(task),
|
title: "平时",
|
||||||
key: `task-${task.id}`,
|
key: "grade",
|
||||||
width: 96,
|
width: 52,
|
||||||
|
fixed: "left",
|
||||||
|
align: "center",
|
||||||
|
render: (row) => renderGrade(row.grade),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "考核",
|
||||||
|
key: "assessment_score",
|
||||||
|
width: 72,
|
||||||
|
fixed: "left",
|
||||||
|
sorter: (a, b) => a.assessment_score - b.assessment_score,
|
||||||
|
render: (row) => renderGrade(row.assessment_grade, row.assessment_score),
|
||||||
|
},
|
||||||
|
...groups.map((g) => ({
|
||||||
|
title: g.type === "tutorial" ? "教程" : "挑战",
|
||||||
|
key: `group-${g.type}`,
|
||||||
align: "center" as const,
|
align: "center" as const,
|
||||||
className: task.included ? "" : "excluded-task-column",
|
children: g.items.map(taskColumn),
|
||||||
render: (row: GradebookRow) => renderScore(row, task),
|
|
||||||
})),
|
})),
|
||||||
{
|
|
||||||
title: "教程合计",
|
|
||||||
key: "tutorial_total",
|
|
||||||
width: 92,
|
|
||||||
fixed: "right",
|
|
||||||
render: (row) => formatScore(row.tutorial_total),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "挑战合计",
|
|
||||||
key: "challenge_total",
|
|
||||||
width: 92,
|
|
||||||
fixed: "right",
|
|
||||||
render: (row) => formatScore(row.challenge_total),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: "总分",
|
title: "总分",
|
||||||
key: "total_score",
|
key: "total_score",
|
||||||
width: 82,
|
width: 72,
|
||||||
fixed: "right",
|
fixed: "right",
|
||||||
render: (row) => formatScore(row.total_score),
|
align: "right",
|
||||||
},
|
sorter: (a, b) => a.total_score - b.total_score,
|
||||||
{
|
render: (row) =>
|
||||||
title: "平均",
|
h(
|
||||||
key: "average_score",
|
"span",
|
||||||
width: 82,
|
{
|
||||||
fixed: "right",
|
class: "total",
|
||||||
render: (row) => formatScore(row.average_score),
|
title: `教程合计 ${formatScore(row.tutorial_total)}\n挑战合计 ${formatScore(row.challenge_total)}\n平均 ${formatScore(row.average_score)}`,
|
||||||
|
},
|
||||||
|
formatScore(row.total_score),
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "已交",
|
title: "已交",
|
||||||
key: "submitted_task_count",
|
key: "submitted_task_count",
|
||||||
width: 70,
|
width: 64,
|
||||||
fixed: "right",
|
fixed: "right",
|
||||||
},
|
align: "right",
|
||||||
{
|
sorter: (a, b) => a.missing_task_count - b.missing_task_count,
|
||||||
title: "缺交",
|
render: (row) =>
|
||||||
key: "missing_task_count",
|
h(
|
||||||
width: 70,
|
"span",
|
||||||
fixed: "right",
|
{
|
||||||
render: (row) => renderMissingCount(row.missing_task_count),
|
class: ["done", { short: row.missing_task_count > 0 }],
|
||||||
|
title: row.missing_task_count ? `缺交 ${row.missing_task_count} 个` : "都交了",
|
||||||
|
},
|
||||||
|
`${row.submitted_task_count}/${row.submitted_task_count + row.missing_task_count}`,
|
||||||
|
),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
@@ -315,7 +306,11 @@ async function loadClasses() {
|
|||||||
try {
|
try {
|
||||||
classes.value = await Account.listClasses()
|
classes.value = await Account.listClasses()
|
||||||
if (!query.classname && classes.value.length > 0) {
|
if (!query.classname && classes.value.length > 0) {
|
||||||
query.classname = classes.value[0]
|
let saved: string | null = null
|
||||||
|
try {
|
||||||
|
saved = localStorage.getItem(CLASS_KEY)
|
||||||
|
} catch {}
|
||||||
|
query.classname = saved && classes.value.includes(saved) ? saved : classes.value[0]!
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
classesLoading.value = false
|
classesLoading.value = false
|
||||||
@@ -359,6 +354,16 @@ async function exportCsv() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => query.classname,
|
||||||
|
(c) => {
|
||||||
|
if (!c) return
|
||||||
|
try {
|
||||||
|
localStorage.setItem(CLASS_KEY, c)
|
||||||
|
} catch {}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => [query.classname, query.task_type, query.include_all_tasks],
|
() => [query.classname, query.task_type, query.include_all_tasks],
|
||||||
() => loadGradebook(),
|
() => loadGradebook(),
|
||||||
@@ -379,67 +384,194 @@ onMounted(async () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.class-select {
|
.gradebook {
|
||||||
width: 150px;
|
height: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.type-select {
|
.toolbar {
|
||||||
width: 112px;
|
flex: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.class-select {
|
||||||
|
width: 120px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-input {
|
.search-input {
|
||||||
width: 160px;
|
width: 150px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-header {
|
.spacer {
|
||||||
min-width: 0;
|
flex: 1;
|
||||||
line-height: 1.2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-header.muted {
|
.summary {
|
||||||
color: var(--wp-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-title {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-meta {
|
|
||||||
display: flex;
|
|
||||||
min-height: 18px;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 4px;
|
|
||||||
margin-top: 2px;
|
|
||||||
color: var(--wp-faint);
|
|
||||||
font-size: 11px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.missing-cell {
|
|
||||||
color: var(--wp-bad);
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
|
||||||
|
|
||||||
.score-link.muted {
|
|
||||||
color: var(--wp-muted);
|
color: var(--wp-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.excluded-task-column) {
|
.rules {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rules p {
|
||||||
|
margin: 0 0 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rules p:last-child {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table :deep(.n-data-table-td) {
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table :deep(.n-data-table-th) {
|
||||||
|
padding: 6px 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* 单元格是 render 函数画的,拿不到 scoped 属性,用表格的类名限定 */
|
||||||
|
.gradebook-table .excluded-col {
|
||||||
background: var(--wp-paper);
|
background: var(--wp-paper);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
.gradebook-table .rank {
|
||||||
.toolbar {
|
font: 12px var(--wp-font-mono);
|
||||||
align-items: flex-start;
|
color: var(--wp-faint);
|
||||||
}
|
}
|
||||||
|
|
||||||
.class-select,
|
.gradebook-table .grade {
|
||||||
.type-select,
|
font-weight: 700;
|
||||||
.search-input {
|
}
|
||||||
width: 100%;
|
|
||||||
}
|
.gradebook-table .grade small {
|
||||||
|
margin-left: 6px;
|
||||||
|
font: 12px var(--wp-font-mono);
|
||||||
|
color: var(--wp-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gradebook-table .g-A {
|
||||||
|
color: var(--wp-brand);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gradebook-table .g-B {
|
||||||
|
color: var(--wp-css);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gradebook-table .g-C {
|
||||||
|
color: var(--wp-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gradebook-table .g-D {
|
||||||
|
color: var(--wp-warn);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gradebook-table .g-E {
|
||||||
|
color: var(--wp-bad);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gradebook-table .task-head {
|
||||||
|
line-height: 1.3;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gradebook-table .task-name {
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--wp-ink);
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gradebook-table .task-head.excluded .task-name {
|
||||||
|
color: var(--wp-faint);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gradebook-table .task-cov {
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gradebook-table .cov-bar {
|
||||||
|
display: block;
|
||||||
|
height: 3px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: var(--wp-line);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gradebook-table .cov-bar span {
|
||||||
|
display: block;
|
||||||
|
height: 100%;
|
||||||
|
background: var(--wp-brand);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gradebook-table .task-head.excluded .cov-bar span {
|
||||||
|
background: var(--wp-faint);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gradebook-table .cell {
|
||||||
|
font: 13px var(--wp-font-mono);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gradebook-table .cell.missing {
|
||||||
|
font: 12px var(--wp-font-sans);
|
||||||
|
color: var(--wp-bad);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gradebook-table .score {
|
||||||
|
padding: 2px 6px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--wp-ink);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gradebook-table .score:hover {
|
||||||
|
background: var(--wp-brand-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gradebook-table .score.unrated {
|
||||||
|
font: 12px var(--wp-font-sans);
|
||||||
|
color: var(--wp-faint);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gradebook-table .missing.excluded,
|
||||||
|
.gradebook-table .score.excluded {
|
||||||
|
color: var(--wp-faint);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gradebook-table .total {
|
||||||
|
font: 600 13px var(--wp-font-mono);
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gradebook-table .done {
|
||||||
|
font: 13px var(--wp-font-mono);
|
||||||
|
color: var(--wp-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gradebook-table .done.short {
|
||||||
|
color: var(--wp-bad);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
Reference in new issue
Block a user