Files
OJ2/apps/web/src/shared/components/HelpRequestList.vue
T
xuyueandClaude Opus 5.5 29c31640b5 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>
2026-11-02 22:53:07 +08:00

210 lines
5.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { errorKind } from 'utils/errorKind'
import { useBreakpoints } from 'shared/composables/breakpoints'
import { useTeacherHelp } from 'shared/composables/teacherHelp'
import { useCollabStore } from 'shared/store/collab'
import type { CollabRequestItem } from 'shared/composables/websocket'
import { SubmissionStatus } from 'utils/constants'
import UserName from './UserName.vue'
/**
* 顶栏「N 人举手」点开的面板(设计稿「举手与协作」HcPanel),挂在按钮下面,不是弹框。
*
* 按题分组,每人写错法(和课堂任务同一套 errorKind)和等了多久。同一道题两人以上举手,
* 用户的做法是**停下来全班讲**:组头给「讲这道题」(去任务页、选中这道题看错法)和
* 「讲完了,都放下」(这道题还在排队的手一次放下)。
*/
const emit = defineEmits<{ close: [] }>()
const collabStore = useCollabStore()
const router = useRouter()
const { help } = useTeacherHelp()
// 接单之后要在题目页的编辑器里替学生写代码,那个编辑器窄屏上没法用 —— 窄屏只让看
const { isDesktop } = useBreakpoints()
// 等待时长每秒走一格。面板只在打开时挂着,关着不转
const now = useNow({ scheduler: (cb) => useIntervalFn(cb, 1000) })
function waited(createdAt: number) {
const seconds = Math.max(0, Math.floor((now.value.getTime() - createdAt) / 1000))
return `${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, '0')}`
}
/** 等了三分钟以上标红 */
function waitedLong(createdAt: number) {
return now.value.getTime() - createdAt >= 180_000
}
function kindOf(item: CollabRequestItem) {
if (!item.latest) return '还没交过'
if (item.latest.result === SubmissionStatus.accepted) return '已经做对了'
return errorKind(item.latest).label
}
function pendingOf(items: CollabRequestItem[]) {
return items.filter((item) => item.status === 'pending')
}
function onHelp(item: CollabRequestItem) {
if (help(item.studentId)) emit('close')
}
/** 「讲这道题」:去这个班的任务页、选中这道题,下面展开错着的人按错法归堆,点人就是讲错误弹层 */
function explain(items: CollabRequestItem[], problemId: string) {
const className = items.find((item) => item.className)?.className
emit('close')
router.push({
name: 'task',
query: { ...(className ? { className } : {}), problem: problemId },
})
}
</script>
<template>
<div class="panel">
<div class="title">
<b>举手 {{ collabStore.requests.length }} 人</b>
</div>
<n-alert v-if="!isDesktop" type="info" :bordered="false">接单要在电脑上打开</n-alert>
<n-empty v-if="collabStore.groupedRequests.length === 0" description="没有人举手" />
<div v-for="group in collabStore.groupedRequests" :key="group.problemId" class="group" :class="{ many: pendingOf(group.items).length >= 2 }">
<div class="group-head">
<span class="group-title">
<b class="num">{{ group.problemId }}</b>
<span class="muted">{{ group.problemTitle }}</span>
</span>
<b class="group-count">{{ group.items.length }} 人</b>
<template v-if="pendingOf(group.items).length >= 2">
<n-button size="small" @click="explain(group.items, group.problemId)">讲这道题</n-button>
<n-button size="small" @click="collabStore.dismiss(group.problemId)"> 讲完了,都放下 </n-button>
</template>
</div>
<div v-for="item in group.items" :key="item.studentId" class="row">
<div class="who">
<UserName :username="item.studentName" />
<span class="kind">{{ kindOf(item) }}</span>
</div>
<template v-if="item.status === 'active'">
<n-text type="info" strong class="helping">{{ item.teacherName }} 在帮</n-text>
</template>
<template v-else>
<span class="waited" :class="{ long: waitedLong(item.createdAt) }"> 等了 {{ waited(item.createdAt) }} </span>
<n-button v-if="isDesktop" type="primary" size="small" @click="onHelp(item)"> 去帮 </n-button>
<n-button quaternary circle size="small" aria-label="放下举手" title="放下举手" @click="collabStore.reject(item.studentId)">
<Icon icon="mdi:close" height="16" />
</n-button>
</template>
</div>
</div>
</div>
</template>
<style scoped>
.panel {
width: 440px;
max-width: calc(100vw - 32px);
max-height: 70vh;
overflow: auto;
display: flex;
flex-direction: column;
gap: 10px;
}
.title {
font-size: 15px;
}
.group {
border: 1px solid var(--n-divider-color, #e6e7eb);
border-radius: 8px;
overflow: hidden;
}
.group.many {
border-color: #f3d6dd;
}
.group-head {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: rgba(128, 128, 128, 0.08);
white-space: nowrap;
}
.group.many .group-head {
background: rgba(208, 48, 80, 0.07);
}
.group-title {
flex: 1 1 auto;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
font-size: 14px;
}
.group-title .muted {
margin-left: 6px;
}
.group-count {
font-size: 14px;
}
.group.many .group-count {
color: #c0304e;
}
.row {
display: flex;
align-items: center;
gap: 10px;
min-height: 48px;
padding: 0 8px 0 12px;
border-top: 1px solid rgba(128, 128, 128, 0.12);
}
.who {
flex: 1 1 auto;
min-width: 0;
display: flex;
flex-direction: column;
font-size: 15px;
}
.kind {
font-size: 12px;
line-height: 16px;
opacity: 0.65;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.waited {
font-size: 13px;
font-variant-numeric: tabular-nums;
opacity: 0.75;
white-space: nowrap;
}
.waited.long {
color: #c0304e;
opacity: 1;
}
.helping {
font-size: 13px;
white-space: nowrap;
}
.muted {
opacity: 0.6;
}
.num {
font-variant-numeric: tabular-nums;
}
</style>