做完这节课(或题库里自己做对)之后列出还能挑战的题;去挑战后编辑器顶上写着要求, 没做到标出是哪几行,做到了写本班第几个、全站第几个。课堂看板做到的格子贴一枚紫色筹码、 在挑战的加一圈紫边,题卡片写挑战成功几人。出题页多一块「挑战」,能从做对的代码里找候选。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0faf78ad8f
commit
1a173f75d7
19 files changed
+1243
-24
No files matched your search
@@ -0,0 +1,127 @@
|
||||
<script setup lang="ts">
|
||||
import { useChallengeStore } from "oj/store/challenge"
|
||||
import { useChallengeTone } from "../composables/challengeTone"
|
||||
import ChallengeIcon from "./ChallengeIcon.vue"
|
||||
|
||||
/**
|
||||
* 挑战中:编辑器顶上一条紫色的,写着要求和全站几人做到,随时「不挑战了」(设计稿 StkDoing)。
|
||||
* 做到了换成「挑战成功」,留着让学生看一眼自己是照什么要求写的,再「回到普通模式」
|
||||
*/
|
||||
const challengeStore = useChallengeStore()
|
||||
const tone = useChallengeTone()
|
||||
const active = computed(() => challengeStore.active)
|
||||
const done = computed(() => !!active.value?.mine)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="active" class="challenge-bar">
|
||||
<span class="pill">
|
||||
<svg
|
||||
v-if="done"
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="3"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M5 12.5l4.5 4.5L19 7.5" />
|
||||
</svg>
|
||||
<ChallengeIcon v-else />
|
||||
{{ done ? "挑战成功" : "挑战" }}
|
||||
</span>
|
||||
<b class="wording">{{ active.wording }}</b>
|
||||
<span class="spacer" />
|
||||
<span v-if="!done" class="meta">{{ active.doneCount }} 人做到</span>
|
||||
<button type="button" class="quit" @click="challengeStore.stop()">
|
||||
{{ done ? "回到普通模式" : "不挑战了" }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.challenge-bar {
|
||||
height: 44px;
|
||||
flex-shrink: 0;
|
||||
box-sizing: border-box;
|
||||
padding: 0 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
background-color: v-bind("tone.soft");
|
||||
border-bottom: 1px solid v-bind("tone.border");
|
||||
white-space: nowrap;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
height: 24px;
|
||||
padding: 0 9px;
|
||||
border-radius: 12px;
|
||||
background-color: v-bind("tone.solid");
|
||||
color: v-bind("tone.onSolid");
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.wording {
|
||||
font-size: 15px;
|
||||
color: v-bind("tone.text");
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.meta {
|
||||
font-size: 13px;
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.quit {
|
||||
height: 28px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid v-bind("tone.border");
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
font: inherit;
|
||||
font-size: 13px;
|
||||
color: v-bind("tone.text");
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 手机上一行放不下:说法最要紧,让它折行,人数不写了 */
|
||||
@media (max-width: 768px) {
|
||||
.challenge-bar {
|
||||
height: auto;
|
||||
min-height: 44px;
|
||||
padding: 6px 12px;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.wording {
|
||||
flex: 1 1 0;
|
||||
}
|
||||
|
||||
.spacer,
|
||||
.meta {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.quit:focus-visible {
|
||||
outline: 2px solid v-bind("tone.solid");
|
||||
outline-offset: 2px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
/** 挑战的标记:一枚筹码(借的是小丑牌的赌注筹码),设计稿「做对后再挑战」 */
|
||||
withDefaults(defineProps<{ size?: number }>(), { size: 14 })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg
|
||||
:width="size"
|
||||
:height="size"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2.2"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<circle cx="12" cy="12" r="9" />
|
||||
<circle cx="12" cy="12" r="4" />
|
||||
<path d="M12 3v3M12 18v3M3 12h3M18 12h3" stroke-linecap="round" />
|
||||
</svg>
|
||||
</template>
|
||||
@@ -0,0 +1,201 @@
|
||||
<script setup lang="ts">
|
||||
import type { ChallengeList } from "@oj2/contract"
|
||||
import { getChallenges } from "oj/api"
|
||||
import { useChallengeStore } from "oj/store/challenge"
|
||||
import { useChallengeTone } from "../composables/challengeTone"
|
||||
import ChallengeIcon from "./ChallengeIcon.vue"
|
||||
|
||||
/**
|
||||
* 「还能挑战」:这几道题里我做对了、还没做到的挑战,一条一行,「去挑战」(设计稿 StkDone)。
|
||||
* 一条都没有就什么都不显示 —— 课堂题多是一两行,能出挑战的只是一部分。
|
||||
*/
|
||||
const props = defineProps<{
|
||||
problemIds: string[]
|
||||
language: string
|
||||
/** 变了就重拉:刚做对 / 刚做到一条挑战,列表里要去掉它 */
|
||||
refreshKey?: string
|
||||
}>()
|
||||
|
||||
const challengeStore = useChallengeStore()
|
||||
const tone = useChallengeTone()
|
||||
|
||||
const groups = ref<ChallengeList>([])
|
||||
let sequence = 0
|
||||
watch(
|
||||
() => [props.problemIds.join(","), props.language, props.refreshKey] as const,
|
||||
async () => {
|
||||
const mine = ++sequence
|
||||
if (!props.problemIds.length || !props.language) {
|
||||
groups.value = []
|
||||
return
|
||||
}
|
||||
try {
|
||||
const list = await getChallenges(props.problemIds, props.language)
|
||||
if (mine === sequence) groups.value = list
|
||||
} catch {
|
||||
if (mine === sequence) groups.value = []
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
const rows = computed(() =>
|
||||
groups.value.flatMap((group) =>
|
||||
// 没做对的题不给接挑战:挑战是做对之后的事
|
||||
group.solved
|
||||
? group.challenges
|
||||
.filter((challenge) => !challenge.mine)
|
||||
.map((challenge) => ({ group, challenge }))
|
||||
: [],
|
||||
),
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section v-if="rows.length" class="challenge-list" aria-label="还能挑战">
|
||||
<div class="head">
|
||||
<span class="pill"><ChallengeIcon />挑战</span>
|
||||
<b>还能挑战</b>
|
||||
</div>
|
||||
<div v-for="({ group, challenge }, i) in rows" :key="challenge.id" class="row">
|
||||
<div class="text">
|
||||
<div class="problem">
|
||||
<b>{{ group.problemId }}</b> <span class="title">{{ group.title }}</span>
|
||||
</div>
|
||||
<b class="wording">{{ challenge.wording }}</b>
|
||||
<span class="stat">
|
||||
{{ group.acCount }} 个做对的人里,{{ challenge.doneCount }} 人做到
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="go"
|
||||
:class="{ ghost: i > 0 }"
|
||||
@click="challengeStore.start(group.problemId, challenge.id)"
|
||||
>
|
||||
去挑战
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M5 12h14M13 6l6 6-6 6" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.challenge-list {
|
||||
margin-top: 14px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
height: 24px;
|
||||
padding: 0 9px;
|
||||
border-radius: 12px;
|
||||
background-color: v-bind("tone.solid");
|
||||
color: v-bind("tone.onSolid");
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 4px 16px;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid v-bind("tone.border");
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.problem {
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.title,
|
||||
.stat {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.stat {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.wording {
|
||||
font-size: 17px;
|
||||
line-height: 24px;
|
||||
color: v-bind("tone.text");
|
||||
}
|
||||
|
||||
.go {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
height: 40px;
|
||||
padding: 0 16px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid v-bind("tone.solid");
|
||||
background-color: v-bind("tone.solid");
|
||||
color: v-bind("tone.onSolid");
|
||||
font: inherit;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.go.ghost {
|
||||
background-color: transparent;
|
||||
color: v-bind("tone.text");
|
||||
border-color: v-bind("tone.border");
|
||||
}
|
||||
|
||||
.go:focus-visible {
|
||||
outline: 2px solid v-bind("tone.solid");
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* 手机上按钮另起一行占满,好按 */
|
||||
@media (max-width: 768px) {
|
||||
.row {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.go {
|
||||
justify-content: center;
|
||||
height: 44px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,194 @@
|
||||
<script setup lang="ts">
|
||||
import type { AstResultInfo } from "@oj2/contract"
|
||||
import { useChallengeStore } from "oj/store/challenge"
|
||||
import { clearChallengeLines, showChallengeLines } from "oj/problem/utils/errorMark"
|
||||
import { useChallengeTone } from "../composables/challengeTone"
|
||||
import ChallengeIcon from "./ChallengeIcon.vue"
|
||||
|
||||
/**
|
||||
* 挑战中交的这一次,挑战成没成(设计稿 StkFail / StkPass)。答案本身是对的 —— 没做到挑战
|
||||
* 也照样算做对,所以没做到时大字写「答案对了,挑战还差一点」,不是错误的红色。
|
||||
*/
|
||||
const props = defineProps<{
|
||||
passed: boolean
|
||||
results: AstResultInfo[]
|
||||
wording: string
|
||||
}>()
|
||||
|
||||
const challengeStore = useChallengeStore()
|
||||
const tone = useChallengeTone()
|
||||
|
||||
const failed = computed(() => props.results.filter((result) => !result.passed))
|
||||
|
||||
/** 「print() 至多调用 1 次,现在 3 次」 */
|
||||
const reason = computed(() =>
|
||||
failed.value
|
||||
.map((result) =>
|
||||
result.actual !== undefined
|
||||
? `${result.description},现在 ${result.actual} 次`
|
||||
: result.description,
|
||||
)
|
||||
.join(";"),
|
||||
)
|
||||
|
||||
const lines = computed(() => [...new Set(failed.value.flatMap((result) => result.lines ?? []))])
|
||||
|
||||
// 编辑器里把那几行标紫。做到了、换了一次提交就清掉
|
||||
watch(
|
||||
() => (props.passed ? [] : lines.value),
|
||||
(value) => (value.length ? showChallengeLines(value) : clearChallengeLines()),
|
||||
{ immediate: true },
|
||||
)
|
||||
onUnmounted(clearChallengeLines)
|
||||
|
||||
/** 做到了:本班、全站第几个。刚判完那一刻列表还没重拉,先不显示 */
|
||||
const mine = computed(() => (props.passed ? (challengeStore.active?.mine ?? null) : null))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="challenge-outcome">
|
||||
<div class="line">
|
||||
<span class="badge" :class="{ outline: !passed }" aria-hidden="true">
|
||||
<ChallengeIcon :size="18" />
|
||||
</span>
|
||||
<div class="text">
|
||||
<b class="title">{{ passed ? "挑战成功" : "答案对了,挑战还差一点" }}</b>
|
||||
<span class="sub">{{ passed ? wording : reason }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!passed" class="chips">
|
||||
<span class="chip ok">
|
||||
<svg
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="3"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M5 12.5l4.5 4.5L19 7.5" />
|
||||
</svg>
|
||||
测试点全对
|
||||
</span>
|
||||
<span v-if="lines.length" class="chip mark">第 {{ lines.join("、") }} 行</span>
|
||||
</div>
|
||||
<div v-if="mine" class="tiles">
|
||||
<div v-if="mine.classRank !== null" class="tile">
|
||||
<b>第 {{ mine.classRank }} 个</b>
|
||||
<span>本班做到的</span>
|
||||
</div>
|
||||
<div class="tile">
|
||||
<b>第 {{ mine.siteRank }} 个</b>
|
||||
<span>全站做到的</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.challenge-outcome {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.badge {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
box-sizing: border-box;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: v-bind("tone.solid");
|
||||
color: v-bind("tone.onSolid");
|
||||
}
|
||||
|
||||
.badge.outline {
|
||||
background-color: transparent;
|
||||
border: 2px solid v-bind("tone.solid");
|
||||
color: v-bind("tone.solid");
|
||||
}
|
||||
|
||||
.text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 22px;
|
||||
line-height: 28px;
|
||||
color: v-bind("tone.text");
|
||||
}
|
||||
|
||||
.sub {
|
||||
font-size: 14px;
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
height: 26px;
|
||||
padding: 0 9px;
|
||||
border-radius: 13px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.chip.ok {
|
||||
background-color: rgba(24, 160, 88, 0.12);
|
||||
color: #18a058;
|
||||
}
|
||||
|
||||
.chip.mark {
|
||||
background-color: v-bind("tone.soft");
|
||||
color: v-bind("tone.text");
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tiles {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.tile {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 10px 14px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid v-bind("tone.border");
|
||||
background-color: v-bind("tone.soft");
|
||||
}
|
||||
|
||||
.tile b {
|
||||
font-size: 24px;
|
||||
line-height: 30px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: v-bind("tone.text");
|
||||
}
|
||||
|
||||
.tile span {
|
||||
font-size: 13px;
|
||||
opacity: 0.75;
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { useThemeVars } from "naive-ui"
|
||||
import { useLessonStore } from "shared/store/lesson"
|
||||
import ChallengeList from "./ChallengeList.vue"
|
||||
|
||||
/**
|
||||
* 通过之后告诉学生「这节课的下一题是哪道」。
|
||||
@@ -16,6 +17,12 @@ import { useLessonStore } from "shared/store/lesson"
|
||||
const props = defineProps<{
|
||||
/** 当前这道题的展示题号 */
|
||||
problemDisplayId: string
|
||||
/** 刚做对用的语言:挑战按语言分 */
|
||||
language: string
|
||||
/** 刚做对的那次提交,换了就重拉「还能挑战」 */
|
||||
refreshKey: string
|
||||
/** 这个入口给不给挑战(见 problemPageContext) */
|
||||
challenges: boolean
|
||||
}>()
|
||||
|
||||
const lessonStore = useLessonStore()
|
||||
@@ -31,6 +38,11 @@ const lesson = computed(() => lessonStore.nextAfter(props.problemDisplayId))
|
||||
|
||||
// 设计稿和上下文条都叫「这节课」,不区分是老师布置的还是从同班提交推断的
|
||||
const label = "这节课"
|
||||
|
||||
/** 这节课的几道题:都做完了以后,这几道里还能挑战的列出来(设计稿 StkDone) */
|
||||
const lessonProblemIds = computed(
|
||||
() => lessonStore.activity?.problems.map((item) => item.problemDisplayId) ?? [],
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -66,13 +78,18 @@ const label = "这节课"
|
||||
<template v-else>
|
||||
<div class="text">
|
||||
<span class="headline">{{ label }}都做完了,一共 {{ lesson.total }} 道</span>
|
||||
<span class="detail">想接着练,可以去题单里挑一个</span>
|
||||
<span v-if="!challenges" class="detail">想接着练,可以去题单里挑一个</span>
|
||||
</div>
|
||||
<router-link to="/problemset" class="next secondary">
|
||||
<router-link v-if="!challenges" to="/problemset" class="next secondary">
|
||||
<span class="next-label">去题单继续练</span>
|
||||
</router-link>
|
||||
</template>
|
||||
</div>
|
||||
<!-- 做完了:先给这节课里还能挑战的,再给题单(设计稿 StkDone)。挑战一条都没有时只剩题单那句 -->
|
||||
<template v-if="lesson && !lesson.next && challenges">
|
||||
<ChallengeList :problem-ids="lessonProblemIds" :language="language" :refresh-key="refreshKey" />
|
||||
<router-link to="/problemset" class="more">去题单继续练</router-link>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@@ -129,6 +146,17 @@ const label = "这节课"
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.more {
|
||||
display: inline-block;
|
||||
margin-top: 12px;
|
||||
font-size: 14px;
|
||||
color: v-bind("theme.primaryColor");
|
||||
}
|
||||
|
||||
.more:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.next.secondary {
|
||||
background-color: transparent;
|
||||
border: 1px solid v-bind("theme.primaryColor");
|
||||
|
||||
@@ -13,6 +13,8 @@ import { errorMarkExtensions } from "oj/problem/utils/errorMark"
|
||||
import { editorFocusTracker } from "oj/problem/utils/symbolKeys"
|
||||
import { hangWrapExtensions } from "shared/extensions/hangIndent"
|
||||
import EditorToolbar from "./EditorToolbar.vue"
|
||||
import ChallengeBar from "./ChallengeBar.vue"
|
||||
import { useChallengeStore } from "oj/store/challenge"
|
||||
import { useDraftKey } from "../composables/draftKey"
|
||||
import { useCollabHere, useTeacherCollab } from "../composables/teacherCollab"
|
||||
import { useProblemPageContext } from "../composables/problemPageContext"
|
||||
@@ -99,9 +101,14 @@ const storageKey = useDraftKey()
|
||||
|
||||
// 桌面:顶栏 56 + 工具栏 48,协作条再占 40(两栏顶满内容区,见 detail.vue 的 problem-split)。
|
||||
// 手机:顶栏两行 + 课堂条 + 页签 + 工具栏约 236px,底部还有「运行例子 / 提交」那条约 64px
|
||||
// 挑战中编辑器顶上多一条 44 的挑战条(协作中的老师不显示,见模板)
|
||||
const challengeStore = useChallengeStore()
|
||||
const showChallengeBar = computed(
|
||||
() => ctx.value.challenges && !teacherCollab.value && !!challengeStore.active,
|
||||
)
|
||||
const editorHeight = computed(() => {
|
||||
const base = isDesktop.value ? 104 : 300
|
||||
return `calc(100vh - ${base + (teacherCollab.value ? 40 : 0)}px)`
|
||||
return `calc(100vh - ${base + (teacherCollab.value ? 40 : 0) + (showChallengeBar.value ? 44 : 0)}px)`
|
||||
})
|
||||
|
||||
function loadCode() {
|
||||
@@ -220,6 +227,7 @@ onBeforeUnmount(() => flowchartStore.attachEditor(null))
|
||||
<template>
|
||||
<div class="problem-editor">
|
||||
<CollabBar v-if="teacherCollab" />
|
||||
<ChallengeBar v-if="showChallengeBar" />
|
||||
<EditorToolbar @change-language="changeLanguage" />
|
||||
<!--
|
||||
协作中教师这边不会落到流程图分支:上面那个 watch 已经把他的语言换成了学生的,
|
||||
|
||||
@@ -21,6 +21,9 @@ import WrongAnswerExplain from "./WrongAnswerExplain.vue"
|
||||
import LessonNext from "./LessonNext.vue"
|
||||
import ResultHeader from "./ResultHeader.vue"
|
||||
import SimilarProblems from "./SimilarProblems.vue"
|
||||
import ChallengeList from "./ChallengeList.vue"
|
||||
import ChallengeOutcome from "./ChallengeOutcome.vue"
|
||||
import { useChallengeStore } from "oj/store/challenge"
|
||||
import { useLessonStore } from "shared/store/lesson"
|
||||
import { useProblemPageContext } from "../composables/problemPageContext"
|
||||
import Markdown from "shared/components/Markdown.vue"
|
||||
@@ -109,6 +112,40 @@ const showSimilar = computed(
|
||||
!lessonStore.includes(problemStore.problem._id),
|
||||
)
|
||||
|
||||
/**
|
||||
* 挑战中交的这一次:正在挑战的那条判出来的结果(设计稿 StkFail / StkPass)。只有做对的提交
|
||||
* 才判挑战;进挑战之前那次做对的不算(见 challenge store 的 baselineSubmissionId)
|
||||
*/
|
||||
const challengeStore = useChallengeStore()
|
||||
const challengeVerdict = computed(() => {
|
||||
const submission = props.submission
|
||||
if (!submission || props.peer || !ctx.value.challenges) return null
|
||||
if (submission.result !== SubmissionStatus.accepted) return null
|
||||
const id = challengeStore.activeId
|
||||
if (!id || submission.id === challengeStore.baselineSubmissionId) return null
|
||||
return submission.statisticInfo?.challenges?.find((verdict) => verdict.id === id) ?? null
|
||||
})
|
||||
// 做到了:重拉一次,挑战条换成「挑战成功」、拿到本班 / 全站第几个
|
||||
watch(
|
||||
() => (challengeVerdict.value?.passed ? props.submission?.id : null),
|
||||
(id) => {
|
||||
if (id) void challengeStore.load()
|
||||
},
|
||||
)
|
||||
|
||||
/**
|
||||
* 「还能挑战」看哪几道:这道在这节课里就看这节课的几道(课堂条「这节课都做完了」之后),
|
||||
* 不在就只看这一道
|
||||
*/
|
||||
const challengeProblemIds = computed(() => {
|
||||
const problem = problemStore.problem
|
||||
if (!problem) return []
|
||||
const lesson = lessonStore.activity
|
||||
return lessonStore.includes(problem._id) && lesson
|
||||
? lesson.problems.map((item) => item.problemDisplayId)
|
||||
: [problem._id]
|
||||
})
|
||||
|
||||
const msg = computed(() => {
|
||||
if (!props.submission) return ""
|
||||
// 走 CompileErrorExplain / RuntimeErrorExplain 那两张中文卡片
|
||||
@@ -298,18 +335,44 @@ const columns: DataTableColumn<JudgeCaseResult>[] = [
|
||||
|
||||
<template>
|
||||
<div v-if="submission">
|
||||
<ResultHeader
|
||||
v-if="header"
|
||||
:kind="header.kind"
|
||||
:title="header.title"
|
||||
:sub="header.sub"
|
||||
:progress="header.progress"
|
||||
/>
|
||||
<LessonNext
|
||||
v-if="showLessonNext && problemStore.problem"
|
||||
:problem-display-id="problemStore.problem._id"
|
||||
/>
|
||||
<SimilarProblems v-if="showSimilar" title="再练几道相似的" />
|
||||
<!-- 挑战中:答案对了就只说挑战成没成,「下一题」「相似题」都不给,免得挤掉挑战 -->
|
||||
<template v-if="challengeVerdict && challengeStore.active">
|
||||
<ChallengeOutcome
|
||||
:passed="challengeVerdict.passed"
|
||||
:results="challengeVerdict.results"
|
||||
:wording="challengeStore.active.wording"
|
||||
/>
|
||||
<ChallengeList
|
||||
v-if="challengeVerdict.passed"
|
||||
:problem-ids="challengeProblemIds"
|
||||
:language="submission.language"
|
||||
:refresh-key="submission.id"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<ResultHeader
|
||||
v-if="header"
|
||||
:kind="header.kind"
|
||||
:title="header.title"
|
||||
:sub="header.sub"
|
||||
:progress="header.progress"
|
||||
/>
|
||||
<LessonNext
|
||||
v-if="showLessonNext && problemStore.problem"
|
||||
:problem-display-id="problemStore.problem._id"
|
||||
:language="submission.language"
|
||||
:refresh-key="submission.id"
|
||||
:challenges="ctx.challenges"
|
||||
/>
|
||||
<!-- 不在这节课里(题库入口自己做的):做对之后先给这道题的挑战,再给相似题 -->
|
||||
<ChallengeList
|
||||
v-if="showSimilar && ctx.challenges && problemStore.problem"
|
||||
:problem-ids="[problemStore.problem._id]"
|
||||
:language="submission.language"
|
||||
:refresh-key="submission.id"
|
||||
/>
|
||||
<SimilarProblems v-if="showSimilar" title="再练几道相似的" />
|
||||
</template>
|
||||
<n-flex
|
||||
vertical
|
||||
v-if="
|
||||
|
||||
Reference in new issue
Block a user