Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1fcd12f6f4
commit
567c745432
2 files changed
+163
No files matched your search
@@ -15,11 +15,15 @@ import { useThemeVars } from "naive-ui"
|
|||||||
import SubmissionView from "oj/submission/components/SubmissionView.vue"
|
import SubmissionView from "oj/submission/components/SubmissionView.vue"
|
||||||
import UserName from "shared/components/UserName.vue"
|
import UserName from "shared/components/UserName.vue"
|
||||||
import type { ClassBoardProblem } from "utils/types"
|
import type { ClassBoardProblem } from "utils/types"
|
||||||
|
import ProblemBrief from "./ProblemBrief.vue"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 课堂看板「讲错误」(设计稿 BdOpen):点一个红格子或错法堆里的名字打开。老师在讲台上
|
* 课堂看板「讲错误」(设计稿 BdOpen):点一个红格子或错法堆里的名字打开。老师在讲台上
|
||||||
* 一个个讲这道题错着的人,所以顶上是这几个人,← → 换人;下面和独立的提交详情页同一块
|
* 一个个讲这道题错着的人,所以顶上是这几个人,← → 换人;下面和独立的提交详情页同一块
|
||||||
* (代码占满宽、为什么错在后),只是不出名字那一行、点「第几次」就地换。
|
* (代码占满宽、为什么错在后),只是不出名字那一行、点「第几次」就地换。
|
||||||
|
*
|
||||||
|
* 「题目」按钮:演示错误时题目和代码要并排(用户要的),点了左边出题目,再点收起。
|
||||||
|
* 平时收着,代码照样占满宽。开没开记在这台电脑上,讲下一个人、下一道题还是那样
|
||||||
*/
|
*/
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
problem: ClassBoardProblem | null
|
problem: ClassBoardProblem | null
|
||||||
@@ -44,6 +48,7 @@ const vars = computed(() => ({
|
|||||||
"--hover": theme.value.hoverColor,
|
"--hover": theme.value.hoverColor,
|
||||||
}))
|
}))
|
||||||
const current = computed(() => props.people[index.value] ?? null)
|
const current = computed(() => props.people[index.value] ?? null)
|
||||||
|
const showProblem = useStorage("oj_explain_show_problem", false)
|
||||||
|
|
||||||
function step(delta: -1 | 1) {
|
function step(delta: -1 | 1) {
|
||||||
const next = index.value + delta
|
const next = index.value + delta
|
||||||
@@ -87,6 +92,14 @@ onKeyStroke(["ArrowLeft", "ArrowRight"], (event) => {
|
|||||||
</div>
|
</div>
|
||||||
<span v-if="people.length > 1" class="keys"><kbd>←</kbd><kbd>→</kbd> 换人</span>
|
<span v-if="people.length > 1" class="keys"><kbd>←</kbd><kbd>→</kbd> 换人</span>
|
||||||
<div class="spacer"></div>
|
<div class="spacer"></div>
|
||||||
|
<n-button
|
||||||
|
v-if="problem"
|
||||||
|
:type="showProblem ? 'primary' : 'default'"
|
||||||
|
:secondary="showProblem"
|
||||||
|
@click="showProblem = !showProblem"
|
||||||
|
>
|
||||||
|
题目
|
||||||
|
</n-button>
|
||||||
<n-button
|
<n-button
|
||||||
v-if="current"
|
v-if="current"
|
||||||
tag="a"
|
tag="a"
|
||||||
@@ -99,8 +112,15 @@ onKeyStroke(["ArrowLeft", "ArrowRight"], (event) => {
|
|||||||
<template #icon><Icon icon="ph:x-bold" /></template>
|
<template #icon><Icon icon="ph:x-bold" /></template>
|
||||||
</n-button>
|
</n-button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="body" :class="{ split: showProblem && problem }">
|
||||||
|
<aside v-if="showProblem && problem" class="statement">
|
||||||
|
<ProblemBrief :display-id="problem.problemDisplayId" />
|
||||||
|
</aside>
|
||||||
|
<div class="main">
|
||||||
<SubmissionView v-if="current" :id="current.submissionId" embedded />
|
<SubmissionView v-if="current" :id="current.submissionId" embedded />
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</n-modal>
|
</n-modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -197,4 +217,25 @@ kbd {
|
|||||||
.spacer {
|
.spacer {
|
||||||
flex: 1 1 0;
|
flex: 1 1 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.body.split {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(300px, 2fr) minmax(0, 3fr);
|
||||||
|
gap: 20px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 题目自己滚:讲到代码后半段时题目还在旁边 */
|
||||||
|
.statement {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
max-height: calc(100vh - 140px);
|
||||||
|
overflow: auto;
|
||||||
|
padding-right: 8px;
|
||||||
|
color: var(--text1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { isSqlProblem } from "@oj2/contract"
|
||||||
|
import { getProblem } from "oj/api"
|
||||||
|
import Markdown from "shared/components/Markdown.vue"
|
||||||
|
import { errorMessage } from "utils/api"
|
||||||
|
import type { ProblemDetail } from "utils/types"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 「讲错误」弹层左边的题目(用户要的:演示学生的错误时题目和代码要并排)。
|
||||||
|
* 只摆讲题用得上的:描述、输入、输出、例子 —— 题目页的状态标签、流程图、相似题都不要。
|
||||||
|
*/
|
||||||
|
const props = defineProps<{ displayId: string }>()
|
||||||
|
|
||||||
|
const problem = ref<ProblemDetail | null>(null)
|
||||||
|
const failed = ref("")
|
||||||
|
|
||||||
|
// 同一道题 ← → 换人不重拉:弹层开着时一直是这道题
|
||||||
|
const cache = new Map<string, ProblemDetail>()
|
||||||
|
watch(
|
||||||
|
() => props.displayId,
|
||||||
|
async (id) => {
|
||||||
|
failed.value = ""
|
||||||
|
const hit = cache.get(id)
|
||||||
|
if (hit) {
|
||||||
|
problem.value = hit
|
||||||
|
return
|
||||||
|
}
|
||||||
|
problem.value = null
|
||||||
|
try {
|
||||||
|
const data = await getProblem(id, "")
|
||||||
|
cache.set(id, data)
|
||||||
|
if (props.displayId === id) problem.value = data
|
||||||
|
} catch (error) {
|
||||||
|
failed.value = errorMessage(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
const isSQL = computed(() => !!problem.value && isSqlProblem(problem.value))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="brief">
|
||||||
|
<n-text v-if="failed" type="error">{{ failed }}</n-text>
|
||||||
|
<n-spin v-else-if="!problem" size="small" />
|
||||||
|
<template v-else>
|
||||||
|
<section class="block">
|
||||||
|
<h3 class="title">描述</h3>
|
||||||
|
<Markdown html :content="problem.description" />
|
||||||
|
</section>
|
||||||
|
<template v-if="!isSQL">
|
||||||
|
<section v-if="problem.inputDescription" class="block">
|
||||||
|
<h3 class="title">输入</h3>
|
||||||
|
<Markdown html :content="problem.inputDescription" />
|
||||||
|
</section>
|
||||||
|
<section v-if="problem.outputDescription" class="block">
|
||||||
|
<h3 class="title">输出</h3>
|
||||||
|
<Markdown html :content="problem.outputDescription" />
|
||||||
|
</section>
|
||||||
|
<section v-if="problem.samples.length" class="block">
|
||||||
|
<h3 class="title">例子</h3>
|
||||||
|
<div v-for="(sample, i) in problem.samples" :key="i" class="sample">
|
||||||
|
<div class="io">
|
||||||
|
<span class="label">例子 {{ i + 1 }} 输入</span>
|
||||||
|
<pre>{{ sample.input }}</pre>
|
||||||
|
</div>
|
||||||
|
<div class="io">
|
||||||
|
<span class="label">输出</span>
|
||||||
|
<pre>{{ sample.output }}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.brief {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 14px;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin: 0 0 4px;
|
||||||
|
padding-left: 8px;
|
||||||
|
border-left: 3px solid var(--success);
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sample {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.io {
|
||||||
|
min-width: 0;
|
||||||
|
border: 1px solid var(--divider);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 4px 10px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text3);
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
margin: 0;
|
||||||
|
font-family: Consolas, Monaco, monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 22px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in new issue
Block a user