refactor(课堂任务): 「课堂看板」改名「课堂任务」,地址换成 /task;班级 PK 换成 /pk

- 界面上的「课堂看板」「课堂作业」统一叫「课堂任务」,顶栏、老师首页、后台菜单一起改
- 前端 oj/classroom → oj/task、oj/class → oj/pk,两个名字不再只差几个字母却毫不相干;旧地址不留跳转
- 后端 routes/classroom.ts 拆开:课堂任务的接口进 routes/task.ts(/task/board|lesson|today),
  首页的「上次来」「班里在做」进 routes/home.ts;共用的取题口径挪进 services/lesson.ts
- 契约 classroom.ts 同样拆成 task.ts / home.ts,ClassBoard* 改叫 TaskBoard*

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-11-02 22:53:07 +08:00
1 parent 46d3fe2dbe
commit 29c31640b5
82 files changed
+798 -766

No files matched your search

@@ -0,0 +1,113 @@
<script setup lang="ts">
import type { TaskBoardStudent } from 'utils/types'
/**
* 任务页的「请假」框:勾今天不在的人。规则在任务页那边(只有今天还没交过的人能算请假、
* 交了题或举手就自动算到,见 board.vue 的 canLeave),这里只管勾。
*/
const props = defineProps<{
/** 能勾的人:今天还没交过的(刚上课时就是全班),按名字排好 */
candidates: TaskBoardStudent[]
/** 打开时已经标着请假的 */
current: string[]
}>()
const emit = defineEmits<{ save: [usernames: string[]] }>()
const show = defineModel<boolean>('show', { required: true })
const selected = ref<string[]>([])
watch(show, (open) => {
if (open) selected.value = [...props.current]
})
/**
* 今天一次都没上过线。机房有两人共用一台电脑的,同桌只有一个登着,所以「谁没来」
* 看今天上没上过线,不看此刻在不在线(见契约 seenAt)
*/
const unseen = (student: TaskBoardStudent | undefined) => !!student && !student.seenAt
/** 今天没上过线的分在前面一堆:请假的人基本在这里面,剩下的是和同桌共用电脑的 */
const sections = computed(() =>
[
{ label: '今天没上线', students: props.candidates.filter(unseen) },
{
label: '今天上过线',
students: props.candidates.filter((student) => !unseen(student)),
},
].filter((section) => section.students.length),
)
function selectAll(list: TaskBoardStudent[]) {
selected.value = [...new Set([...selected.value, ...list.map((student) => student.username)])]
}
function save() {
emit('save', selected.value)
show.value = false
}
</script>
<template>
<n-modal
v-model:show="show"
preset="card"
:title="candidates.length ? `今天还没交过的 ${candidates.length} 人里,谁请假了?` : '今天全班都交过题了'"
style="width: 640px; max-width: calc(100vw - 32px)"
>
<n-checkbox-group v-if="candidates.length" v-model:value="selected">
<div v-for="section in sections" :key="section.label" class="leave-section">
<div v-if="sections.length > 1 || unseen(section.students[0])" class="leave-head">
{{ section.label }} {{ section.students.length }} 人
<n-button
v-if="unseen(section.students[0])"
text
type="primary"
:disabled="section.students.every((student) => selected.includes(student.username))"
@click="selectAll(section.students)"
>
全勾上
</n-button>
</div>
<div class="leave-grid">
<n-checkbox v-for="student in section.students" :key="student.userId" :value="student.username" :label="student.username" class="leave-option" />
</div>
</div>
</n-checkbox-group>
<n-text v-else depth="3">请假的人一定是没交过题的,现在没有人可以标。</n-text>
<template #footer>
<n-flex align="center" :wrap="false">
<n-text depth="3" class="meta" style="flex-grow: 1"> 已选 {{ selected.length }} 人 · 只记在这台电脑上,今天有效 </n-text>
<n-button :disabled="!selected.length" @click="selected = []">全部取消</n-button>
<n-button @click="show = false">取消</n-button>
<n-button type="primary" @click="save">确定</n-button>
</n-flex>
</template>
</n-modal>
</template>
<style scoped>
.leave-section + .leave-section {
margin-top: 18px;
}
.leave-head {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 10px;
font-weight: 600;
}
.leave-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 10px 8px;
}
.leave-option {
font-size: 15px;
}
.meta {
font-size: var(--oj-fs-sec);
}
</style>