题目页换题是同一个组件复用、只换路由参数,几处只在创建时读一次的状态就停在了上一道题: - 例子:新题下面摆着旧题的例子,「测试」也拿旧例子比对 - 语言:下拉选项不跟着变,也不回退——从 Python 题跳到 SQL 题,语言停在 Python,提交必被拒 - 比赛 / 题单的编辑器只在挂载时载入一次代码 - 流程图:分数按钮、评分弹框、「我的流程图」还是上一道题的,上一道题还没评完的那次会挂到新题上 语言回退挪进 problem store 的 supportedLanguage(),两个编辑器在载入草稿之前调用, 保证先定语言再读这门语言的草稿。没有用给整块加 key 强制重新挂载的办法:课堂协作的 绑定依赖组件复用,重挂载会让协作中切题的老师语言恢复不了。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
71670e609b
commit
1b149a4f3f
7 files changed
+92
-23
No files matched your search
@@ -27,7 +27,8 @@ const editorHeight = computed(() =>
|
|||||||
isDesktop.value ? "calc(100vh - 133px)" : "calc(100vh - 172px)",
|
isDesktop.value ? "calc(100vh - 133px)" : "calc(100vh - 172px)",
|
||||||
)
|
)
|
||||||
|
|
||||||
onMounted(() => {
|
function loadCode() {
|
||||||
|
codeStore.code.language = problemStore.supportedLanguage(codeStore.code.language)
|
||||||
const savedCode = storage.get(storageKey.value)
|
const savedCode = storage.get(storageKey.value)
|
||||||
codeStore.setCode(
|
codeStore.setCode(
|
||||||
savedCode ||
|
savedCode ||
|
||||||
@@ -35,7 +36,12 @@ onMounted(() => {
|
|||||||
SOURCES[codeStore.code.language],
|
SOURCES[codeStore.code.language],
|
||||||
)
|
)
|
||||||
beginEditTrace(`problem_${problem.value!._id}_contest_${contestID}`, codeStore.code.value.length)
|
beginEditTrace(`problem_${problem.value!._id}_contest_${contestID}`, codeStore.code.value.length)
|
||||||
})
|
}
|
||||||
|
|
||||||
|
onMounted(loadCode)
|
||||||
|
|
||||||
|
// 换题时组件复用、不重新挂载,和 ProblemEditor 一样得自己重新载入
|
||||||
|
watch(() => problem.value?._id, loadCode)
|
||||||
|
|
||||||
const changeCode = (v: string) => {
|
const changeCode = (v: string) => {
|
||||||
storage.set(storageKey.value, v)
|
storage.set(storageKey.value, v)
|
||||||
|
|||||||
@@ -185,7 +185,9 @@ const handleMenuSelect = (key: string) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const languageOptions: DropdownOption[] = languages.value.map((it) => ({
|
// computed:换题时组件复用,选项得跟着新题的语言走
|
||||||
|
const languageOptions = computed<DropdownOption[]>(() =>
|
||||||
|
languages.value.map((it) => ({
|
||||||
label: () =>
|
label: () =>
|
||||||
h(NFlex, { align: "center" }, () => [
|
h(NFlex, { align: "center" }, () => [
|
||||||
h(Icon, {
|
h(Icon, {
|
||||||
@@ -195,7 +197,8 @@ const languageOptions: DropdownOption[] = languages.value.map((it) => ({
|
|||||||
LANGUAGE_SHOW_VALUE[it],
|
LANGUAGE_SHOW_VALUE[it],
|
||||||
]),
|
]),
|
||||||
value: it,
|
value: it,
|
||||||
}))
|
})),
|
||||||
|
)
|
||||||
|
|
||||||
const copy = async () => {
|
const copy = async () => {
|
||||||
const success = await copyToClipboard(codeStore.code.value)
|
const success = await copyToClipboard(codeStore.code.value)
|
||||||
@@ -247,12 +250,7 @@ const goEdit = () => {
|
|||||||
window.open(router.resolve(url).href, "_blank")
|
window.open(router.resolve(url).href, "_blank")
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
// 语言回退不在这里做:它得排在载入草稿之前,见 problem store 的 supportedLanguage
|
||||||
if (!languages.value.includes(codeStore.code.language)) {
|
|
||||||
// 回退到题目支持的第一种语言(如 SQL 题只有 "SQL",硬编码 Python 会被后端拒绝)
|
|
||||||
codeStore.code.language = languages.value[0] ?? "Python"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -88,14 +88,25 @@ const hasTriedButNotPassed = computed(() => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const samples = ref<Sample[]>(
|
function freshSamples(): Sample[] {
|
||||||
problem.value!.samples.map((sample, index) => ({
|
return (problem.value?.samples ?? []).map((sample, index) => ({
|
||||||
...sample,
|
...sample,
|
||||||
id: index,
|
id: index,
|
||||||
msg: "",
|
msg: "",
|
||||||
status: "not_test",
|
status: "not_test",
|
||||||
loading: false,
|
loading: false,
|
||||||
})),
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const samples = ref<Sample[]>(freshSamples())
|
||||||
|
|
||||||
|
// 题目页换题是同一个组件复用(顶栏题号直达、「下一题」、相似题都是只换路由参数),
|
||||||
|
// 不跟着重建的话,新题下面摆的还是上一道题的例子,「测试」也拿旧例子去比
|
||||||
|
watch(
|
||||||
|
() => problem.value?._id,
|
||||||
|
() => {
|
||||||
|
samples.value = freshSamples()
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
// 文案和配色分类都由后端生成 —— 原来这里有一份 NODE_TARGET_LABELS +
|
// 文案和配色分类都由后端生成 —— 原来这里有一份 NODE_TARGET_LABELS +
|
||||||
@@ -116,7 +127,10 @@ async function test(sample: Sample, index: number) {
|
|||||||
}
|
}
|
||||||
return sample
|
return sample
|
||||||
})
|
})
|
||||||
|
const problemId = problem.value?._id
|
||||||
const res = await createTestSubmission(codeStore.code, sample.input)
|
const res = await createTestSubmission(codeStore.code, sample.input)
|
||||||
|
// 跑的这一会儿换了题:结果是上一道题的例子的,按下标写进去就串到新题上了
|
||||||
|
if (problem.value?._id !== problemId) return
|
||||||
samples.value = samples.value.map((sample) => {
|
samples.value = samples.value.map((sample) => {
|
||||||
if (sample.id === index) {
|
if (sample.id === index) {
|
||||||
const status = res.status === 3 && res.output.trim() === sample.output ? "passed" : "failed"
|
const status = res.status === 3 && res.output.trim() === sample.output ? "passed" : "failed"
|
||||||
@@ -133,6 +147,7 @@ async function test(sample: Sample, index: number) {
|
|||||||
|
|
||||||
const id = setTimeout(() => {
|
const id = setTimeout(() => {
|
||||||
clearTimeout(id)
|
clearTimeout(id)
|
||||||
|
if (problem.value?._id !== problemId) return
|
||||||
samples.value = samples.value.map((sample) => {
|
samples.value = samples.value.map((sample) => {
|
||||||
if (sample.id === index) {
|
if (sample.id === index) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ const editorHeight = computed(() =>
|
|||||||
)
|
)
|
||||||
|
|
||||||
function loadCode() {
|
function loadCode() {
|
||||||
|
codeStore.code.language = problemStore.supportedLanguage(codeStore.code.language)
|
||||||
const savedCode = storage.get(storageKey.value)
|
const savedCode = storage.get(storageKey.value)
|
||||||
codeStore.setCode(
|
codeStore.setCode(
|
||||||
savedCode ||
|
savedCode ||
|
||||||
|
|||||||
@@ -153,16 +153,21 @@ const { start: startPollingDeadline, stop: stopPollingDeadline } = useTimeoutFn(
|
|||||||
{ immediate: false },
|
{ immediate: false },
|
||||||
)
|
)
|
||||||
|
|
||||||
function settle(submissionId: string, outcome: Outcome) {
|
/** 不再跟这一次评分:结算完、或者换了题 */
|
||||||
// 一个学生可能同时开着几道题的页面,每条连接都订在同一个用户 topic 上,
|
function stopMonitoring() {
|
||||||
// 别的页面的评分结果照样会推到这里来 —— 必须认 id,不然会张冠李戴
|
|
||||||
if (!submissionId || submissionId !== monitoringId.value) return
|
|
||||||
monitoringId.value = ""
|
monitoringId.value = ""
|
||||||
unsubscribe()
|
unsubscribe()
|
||||||
pausePolling()
|
pausePolling()
|
||||||
stopPollingFallback()
|
stopPollingFallback()
|
||||||
stopPollingDeadline()
|
stopPollingDeadline()
|
||||||
loading.value = false
|
loading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function settle(submissionId: string, outcome: Outcome) {
|
||||||
|
// 一个学生可能同时开着几道题的页面,每条连接都订在同一个用户 topic 上,
|
||||||
|
// 别的页面的评分结果照样会推到这里来 —— 必须认 id,不然会张冠李戴
|
||||||
|
if (!submissionId || submissionId !== monitoringId.value) return
|
||||||
|
stopMonitoring()
|
||||||
|
|
||||||
if (!outcome.ok) {
|
if (!outcome.ok) {
|
||||||
message.error(outcome.error ? `流程图评分失败: ${outcome.error}` : "流程图评分失败,请稍后重试")
|
message.error(outcome.error ? `流程图评分失败: ${outcome.error}` : "流程图评分失败,请稍后重试")
|
||||||
@@ -261,8 +266,11 @@ function submit() {
|
|||||||
// ==================== 数据获取和处理函数 ====================
|
// ==================== 数据获取和处理函数 ====================
|
||||||
|
|
||||||
async function getCurrentSubmission() {
|
async function getCurrentSubmission() {
|
||||||
if (!problem.value?.id) return
|
const problemId = problem.value?.id
|
||||||
const data = await getCurrentProblemFlowchartSubmission(problem.value.id)
|
if (!problemId) return
|
||||||
|
const data = await getCurrentProblemFlowchartSubmission(problemId)
|
||||||
|
// 换题之后才回来的是上一道题的分数,别盖掉新题的
|
||||||
|
if (problem.value?.id !== problemId) return
|
||||||
submissionCount.value = data.count
|
submissionCount.value = data.count
|
||||||
latestRating.value = {
|
latestRating.value = {
|
||||||
score: data.score,
|
score: data.score,
|
||||||
@@ -271,8 +279,10 @@ async function getCurrentSubmission() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function getSubmission(submissionPage = 0) {
|
async function getSubmission(submissionPage = 0) {
|
||||||
if (!problem.value?.id) return
|
const problemId = problem.value?.id
|
||||||
const data = await getFlowchartSubmissionDetail(problem.value.id, submissionPage)
|
if (!problemId) return
|
||||||
|
const data = await getFlowchartSubmissionDetail(problemId, submissionPage)
|
||||||
|
if (problem.value?.id !== problemId) return
|
||||||
submissionCount.value = data.count
|
submissionCount.value = data.count
|
||||||
const submission = data.submission
|
const submission = data.submission
|
||||||
// 翻到没有提交的页时后端返回 null(契约里 submission 是 nullable)——
|
// 翻到没有提交的页时后端返回 null(契约里 submission 是 nullable)——
|
||||||
@@ -380,19 +390,46 @@ const getPercentType = (percent: number) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 生命周期钩子 ====================
|
// ==================== 生命周期钩子 ====================
|
||||||
onMounted(async () => {
|
/** 这道题最近一次的评分;画到了 A/S 的话,把那张图亮到「我的流程图」 */
|
||||||
connect()
|
async function loadLatest() {
|
||||||
|
const problemId = problem.value?.id
|
||||||
await getCurrentSubmission()
|
await getCurrentSubmission()
|
||||||
page.value = submissionCount.value
|
page.value = submissionCount.value
|
||||||
const grade = latestRating.value.grade
|
const grade = latestRating.value.grade
|
||||||
if ((grade === "A" || grade === "S") && submissionCount.value > 0) {
|
if ((grade === "A" || grade === "S") && submissionCount.value > 0) {
|
||||||
await getSubmission(submissionCount.value)
|
await getSubmission(submissionCount.value)
|
||||||
|
// 查的这一会儿又换了题,这张图不是新题的
|
||||||
|
if (problem.value?.id !== problemId) return
|
||||||
if (myMermaidCode.value) {
|
if (myMermaidCode.value) {
|
||||||
myFlowchartStore.show(myMermaidCode.value)
|
myFlowchartStore.show(myMermaidCode.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
connect()
|
||||||
|
loadLatest()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 换题。题目页换题是同一个组件复用(只换路由参数),两道题都能画流程图、语言又停在
|
||||||
|
* Flowchart 的时候,这个组件连卸载都不会:分数按钮和评分弹框还是上一道题的,
|
||||||
|
* 上一道还在评的那次评完,还会把它的图挂到新题的「我的流程图」上。
|
||||||
|
*/
|
||||||
|
watch(
|
||||||
|
() => problem.value?.id,
|
||||||
|
(next, previous) => {
|
||||||
|
if (!previous || next === previous) return
|
||||||
|
stopMonitoring()
|
||||||
|
showDetailModal.value = false
|
||||||
|
latestRating.value = { score: 0, grade: "" }
|
||||||
|
submissionCount.value = 0
|
||||||
|
page.value = 1
|
||||||
|
lastSubmittedMermaidCode.value = ""
|
||||||
|
loadLatest()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
// 组件卸载时断开连接
|
// 组件卸载时断开连接
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
disconnect()
|
disconnect()
|
||||||
|
|||||||
@@ -99,6 +99,8 @@ watch(
|
|||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
screenModeStore.resetScreenMode()
|
screenModeStore.resetScreenMode()
|
||||||
|
// 「我的流程图」是上一道题的。这道题也画到了 A/S 的话,SubmitFlowchart 查完会再亮出来
|
||||||
|
myFlowchartStore.hide()
|
||||||
// 并行预取右侧编辑器 chunk(CodeMirror ~370K+),
|
// 并行预取右侧编辑器 chunk(CodeMirror ~370K+),
|
||||||
// 避免等 getProblem 返回后才串行下载,编辑器才迟迟出现
|
// 避免等 getProblem 返回后才串行下载,编辑器才迟迟出现
|
||||||
;(isPublicProblemRoute.value ? loadProblemEditor : loadContestEditor)()
|
;(isPublicProblemRoute.value ? loadProblemEditor : loadContestEditor)()
|
||||||
|
|||||||
@@ -32,6 +32,15 @@ export const useProblemStore = defineStore("problem", () => {
|
|||||||
return problem.value?.languages ?? []
|
return problem.value?.languages ?? []
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 这道题收不收当前语言,不收就退到它支持的第一种(SQL 题只有 "SQL",硬编码的
|
||||||
|
* Python 会被后端拒绝)。编辑器每次载入代码之前都要过一遍:草稿的键里带着语言,
|
||||||
|
* 先载入再改语言,编辑器里摆的就是另一种语言的模板。
|
||||||
|
*/
|
||||||
|
function supportedLanguage(current: LANGUAGE): LANGUAGE {
|
||||||
|
return languages.value.includes(current) ? current : (languages.value[0] ?? "Python")
|
||||||
|
}
|
||||||
|
|
||||||
function incrementFailCount() {
|
function incrementFailCount() {
|
||||||
sessionFailCount.value++
|
sessionFailCount.value++
|
||||||
}
|
}
|
||||||
@@ -47,6 +56,7 @@ export const useProblemStore = defineStore("problem", () => {
|
|||||||
problem,
|
problem,
|
||||||
failCount,
|
failCount,
|
||||||
languages,
|
languages,
|
||||||
|
supportedLanguage,
|
||||||
incrementFailCount,
|
incrementFailCount,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
Reference in new issue
Block a user