feat(协作): 老师协作时右栏顶上一条协作条,结果页签先摆学生最近一次交的,抽屉变成「他的提交」
- 协作条:「正在帮 谁 · 语言 · 看他交过什么 ↗ · 结束协作」,从工具栏里挪出来,一眼看得出 编辑器里是学生的代码。「看他交过什么」带上学生用户名、开新标签(原来只带题号,还得再筛一遍) - 老师接单后「结果」页签原来是空的:现在摆学生在这道题上最近一次交的结果,可刷新; 不给 AI 提示(那是按登录的人自己的失败次数解锁的) - 抽屉「我的提交」在协作中变成「他的提交」,列的是学生的提交,只能复制、不能放回编辑器 (放回去会经协作通道直接盖掉学生正在写的代码),也不显示老师自己的排名 - 学生排队求助中、或者老师正在帮他时换题:先确认一句。换题会断开协作 / 撤掉举手, 原来一点「下一题」就把正在帮他的老师踢出去了 - 求助按钮文案:「举手求助」→「已举手 · 前面 2 人」→「某某 老师在帮你」 - 提交列表加 exactUsername=1:用户名精确匹配。原来只有「包含」,ks24a1 会把 ks24a10 的提交 也捞进来 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
11368813cc
commit
0c2ff68e9b
13 files changed
+287
-45
No files matched your search
@@ -296,6 +296,19 @@ async function astOnlyByUser(where: SQL | undefined, userIds: number[]) {
|
||||
* 快照实测 count 65ms → 0.6ms。`ks2` 这种匹配上千个账号的宽前缀退回扫表,30~50ms,
|
||||
* 和原来持平。
|
||||
*/
|
||||
/**
|
||||
* 用户名**精确**匹配。usernameFilter 是「包含」,学号互相包含是常态(ks24a1 包含在 ks24a10 里),
|
||||
* 协作中老师看「这个学生」的提交时不能把别人的也捞进来。查无此人留恒假条件
|
||||
*/
|
||||
export async function exactUsernameFilter(username: string) {
|
||||
const [row] = await db
|
||||
.select({ id: schema.user.id })
|
||||
.from(schema.user)
|
||||
.where(eq(schema.user.username, username))
|
||||
.limit(1)
|
||||
return row ? eq(schema.submission.userId, row.id) : sql`false`
|
||||
}
|
||||
|
||||
export async function usernameFilter(username: string) {
|
||||
const like = `%${username}%`
|
||||
const users = await db
|
||||
|
||||
@@ -32,7 +32,12 @@ import { problemSetJoinTimes } from "../services/problemset"
|
||||
import { consumeToken } from "../services/throttling"
|
||||
import { dayStart } from "../time"
|
||||
import { asFilterValue, asRecord, isAdminRole, queryInteger } from "./helpers"
|
||||
import { problemFilter, submissionStatisticsRoutes, usernameFilter } from "./submission-statistics"
|
||||
import {
|
||||
exactUsernameFilter,
|
||||
problemFilter,
|
||||
submissionStatisticsRoutes,
|
||||
usernameFilter,
|
||||
} from "./submission-statistics"
|
||||
|
||||
export const submissionRoutes = new Hono<ContestEnv>()
|
||||
|
||||
@@ -489,7 +494,11 @@ submissionRoutes.get("/submissions", optionalAuth, async (c) => {
|
||||
filters.push(
|
||||
...(await Promise.all([
|
||||
displayId ? problemFilter(displayId, null) : undefined,
|
||||
username ? usernameFilter(username) : undefined,
|
||||
username
|
||||
? c.req.query("exactUsername") === "1"
|
||||
? exactUsernameFilter(username)
|
||||
: usernameFilter(username)
|
||||
: undefined,
|
||||
])),
|
||||
)
|
||||
if (myself) filters.push(eq(schema.submission.userId, myself.id))
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from "pinia"
|
||||
import { useProblemStore } from "oj/store/problem"
|
||||
import { useCollabStore } from "shared/store/collab"
|
||||
import { LANGUAGE_SHOW_VALUE } from "utils/constants"
|
||||
|
||||
/**
|
||||
* 老师协作时右栏顶上的一条:正在帮谁、他用什么语言、看他交过什么、结束协作。
|
||||
*
|
||||
* 原来这几样挤在工具栏里(一个标签 + 一个按钮),和老师自己的「提交」「课堂统计」混在一行,
|
||||
* 一眼看不出「现在编辑器里是学生的代码」。学生那边不加横幅,省下编辑器的 40px。
|
||||
*/
|
||||
const collabStore = useCollabStore()
|
||||
const { problem } = storeToRefs(useProblemStore())
|
||||
const router = useRouter()
|
||||
|
||||
/**
|
||||
* 「看他交过什么」:只看这个学生、这道题。开新标签 —— 跳走这一页协作就断了。
|
||||
* 原来工具栏的「本题提交」只带题号,老师得再手动筛一遍用户名
|
||||
*/
|
||||
function openHisSubmissions() {
|
||||
const room = collabStore.room
|
||||
if (!room || !problem.value) return
|
||||
const target = router.resolve({
|
||||
name: "submissions",
|
||||
query: { problem: problem.value._id, username: room.peerName },
|
||||
})
|
||||
window.open(target.href, "_blank")
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="collabStore.room" class="collab-bar" role="status">
|
||||
<span class="who">
|
||||
正在帮 <b>{{ collabStore.room.peerName }}</b> ·
|
||||
{{ LANGUAGE_SHOW_VALUE[collabStore.room.language] }}
|
||||
</span>
|
||||
<n-button text class="link" @click="openHisSubmissions">看他交过什么 ↗</n-button>
|
||||
<!-- 显式的「结束」:求助记录一并清掉。跳走页面发的是 leave("left"),
|
||||
那边只是退回排队 —— 见 store 里 leave 的注释 -->
|
||||
<n-button size="small" class="end" @click="collabStore.leave('done')">结束协作</n-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.collab-bar {
|
||||
height: 40px;
|
||||
flex: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 0 12px;
|
||||
border-radius: 6px;
|
||||
background-color: #2080f0;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.who {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.link {
|
||||
--n-text-color: #fff !important;
|
||||
--n-text-color-hover: rgba(255, 255, 255, 0.85) !important;
|
||||
--n-text-color-pressed: rgba(255, 255, 255, 0.7) !important;
|
||||
--n-text-color-focus: #fff !important;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.end {
|
||||
margin-left: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -108,14 +108,14 @@ const showCollabBar = computed(() => collabHere.value && userStore.isTeacherOrAb
|
||||
const helpButtonText = computed(() => {
|
||||
if (collabStore.helpStatus === "active") {
|
||||
const name = collabStore.teacherName
|
||||
return name ? `${name} 老师帮你中` : "老师正在帮你"
|
||||
return name ? `${name} 老师在帮你` : "老师正在帮你"
|
||||
}
|
||||
if (collabStore.helpStatus === "pending") {
|
||||
return collabStore.queueAhead > 0
|
||||
? `已求助 · 前面 ${collabStore.queueAhead} 人`
|
||||
: "已求助 · 待接入"
|
||||
? `已举手 · 前面 ${collabStore.queueAhead} 人`
|
||||
: "已举手 · 老师马上来"
|
||||
}
|
||||
return "求助"
|
||||
return "举手求助"
|
||||
})
|
||||
|
||||
const helpButtonType = computed(() => {
|
||||
@@ -301,18 +301,6 @@ const goEdit = () => {
|
||||
<n-button :size="buttonSize">更多</n-button>
|
||||
</n-dropdown>
|
||||
|
||||
<template v-if="showCollabBar">
|
||||
<n-tag type="success" :size="buttonSize">
|
||||
正在帮 {{ collabStore.room!.peerName }} ·
|
||||
{{ LANGUAGE_SHOW_VALUE[collabStore.room!.language] }}
|
||||
</n-tag>
|
||||
<!-- 显式的「结束」:求助记录一并清掉。跳走页面发的是 leave("left"),
|
||||
那边只是退回排队 —— 见 store 里 leave 的注释 -->
|
||||
<n-button :size="buttonSize" type="primary" @click="collabStore.leave('done')">
|
||||
结束协作
|
||||
</n-button>
|
||||
</template>
|
||||
|
||||
<n-button
|
||||
v-if="showHelpButton"
|
||||
:size="buttonSize"
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from "pinia"
|
||||
import { getSubmission, getSubmissions } from "oj/api"
|
||||
import { useProblemStore } from "oj/store/problem"
|
||||
import { useCollabStore } from "shared/store/collab"
|
||||
import { parseTime } from "utils/functions"
|
||||
import type { Submission } from "utils/types"
|
||||
|
||||
/**
|
||||
* 老师接单之后落在题目页,「结果」页签原来是空的 —— 学生多半是交了几次没过才举的手,
|
||||
* 老师最先想知道的就是「他交上去是什么结果」。这里摆出学生最近一次交的(设计文档第 9 节)。
|
||||
* 管理员角色本来就能看任何人的提交,不需要新接口。
|
||||
*/
|
||||
const SubmissionResult = defineAsyncComponent(() => import("./SubmissionResult.vue"))
|
||||
|
||||
const collabStore = useCollabStore()
|
||||
const { problem } = storeToRefs(useProblemStore())
|
||||
|
||||
const loading = ref(false)
|
||||
const failed = ref(false)
|
||||
const latest = ref<Submission | null>(null)
|
||||
const checked = ref(false)
|
||||
|
||||
const peer = computed(() => collabStore.room?.peerName ?? "")
|
||||
|
||||
async function load() {
|
||||
if (!peer.value || !problem.value) return
|
||||
loading.value = true
|
||||
failed.value = false
|
||||
try {
|
||||
const list = await getSubmissions({
|
||||
username: peer.value,
|
||||
exactUsername: "1",
|
||||
problemDisplayId: problem.value._id,
|
||||
limit: 1,
|
||||
offset: 0,
|
||||
})
|
||||
const row = list.results[0]
|
||||
latest.value = row ? await getSubmission(row.id) : null
|
||||
checked.value = true
|
||||
} catch {
|
||||
failed.value = true
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch([peer, () => problem.value?._id], load, { immediate: true })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="peer-latest">
|
||||
<n-flex align="center" justify="space-between" class="head">
|
||||
<span>
|
||||
<b>{{ peer }}</b> 最近一次交的
|
||||
<n-text v-if="latest" depth="3">· {{ parseTime(latest.createTime, "HH:mm:ss") }}</n-text>
|
||||
</span>
|
||||
<n-button size="small" :loading="loading" @click="load">刷新</n-button>
|
||||
</n-flex>
|
||||
|
||||
<n-text v-if="failed" type="error">读不出他的提交,点「刷新」再试</n-text>
|
||||
<SubmissionResult v-else-if="latest" :submission="latest" peer />
|
||||
<n-empty v-else-if="checked" class="empty" description="他还没交过这道题" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.head {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.empty {
|
||||
margin-top: 32px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,13 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from "pinia"
|
||||
import { useProblemStore } from "oj/store/problem"
|
||||
import { useCollabStore } from "shared/store/collab"
|
||||
import { useUserStore } from "shared/store/user"
|
||||
import {
|
||||
DRAWER_TITLE,
|
||||
useProblemPageContext,
|
||||
type ProblemDrawer,
|
||||
} from "../composables/problemPageContext"
|
||||
import { useTeacherCollab } from "../composables/teacherCollab"
|
||||
|
||||
/**
|
||||
* 统计 / 点评 / 我的提交。原来是左栏的三个页签,课上几乎没人点,却和「题目」挤在一排;
|
||||
@@ -30,7 +30,6 @@ const ctx = useProblemPageContext()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const collabStore = useCollabStore()
|
||||
const { problem } = storeToRefs(useProblemStore())
|
||||
|
||||
const show = computed({
|
||||
@@ -55,6 +54,14 @@ watch(
|
||||
},
|
||||
)
|
||||
|
||||
// 协作中的老师看的是学生的提交(ProblemSubmission 里按学生用户名查)
|
||||
const teacherCollab = useTeacherCollab()
|
||||
const drawerTitle = computed(() =>
|
||||
lastOpened.value === "submission" && teacherCollab.value
|
||||
? "他的提交"
|
||||
: DRAWER_TITLE[lastOpened.value],
|
||||
)
|
||||
|
||||
/**
|
||||
* 「看这道题所有人的提交」,原来是工具栏上的「本题提交」。可见条件照旧:
|
||||
* 比赛里总给(看的是这场比赛的提交),题库里要管理员或者开着提交列表。
|
||||
@@ -72,11 +79,7 @@ function goAllSubmissions() {
|
||||
// 协作中走新标签:教师端「页面即协作现场」,跳走这一页协作就结束了
|
||||
// (求助会退回排队,但老师还得再接一次)。而「看看这学生都交了什么」恰好是
|
||||
// 协作时最常点的一个
|
||||
const collabHere =
|
||||
collabStore.room !== null &&
|
||||
collabStore.room.problemId === problem.value?._id &&
|
||||
userStore.isTeacherOrAbove
|
||||
if (collabHere) {
|
||||
if (teacherCollab.value) {
|
||||
window.open(router.resolve(target).href, "_blank")
|
||||
return
|
||||
}
|
||||
@@ -94,7 +97,7 @@ function goAllSubmissions() {
|
||||
:trap-focus="false"
|
||||
:block-scroll="!props.to"
|
||||
>
|
||||
<n-drawer-content :title="DRAWER_TITLE[lastOpened]" closable :native-scrollbar="false">
|
||||
<n-drawer-content :title="drawerTitle" closable :native-scrollbar="false">
|
||||
<ProblemInfo v-if="lastOpened === 'info'" />
|
||||
<ProblemReaction v-else-if="lastOpened === 'reaction'" />
|
||||
<template v-else>
|
||||
|
||||
@@ -11,6 +11,7 @@ import type { LANGUAGE } from "utils/types"
|
||||
import { beginEditTrace, editTraceExtensions } from "oj/problem/utils/editTrace"
|
||||
import { errorMarkExtensions } from "oj/problem/utils/errorMark"
|
||||
import EditorToolbar from "./EditorToolbar.vue"
|
||||
import CollabBar from "./CollabBar.vue"
|
||||
import { useFlowchartStore } from "oj/store/flowchart"
|
||||
|
||||
const FlowchartEditor = defineAsyncComponent(
|
||||
@@ -97,9 +98,11 @@ const storageKey = computed(() =>
|
||||
: `problem_${problem.value!._id}_contest_${contestID}_lang_${codeStore.code.language}`,
|
||||
)
|
||||
|
||||
const editorHeight = computed(() =>
|
||||
isDesktop.value ? "calc(100vh - 133px)" : "calc(100vh - 172px)",
|
||||
)
|
||||
// 协作条 40px + 和工具栏之间的 12px 间距,要从编辑器里让出来,不然页面底下多出一截滚动条
|
||||
const editorHeight = computed(() => {
|
||||
const base = isDesktop.value ? 133 : 172
|
||||
return `calc(100vh - ${base + (teacherCollab.value ? 52 : 0)}px)`
|
||||
})
|
||||
|
||||
function loadCode() {
|
||||
codeStore.code.language = problemStore.supportedLanguage(codeStore.code.language)
|
||||
@@ -166,6 +169,7 @@ provide("flowchartEditorRef", flowchartEditorRef)
|
||||
|
||||
<template>
|
||||
<n-flex vertical>
|
||||
<CollabBar v-if="teacherCollab" />
|
||||
<EditorToolbar :storage-key="storageKey" @change-language="changeLanguage" />
|
||||
<!--
|
||||
协作中教师这边不会落到流程图分支:上面那个 watch 已经把他的语言换成了学生的,
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import { storeToRefs } from "pinia"
|
||||
import { getRankOfProblem, getSubmission, getSubmissions } from "oj/api"
|
||||
import { useCodeStore } from "oj/store/code"
|
||||
import { useProblemStore } from "oj/store/problem"
|
||||
import Pagination from "shared/components/Pagination.vue"
|
||||
import SubmissionResultTag from "shared/components/SubmissionResultTag.vue"
|
||||
import { useCollabStore } from "shared/store/collab"
|
||||
@@ -12,6 +10,7 @@ import { copyToClipboard, parseTime } from "utils/functions"
|
||||
import type { ProblemRank } from "@oj2/contract"
|
||||
import type { LANGUAGE, SubmissionListItem } from "utils/types"
|
||||
import { useProblemPageContext } from "../composables/problemPageContext"
|
||||
import { useTeacherCollab } from "../composables/teacherCollab"
|
||||
|
||||
/**
|
||||
* 「我的提交」抽屉:这道题自己交过的每一次。
|
||||
@@ -24,7 +23,6 @@ import { useProblemPageContext } from "../composables/problemPageContext"
|
||||
const userStore = useUserStore()
|
||||
const codeStore = useCodeStore()
|
||||
const collabStore = useCollabStore()
|
||||
const { problem } = storeToRefs(useProblemStore())
|
||||
const ctx = useProblemPageContext()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -36,12 +34,10 @@ const problemDisplayId = computed(() => String(route.params.problemID ?? ""))
|
||||
* 协作中的教师:编辑器里是学生的代码,「放回编辑器」会经 Yjs 直接盖掉学生正在写的
|
||||
* 那一份,所以不给(设计文档第 9 节)。
|
||||
*/
|
||||
const teacherCollab = computed(
|
||||
() =>
|
||||
userStore.isTeacherOrAbove &&
|
||||
collabStore.room !== null &&
|
||||
collabStore.room.problemId === problem.value?._id,
|
||||
)
|
||||
const teacherCollab = useTeacherCollab()
|
||||
|
||||
/** 协作中看的是学生的提交(设计文档第 9 节):房间里的 peerName 就是学生的用户名 */
|
||||
const peer = computed(() => (teacherCollab.value ? (collabStore.room?.peerName ?? null) : null))
|
||||
|
||||
// ==================== 班上第几个做对的 ====================
|
||||
// 只在题库入口给:比赛有自己的排名,题单里整个抽屉都不出现
|
||||
@@ -98,7 +94,9 @@ const blocked = computed(() => {
|
||||
async function listSubmissions() {
|
||||
const res = await getSubmissions({
|
||||
...query,
|
||||
myself: "1",
|
||||
...(peer.value
|
||||
? { username: peer.value, exactUsername: "1" as const }
|
||||
: { myself: "1" as const }),
|
||||
offset: query.limit * (query.page - 1),
|
||||
problemDisplayId: problemDisplayId.value,
|
||||
contestId: ctx.value.contestId,
|
||||
@@ -165,7 +163,8 @@ function openDetail(id: string) {
|
||||
onMounted(() => {
|
||||
if (blocked.value) return
|
||||
listSubmissions()
|
||||
if (ctx.value.entry === "problem") {
|
||||
// 排名是按当前登录的人算的,协作中那是老师自己的,不给
|
||||
if (ctx.value.entry === "problem" && !peer.value) {
|
||||
getRankOfProblem(problemDisplayId.value)
|
||||
.then((res) => {
|
||||
rank.value = res
|
||||
@@ -190,7 +189,11 @@ watch(query, () => {
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<n-empty v-if="listed && !submissions.length" class="empty" description="这道题你还没有交过" />
|
||||
<n-empty
|
||||
v-if="listed && !submissions.length"
|
||||
class="empty"
|
||||
:description="peer ? '他还没有交过这道题' : '这道题你还没有交过'"
|
||||
/>
|
||||
|
||||
<ul v-else class="list">
|
||||
<li v-for="(row, index) in submissions" :key="row.id" class="item">
|
||||
|
||||
@@ -3,6 +3,7 @@ import { storeToRefs } from "pinia"
|
||||
import { useProblemStore } from "oj/store/problem"
|
||||
import { useSubmissionStore } from "oj/store/submission"
|
||||
import { useCodeStore } from "oj/store/code"
|
||||
import { useTeacherCollab } from "../composables/teacherCollab"
|
||||
|
||||
/**
|
||||
* 左栏「结果」页签:三段 —— 提交结果 / 运行例子 / 自己输入。
|
||||
@@ -19,12 +20,16 @@ const PythonErrorExplain = defineAsyncComponent(() => import("./PythonErrorExpla
|
||||
const SampleRunResult = defineAsyncComponent(() => import("./SampleRunResult.vue"))
|
||||
const CustomRun = defineAsyncComponent(() => import("./CustomRun.vue"))
|
||||
const FlowchartResult = defineAsyncComponent(() => import("./FlowchartResult.vue"))
|
||||
const PeerLatest = defineAsyncComponent(() => import("./PeerLatest.vue"))
|
||||
|
||||
const submissionStore = useSubmissionStore()
|
||||
const { submission, syntaxErrorInfo, resultSegment } = storeToRefs(submissionStore)
|
||||
const { problem } = storeToRefs(useProblemStore())
|
||||
const codeStore = useCodeStore()
|
||||
|
||||
/** 协作中的老师自己还没交过:先摆学生最近一次交的 */
|
||||
const teacherCollab = useTeacherCollab()
|
||||
|
||||
/** 正在画流程图:结果就是 AI 的点评,没有例子可跑 */
|
||||
const drawing = computed(() => codeStore.code.language === "Flowchart")
|
||||
|
||||
@@ -51,6 +56,7 @@ const canTrial = computed(() => {
|
||||
<PythonErrorExplain :err-info="syntaxErrorInfo" />
|
||||
</n-flex>
|
||||
<SubmissionResult v-else-if="submission" :submission="submission" />
|
||||
<PeerLatest v-else-if="teacherCollab" />
|
||||
<n-empty
|
||||
v-else
|
||||
class="empty"
|
||||
|
||||
@@ -26,6 +26,11 @@ import { useDark } from "@vueuse/core"
|
||||
|
||||
const props = defineProps<{
|
||||
submission?: Submission
|
||||
/**
|
||||
* 看的是别人的提交(协作中的老师看学生最近一次交的):不给 AI 提示 —— 那是按登录的人
|
||||
* 自己的失败次数解锁的 —— 也不给「这节课的下一题」
|
||||
*/
|
||||
peer?: boolean
|
||||
}>()
|
||||
|
||||
const isDark = useDark()
|
||||
@@ -89,7 +94,8 @@ const sampleCheck = computed(() => {
|
||||
*/
|
||||
const ctx = useProblemPageContext()
|
||||
const showLessonNext = computed(
|
||||
() => props.submission?.result === SubmissionStatus.accepted && ctx.value.lessonNext,
|
||||
() =>
|
||||
!props.peer && props.submission?.result === SubmissionStatus.accepted && ctx.value.lessonNext,
|
||||
)
|
||||
|
||||
const msg = computed(() => {
|
||||
@@ -130,7 +136,7 @@ const partialCases = computed(() => submissionPartialCases(props.submission))
|
||||
// system_error 也要排掉:那是判题机自己崩了,学生代码没毛病,让 AI 去分析
|
||||
// 只会瞎编一通,后端的失败计数同样不认这个状态。
|
||||
const showAIHint = computed(() => {
|
||||
if (!props.submission) return false
|
||||
if (!props.submission || props.peer) return false
|
||||
// 比赛题不给提示,和「求助」按钮一致。用 problem.contestId 而不是路由参数:
|
||||
// 带 contestId 的题目只可能从比赛入口进来(题库列表按 contest_id is null 过滤)。
|
||||
if (problemStore.problem?.contestId != null) return false
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { useProblemStore } from "oj/store/problem"
|
||||
import { useCollabStore } from "shared/store/collab"
|
||||
import { useUserStore } from "shared/store/user"
|
||||
|
||||
/**
|
||||
* 老师正在这道题上协作:编辑器里是**学生的**代码,「我的提交」看的是学生的,
|
||||
* 跳走这一页协作就断了(所以跳转都开新标签,设计文档第 9 节)。
|
||||
*/
|
||||
export function useTeacherCollab() {
|
||||
const userStore = useUserStore()
|
||||
const collabStore = useCollabStore()
|
||||
const problemStore = useProblemStore()
|
||||
return computed(
|
||||
() =>
|
||||
userStore.isTeacherOrAbove &&
|
||||
collabStore.room !== null &&
|
||||
collabStore.room.problemId === problemStore.problem?._id,
|
||||
)
|
||||
}
|
||||
@@ -8,6 +8,8 @@ import { useMyFlowchartStore } from "shared/store/myFlowchart"
|
||||
import { useUserStore } from "shared/store/user"
|
||||
import { useSubmissionStore } from "oj/store/submission"
|
||||
import { useFlowchartStore } from "oj/store/flowchart"
|
||||
import { useCollabStore } from "shared/store/collab"
|
||||
import type { RouteLocationNormalized } from "vue-router"
|
||||
// 判完之后该发生的事(烟花、点评、回题单)。静态引入:它得在第一次判完之前就挂好
|
||||
import SubmissionEffects from "./components/SubmissionEffects.vue"
|
||||
import ResultTabLabel from "./components/ResultTabLabel.vue"
|
||||
@@ -17,6 +19,7 @@ import {
|
||||
useProblemPageContext,
|
||||
type ProblemDrawer as DrawerKey,
|
||||
} from "./composables/problemPageContext"
|
||||
import { useTeacherCollab } from "./composables/teacherCollab"
|
||||
|
||||
// 抽成具名 loader,便于进页面时与接口并行预取编辑器 chunk。
|
||||
// 题库、比赛、题单三种入口用的是同一个编辑器:原来比赛和题单用的是另一个精简版,
|
||||
@@ -88,12 +91,17 @@ watch(
|
||||
|
||||
const drawer = ref<DrawerKey | null>(null)
|
||||
|
||||
// 协作中的老师看的是学生的提交
|
||||
const teacherCollab = useTeacherCollab()
|
||||
const drawerLabel = (key: DrawerKey) =>
|
||||
key === "submission" && teacherCollab.value ? "他的提交" : DRAWER_TITLE[key]
|
||||
|
||||
function toggleDrawer(key: DrawerKey) {
|
||||
drawer.value = drawer.value === key ? null : key
|
||||
}
|
||||
|
||||
const drawerMenu = computed<DropdownOption[]>(() =>
|
||||
ctx.value.drawers.map((key) => ({ label: DRAWER_TITLE[key], key })),
|
||||
ctx.value.drawers.map((key) => ({ label: drawerLabel(key), key })),
|
||||
)
|
||||
|
||||
// 交上去、或者语法检查没过:切到「结果」,抽屉开着就先关掉。提交按钮在编辑器那边,页签归这一页管
|
||||
@@ -106,6 +114,35 @@ watch(
|
||||
},
|
||||
)
|
||||
|
||||
/**
|
||||
* 学生正在排队求助、或者老师正在帮他时换题:协作是开在这道题上的,换题就断了
|
||||
* (SyncCodeEditor 的 detach),排队的求助也跟着撤掉。先问一句,别让他一点「下一题」
|
||||
* 就把正在帮他的老师踢出去。只拦「离开这道题」,切页签改 `?tab=` 不算
|
||||
*/
|
||||
const collabStore = useCollabStore()
|
||||
const dialog = useDialog()
|
||||
function confirmLeavingHelp(to: RouteLocationNormalized, from: RouteLocationNormalized) {
|
||||
if (collabStore.isTeacher || collabStore.helpStatus === "idle") return true
|
||||
if (to.name === from.name && to.params.problemID === from.params.problemID) return true
|
||||
const active = collabStore.helpStatus === "active"
|
||||
return new Promise<boolean>((resolve) => {
|
||||
dialog.warning({
|
||||
title: active ? "老师正在帮你看这道题" : "你正在排队等老师",
|
||||
content: active
|
||||
? "换题之后,老师那边的协作就断了。确定要换吗?"
|
||||
: "换题之后这次举手就撤掉了,要在新的题目里重新举手。确定要换吗?",
|
||||
positiveText: "换题",
|
||||
negativeText: "留在这道题",
|
||||
onPositiveClick: () => resolve(true),
|
||||
onNegativeClick: () => resolve(false),
|
||||
onClose: () => resolve(false),
|
||||
onMaskClick: () => resolve(false),
|
||||
})
|
||||
})
|
||||
}
|
||||
onBeforeRouteUpdate(confirmLeavingHelp)
|
||||
onBeforeRouteLeave(confirmLeavingHelp)
|
||||
|
||||
// 结果页签的标题带状态图标(ResultTabLabel),n-tab-pane 的 tab 接受渲染函数
|
||||
const resultTab = () => h(ResultTabLabel)
|
||||
|
||||
@@ -187,7 +224,7 @@ onBeforeUnmount(() => {
|
||||
:secondary="drawer === key"
|
||||
@click="toggleDrawer(key)"
|
||||
>
|
||||
{{ DRAWER_TITLE[key] }}
|
||||
{{ drawerLabel(key) }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</div>
|
||||
|
||||
@@ -266,6 +266,8 @@ export interface SubmissionListPayload {
|
||||
myself?: "1" | "0"
|
||||
result?: string
|
||||
username?: string
|
||||
/** "1" = username 精确匹配(默认是「包含」)。协作中老师看某个学生的提交用 */
|
||||
exactUsername?: "1"
|
||||
contestId?: string
|
||||
problemDisplayId?: string
|
||||
language: LANGUAGE | ""
|
||||
|
||||
Reference in new issue
Block a user