update
Deploy / deploy (push) Canceled after 0s

This commit is contained in:
xuyue committed 2026-09-27 19:36:28 +08:00
1 parent 23844ae301
commit d25b5dbe6d
268 files changed
+3684 -8861

No files matched your search

@@ -20,8 +20,7 @@ const { isDesktop } = useBreakpoints()
const contestID = route.params.contestID || null
const storageKey = computed(
() =>
`problem_${problem.value!._id}_contest_${contestID}_lang_${codeStore.code.language}`,
() => `problem_${problem.value!._id}_contest_${contestID}_lang_${codeStore.code.language}`,
)
const editorHeight = computed(() =>
@@ -35,10 +34,7 @@ onMounted(() => {
problem.value!.template[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)
})
const changeCode = (v: string) => {
@@ -50,18 +46,14 @@ const changeLanguage = (v: LANGUAGE) => {
codeStore.setCode(
savedCode && storageKey.value.split("_").pop() === v
? savedCode
: problem.value!.template[codeStore.code.language] ||
SOURCES[codeStore.code.language],
: problem.value!.template[codeStore.code.language] || SOURCES[codeStore.code.language],
)
}
</script>
<template>
<n-flex vertical>
<EditorToolbar
:storage-key="storageKey"
@change-language="changeLanguage"
/>
<EditorToolbar :storage-key="storageKey" @change-language="changeLanguage" />
<CodeEditor
v-model:value="codeStore.code.value"
:language="codeStore.code.language"
@@ -20,8 +20,7 @@ const { input, output } = storeToRefs(codeStore)
const { problem } = storeToRefs(problemStore)
const storageKey = computed(
() =>
`problem_${problem.value!._id}_contest_${contestID}_lang_${codeStore.code.language}`,
() => `problem_${problem.value!._id}_contest_${contestID}_lang_${codeStore.code.language}`,
)
onMounted(() => {
@@ -29,8 +28,7 @@ onMounted(() => {
codeStore.setCode(storage.get(storageKey.value))
} else {
codeStore.setCode(
problem.value!.template[codeStore.code.language] ||
SOURCES[codeStore.code.language],
problem.value!.template[codeStore.code.language] || SOURCES[codeStore.code.language],
)
}
})
@@ -40,15 +38,11 @@ function changeCode(v: string) {
}
function changeLanguage(v: string) {
if (
storage.get(storageKey.value) &&
storageKey.value.split("_").pop() === v
) {
if (storage.get(storageKey.value) && storageKey.value.split("_").pop() === v) {
codeStore.setCode(storage.get(storageKey.value))
} else {
codeStore.setCode(
problem.value!.template[codeStore.code.language] ||
SOURCES[codeStore.code.language],
problem.value!.template[codeStore.code.language] || SOURCES[codeStore.code.language],
)
}
}
@@ -60,8 +54,7 @@ const copy = async () => {
const reset = () => {
codeStore.setCode(
problem.value!.template[codeStore.code.language] ||
SOURCES[codeStore.code.language],
problem.value!.template[codeStore.code.language] || SOURCES[codeStore.code.language],
)
storage.remove(storageKey.value)
message.success("代码重置成功")
@@ -72,12 +65,10 @@ const runCode = async () => {
output.value = res.output
}
const languageOptions: DropdownOption[] = problem.value!.languages.map(
(it) => ({
label: () => LANGUAGE_SHOW_VALUE[it],
value: it,
}),
)
const languageOptions: DropdownOption[] = problem.value!.languages.map((it) => ({
label: () => LANGUAGE_SHOW_VALUE[it],
value: it,
}))
</script>
<template>
@@ -94,9 +85,7 @@ const languageOptions: DropdownOption[] = problem.value!.languages.map(
/>
<n-button @click="copy">复制代码</n-button>
<n-button @click="reset">重置代码</n-button>
<n-button type="primary" secondary @click="runCode">
运行代码
</n-button>
<n-button type="primary" secondary @click="runCode"> 运行代码 </n-button>
</n-flex>
<CodeEditor
v-model:value="codeStore.code.value"
@@ -106,12 +95,7 @@ const languageOptions: DropdownOption[] = problem.value!.languages.map(
</n-flex>
</template>
<template #2>
<n-split
direction="vertical"
:default-size="1 / 3"
:min="1 / 5"
:max="3 / 5"
>
<n-split direction="vertical" :default-size="1 / 3" :min="1 / 5" :max="3 / 5">
<template #1>
<div class="title">输入框</div>
<n-input
@@ -19,14 +19,10 @@ import { Icon } from "@iconify/vue"
import { NFlex } from "naive-ui"
import SubmitCode from "./SubmitCode.vue"
const SubmitFlowchart = defineAsyncComponent(
() => import("./SubmitFlowchart.vue"),
)
const SubmitFlowchart = defineAsyncComponent(() => import("./SubmitFlowchart.vue"))
// 只有老师看得见(下面的弹框挂了 isTeacherOrAbove),静态 import 的话每个学生
// 打开题目都要白拉一份 chart.js(~68KB gzip)
const StatisticsPanel = defineAsyncComponent(
() => import("shared/components/StatisticsPanel.vue"),
)
const StatisticsPanel = defineAsyncComponent(() => import("shared/components/StatisticsPanel.vue"))
interface Props {
storageKey: string
@@ -74,13 +70,9 @@ const showHelpButton = computed(
* 不像弹框那样按一下 Esc 就把协作关掉了。
*/
const collabHere = computed(
() =>
collabStore.room !== null &&
collabStore.room.problemId === problem.value?._id,
)
const showCollabBar = computed(
() => collabHere.value && userStore.isTeacherOrAbove,
() => collabStore.room !== null && collabStore.room.problemId === problem.value?._id,
)
const showCollabBar = computed(() => collabHere.value && userStore.isTeacherOrAbove)
/**
* 状态全塞进按钮本身。原来旁边还挂一个 n-tag 说明排队情况,一行工具栏
@@ -212,8 +204,7 @@ const copy = async () => {
const reset = () => {
codeStore.setCode(
problem.value!.template[codeStore.code.language] ||
SOURCES[codeStore.code.language],
problem.value!.template[codeStore.code.language] || SOURCES[codeStore.code.language],
)
storage.remove(storageKey)
message.success("代码重置成功")
@@ -280,11 +271,7 @@ onMounted(() => {
<SubmitCode v-else />
<n-button
v-if="isDesktop && showGoSubmissionButton"
:size="buttonSize"
@click="goSubmissions"
>
<n-button v-if="isDesktop && showGoSubmissionButton" :size="buttonSize" @click="goSubmissions">
提交信息
</n-button>
@@ -313,11 +300,7 @@ onMounted(() => {
</n-tag>
<!-- 显式的「结束」:求助记录一并清掉。跳走页面发的是 leave("left"),
那边只是退回排队 —— 见 store 里 leave 的注释 -->
<n-button
:size="buttonSize"
type="primary"
@click="collabStore.leave('done')"
>
<n-button :size="buttonSize" type="primary" @click="collabStore.leave('done')">
结束协作
</n-button>
</template>
@@ -22,11 +22,7 @@ watch(
<n-alert v-if="renderError" type="error" title="渲染失败" size="small">
{{ renderError }}
</n-alert>
<div
v-show="!renderError"
ref="mermaidContainer"
class="flowchart-container"
></div>
<div v-show="!renderError" ref="mermaidContainer" class="flowchart-container"></div>
</div>
</template>
@@ -108,9 +108,7 @@ const KIND_TAG_TYPE = {
count: "info",
} as const
const astRequirements = computed(() =>
Object.entries(problem.value?.astRequirements ?? {}),
)
const astRequirements = computed(() => Object.entries(problem.value?.astRequirements ?? {}))
async function test(sample: Sample, index: number) {
samples.value = samples.value.map((sample) => {
@@ -122,10 +120,7 @@ async function test(sample: Sample, index: number) {
const res = await createTestSubmission(codeStore.code, sample.input)
samples.value = samples.value.map((sample) => {
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"
return {
...sample,
msg: res.output,
@@ -267,9 +262,7 @@ function type(status: ProblemStatus) {
:total-rows="sqlExpectedQuery.total_rows"
:truncated="sqlExpectedQuery.truncated"
/>
<p v-if="!problem.sqlConfig?.order_sensitive" class="sqlNote">
结果顺序不限
</p>
<p v-if="!problem.sqlConfig?.order_sensitive" class="sqlNote">结果顺序不限</p>
</template>
<div v-for="t in sqlChangedTables" :key="t.name">
<p class="sqlTableName">
@@ -313,9 +306,7 @@ function type(status: ProblemStatus) {
</p>
<n-list bordered style="margin-bottom: 8px">
<n-list-item v-for="(rule, i) in rules" :key="i">
<n-tag :type="KIND_TAG_TYPE[rule.kind]">{{
rule.description
}}</n-tag>
<n-tag :type="KIND_TAG_TYPE[rule.kind]">{{ rule.description }}</n-tag>
</n-list-item>
</n-list>
</div>
@@ -330,19 +321,11 @@ function type(status: ProblemStatus) {
例子 {{ index + 1 }}
</n-flex>
</p>
<n-button
size="small"
:type="type(sample.status)"
@click="test(sample, index)"
>
<n-button size="small" :type="type(sample.status)" @click="test(sample, index)">
{{ label(sample.status, sample.loading) }}
</n-button>
</n-flex>
<n-descriptions
bordered
:column="2"
label-style="width: 50%; min-width: 100px"
>
<n-descriptions bordered :column="2" label-style="width: 50%; min-width: 100px">
<n-descriptions-item>
<template #label>
<n-flex>
@@ -31,9 +31,7 @@ const { problem } = storeToRefs(problemStore)
* 协作跟着结束」:页面上没有弹框可关,结束协作只有工具栏那个按钮和离开这一页两条路。
*/
const collabHere = computed(
() =>
collabStore.room !== null &&
collabStore.room.problemId === problem.value?._id,
() => collabStore.room !== null && collabStore.room.problemId === problem.value?._id,
)
/** 协作中的教师:编辑器里是学生的代码,不是他自己的 */
@@ -66,8 +64,7 @@ watch(
const wasHere = previous?.[0] ?? false
if (here && language) {
if (!wasHere) teacherLanguageBefore = codeStore.code.language
if (codeStore.code.language !== language)
codeStore.code.language = language
if (codeStore.code.language !== language) codeStore.code.language = language
return
}
if (!here && wasHere) {
@@ -84,8 +81,7 @@ const { isDesktop } = useBreakpoints()
const contestID = route.params.contestID || null
const storageKey = computed(
() =>
`problem_${problem.value!._id}_contest_${contestID}_lang_${codeStore.code.language}`,
() => `problem_${problem.value!._id}_contest_${contestID}_lang_${codeStore.code.language}`,
)
const editorHeight = computed(() =>
@@ -100,10 +96,7 @@ function loadCode() {
SOURCES[codeStore.code.language],
)
// 换了题才重新计数,同一道题重复 loadCode(协作结束读回草稿)是接着记
beginEditTrace(
`problem_${problem.value!._id}_contest_${contestID}`,
codeStore.code.value.length,
)
beginEditTrace(`problem_${problem.value!._id}_contest_${contestID}`, codeStore.code.value.length)
}
onMounted(loadCode)
@@ -128,8 +121,7 @@ const changeLanguage = (v: LANGUAGE) => {
codeStore.setCode(
savedCode && storageKey.value.split("_").pop() === v
? savedCode
: problem.value!.template[codeStore.code.language] ||
SOURCES[codeStore.code.language],
: problem.value!.template[codeStore.code.language] || SOURCES[codeStore.code.language],
)
}
@@ -139,10 +131,7 @@ provide("flowchartEditorRef", flowchartEditorRef)
<template>
<n-flex vertical>
<EditorToolbar
:storage-key="storageKey"
@change-language="changeLanguage"
/>
<EditorToolbar :storage-key="storageKey" @change-language="changeLanguage" />
<!--
协作中教师这边不会落到流程图分支:上面那个 watch 已经把他的语言换成了学生的,
而求助入口本身就排掉了流程图(EditorToolbar.vue 的 showHelpButton、服务端的
@@ -150,10 +139,7 @@ provide("flowchartEditorRef", flowchartEditorRef)
学生自己切到流程图就是不写代码了,编辑器卸载、协作正常结束(SyncCodeEditor
的 detach),这是原来就有的语义。
-->
<FlowchartEditor
v-if="codeStore.code.language === 'Flowchart'"
ref="flowchartEditorRef"
/>
<FlowchartEditor v-if="codeStore.code.language === 'Flowchart'" ref="flowchartEditorRef" />
<SyncCodeEditor
v-else
v-model:value="codeStore.code.value"
@@ -9,10 +9,7 @@ const mermaidContainer = useTemplateRef<HTMLElement>("mermaidContainer")
const { renderError, renderFlowchart } = useMermaid()
const renderProblemFlowchart = async () => {
await renderFlowchart(
mermaidContainer.value,
problem.value?.mermaidCode ?? "",
)
await renderFlowchart(mermaidContainer.value, problem.value?.mermaidCode ?? "")
}
onMounted(renderProblemFlowchart)
@@ -6,14 +6,7 @@ import { DIFFICULTY, JUDGE_STATUS } from "utils/constants"
import type { SUBMISSION_RESULT } from "utils/types"
import { getACRateNumber, getTagColor, parseTime } from "utils/functions"
import { Pie } from "vue-chartjs"
import {
Chart as ChartJS,
ArcElement,
Title,
Tooltip,
Legend,
Colors,
} from "chart.js"
import { Chart as ChartJS, ArcElement, Title, Tooltip, Legend, Colors } from "chart.js"
import { getProblemBeatRate } from "oj/api"
import { getProblemYearlyAC, type YearlyACData } from "oj/api"
import ProblemYearlyChart from "./ProblemYearlyChart.vue"
@@ -106,12 +99,7 @@ onMounted(() => {
</script>
<template>
<n-descriptions
bordered
label-placement="left"
:column="isDesktop ? 3 : 1"
v-if="problem"
>
<n-descriptions bordered label-placement="left" :column="isDesktop ? 3 : 1" v-if="problem">
<n-descriptions-item label="编号">
{{ problem._id }}
</n-descriptions-item>
@@ -141,10 +129,7 @@ onMounted(() => {
<n-flex vertical align="center">
<Icon v-if="isDesktop" :icon="item.icon" width="40" />
<n-h2 class="number">
<n-number-animation
:to="item.title"
:precision="item.int ? 0 : 2"
/>
<n-number-animation :to="item.title" :precision="item.int ? 0 : 2" />
<span v-if="item.suffix">{{ item.suffix }}</span>
</n-h2>
<n-h4 class="number-label">{{ item.content }}</n-h4>
@@ -9,20 +9,8 @@ defineProps<{
<template>
<n-flex align="center">
<span>{{ problem.title }}</span>
<Icon
v-if="problem.allowFlowchart"
width="18"
icon="vscode-icons:file-type-drawio"
/>
<Icon
v-else-if="problem.showFlowchart"
width="18"
icon="vscode-icons:file-type-graphql"
/>
<Icon
v-if="problem.hasAstRules"
width="18"
icon="vscode-icons:file-type-light-todo"
/>
<Icon v-if="problem.allowFlowchart" width="18" icon="vscode-icons:file-type-drawio" />
<Icon v-else-if="problem.showFlowchart" width="18" icon="vscode-icons:file-type-graphql" />
<Icon v-if="problem.hasAstRules" width="18" icon="vscode-icons:file-type-light-todo" />
</n-flex>
</template>
@@ -54,15 +54,12 @@ function getWheelItemStyle(index: number): CSSProperties {
const dividerAngle = index * sliceAngle - sliceAngle / 2
const position = pointOnCircle(centerAngle, wheelGeometry.contentRadius)
const push = pointOnCircle(centerAngle, wheelGeometry.pushRadius)
const arcPoints = Array.from(
{ length: wheelGeometry.arcPointCount },
(_, pointIndex) => {
const progress = pointIndex / (wheelGeometry.arcPointCount - 1)
const angle = startAngle + (endAngle - startAngle) * progress
const point = pointOnCircle(angle, wheelGeometry.outerRadius)
return `${point.x.toFixed(3)}% ${point.y.toFixed(3)}%`
},
)
const arcPoints = Array.from({ length: wheelGeometry.arcPointCount }, (_, pointIndex) => {
const progress = pointIndex / (wheelGeometry.arcPointCount - 1)
const angle = startAngle + (endAngle - startAngle) * progress
const point = pointOnCircle(angle, wheelGeometry.outerRadius)
return `${point.x.toFixed(3)}% ${point.y.toFixed(3)}%`
})
return {
"--segment-path": `polygon(50% 50%, ${arcPoints.join(", ")})`,
@@ -192,9 +189,7 @@ function getPointerIndex(event: PointerEvent | MouseEvent) {
const angle = (Math.atan2(y, x) * 180) / Math.PI
const rawIndex = Math.round((angle - wheelGeometry.startAngle) / sliceAngle)
return (
((rawIndex % wheelItems.length) + wheelItems.length) % wheelItems.length
)
return ((rawIndex % wheelItems.length) + wheelItems.length) % wheelItems.length
}
function preview(index: number, fromKeyboard = false) {
@@ -225,14 +220,7 @@ function onWheelClick(event: MouseEvent) {
}
async function pick(key: ReactionKey) {
if (
!problem.value ||
!solved.value ||
locked.value ||
loading.value ||
submitting.value
)
return
if (!problem.value || !solved.value || locked.value || loading.value || submitting.value) return
activeIndex.value = null
keyboardActive.value = false
@@ -345,18 +333,10 @@ watch(
>
<span class="option-content">
<span class="option-icon" aria-hidden="true">
<Icon
:icon="
submitting === item.key
? 'svg-spinners:180-ring-with-bg'
: item.icon
"
/>
<Icon :icon="submitting === item.key ? 'svg-spinners:180-ring-with-bg' : item.icon" />
</span>
<span class="option-label">{{ item.label }}</span>
<span v-if="counts" class="option-count">
{{ counts[item.key] }} 人
</span>
<span v-if="counts" class="option-count"> {{ counts[item.key] }} 人 </span>
</span>
</button>
@@ -420,22 +400,13 @@ watch(
.segment-face:is(.is-active, .is-submitting) {
z-index: 2;
background: color-mix(
in srgb,
var(--reaction-accent) 10%,
var(--reaction-card)
);
transform: translate(calc(var(--push-x) * 0.7), calc(var(--push-y) * 0.7))
scale(1.045);
background: color-mix(in srgb, var(--reaction-accent) 10%, var(--reaction-card));
transform: translate(calc(var(--push-x) * 0.7), calc(var(--push-y) * 0.7)) scale(1.045);
}
.segment-face.is-selected {
z-index: 3;
background: color-mix(
in srgb,
var(--reaction-accent) 18%,
var(--reaction-card)
);
background: color-mix(in srgb, var(--reaction-accent) 18%, var(--reaction-card));
transform: translate(var(--push-x), var(--push-y)) scale(1.075);
}
@@ -512,14 +483,11 @@ watch(
}
.reaction-option:is(.is-active, .is-submitting) .option-content {
transform: translate(calc(var(--push-x) * 0.8), calc(var(--push-y) * 0.8))
scale(1.18);
transform: translate(calc(var(--push-x) * 0.8), calc(var(--push-y) * 0.8)) scale(1.18);
}
.reaction-option:is(.is-active, .is-submitting):active:not(:disabled)
.option-content {
transform: translate(calc(var(--push-x) * 0.8), calc(var(--push-y) * 0.8))
scale(1.1);
.reaction-option:is(.is-active, .is-submitting):active:not(:disabled) .option-content {
transform: translate(calc(var(--push-x) * 0.8), calc(var(--push-y) * 0.8)) scale(1.1);
}
.reaction-option.is-selected .option-content {
@@ -623,13 +591,11 @@ watch(
}
.segment-face:is(.is-active, .is-submitting) {
transform: translate(calc(var(--push-x) * 0.5), calc(var(--push-y) * 0.5))
scale(1.03);
transform: translate(calc(var(--push-x) * 0.5), calc(var(--push-y) * 0.5)) scale(1.03);
}
.segment-face.is-selected {
transform: translate(calc(var(--push-x) * 0.75), calc(var(--push-y) * 0.75))
scale(1.05);
transform: translate(calc(var(--push-x) * 0.75), calc(var(--push-y) * 0.75)) scale(1.05);
}
.option-content {
@@ -637,19 +603,15 @@ watch(
}
.reaction-option:is(.is-active, .is-submitting) .option-content {
transform: translate(calc(var(--push-x) * 0.55), calc(var(--push-y) * 0.55))
scale(1.12);
transform: translate(calc(var(--push-x) * 0.55), calc(var(--push-y) * 0.55)) scale(1.12);
}
.reaction-option:is(.is-active, .is-submitting):active:not(:disabled)
.option-content {
transform: translate(calc(var(--push-x) * 0.55), calc(var(--push-y) * 0.55))
scale(1.06);
.reaction-option:is(.is-active, .is-submitting):active:not(:disabled) .option-content {
transform: translate(calc(var(--push-x) * 0.55), calc(var(--push-y) * 0.55)) scale(1.06);
}
.reaction-option.is-selected .option-content {
transform: translate(calc(var(--push-x) * 0.75), calc(var(--push-y) * 0.75))
scale(1.17);
transform: translate(calc(var(--push-x) * 0.75), calc(var(--push-y) * 0.75)) scale(1.17);
}
.option-icon {
@@ -48,10 +48,7 @@ const columns: DataTableColumn<SubmissionListItem>[] = [
NTooltip,
{},
{
trigger: () =>
h(NButton, { text: true }, () =>
h(Icon, { icon: "catppuccin:lock" }),
),
trigger: () => h(NButton, { text: true }, () => h(Icon, { icon: "catppuccin:lock" })),
default: () =>
"这道题在你已经加入的题单里,加入之前的提交先藏起来了。在题单中做出此题即可解锁;题单过了截止时间也会解锁。",
},
@@ -190,12 +187,7 @@ watch(query, listSubmissions)
</n-flex>
</template>
</n-alert>
<n-alert
class="tip"
type="error"
:show-icon="false"
v-if="rank === -1 && class_ac_count > 0"
>
<n-alert class="tip" type="error" :show-icon="false" v-if="rank === -1 && class_ac_count > 0">
<template #header>
<n-flex align="center">
<span>
@@ -252,12 +244,7 @@ watch(query, listSubmissions)
</n-flex>
</template>
</n-alert>
<n-alert
class="tip"
type="error"
:show-icon="false"
v-if="rank === -1 && all_ac_count > 0"
>
<n-alert class="tip" type="error" :show-icon="false" v-if="rank === -1 && all_ac_count > 0">
<template #header>
<n-flex align="center">
<span>
@@ -288,12 +275,7 @@ watch(query, listSubmissions)
<template v-if="userStore.showSubmissions && userStore.isAuthed">
<!-- 错误分布统计 -->
<n-flex
v-if="statusDistribution.length"
class="tip"
align="center"
:wrap="true"
>
<n-flex v-if="statusDistribution.length" class="tip" align="center" :wrap="true">
<span style="font-weight: bold; font-size: 13px">我的提交统计:</span>
<n-tag
v-for="item in statusDistribution"
@@ -306,17 +288,8 @@ watch(query, listSubmissions)
</n-tag>
</n-flex>
<n-data-table
v-if="submissions.length > 0"
striped
:columns="columns"
:data="submissions"
/>
<Pagination
:total="total"
v-model:limit="query.limit"
v-model:page="query.page"
/>
<n-data-table v-if="submissions.length > 0" striped :columns="columns" :data="submissions" />
<Pagination :total="total" v-model:limit="query.limit" v-model:page="query.page" />
</template>
<!-- 代码详情弹框 -->
@@ -18,15 +18,7 @@ import {
} from "chart.js"
import type { YearlyACData } from "oj/api"
ChartJS.register(
CategoryScale,
Filler,
LinearScale,
LineElement,
PointElement,
Title,
Tooltip,
)
ChartJS.register(CategoryScale, Filler, LinearScale, LineElement, PointElement, Title, Tooltip)
const props = defineProps<{ data: YearlyACData[] }>()
@@ -30,9 +30,7 @@ defineProps<{
</tr>
</tbody>
</n-table>
<p v-if="truncated" class="truncNote">
共 {{ totalRows }} 行,仅展示前 {{ rows.length }} 行
</p>
<p v-if="truncated" class="truncNote">共 {{ totalRows }} 行,仅展示前 {{ rows.length }} 行</p>
</template>
<style scoped>
@@ -52,9 +52,7 @@ const hintFeedbackSending = ref(false)
const TYPE_STEP = 3
const TYPE_INTERVAL = 24
let typingTimer: ReturnType<typeof setInterval> | null = null
const hintTyping = computed(
() => hintContent.value.length < hintTarget.value.length,
)
const hintTyping = computed(() => hintContent.value.length < hintTarget.value.length)
function stopTyping() {
if (typingTimer === null) return
@@ -69,10 +67,7 @@ function startTyping() {
stopTyping()
return
}
hintContent.value = hintTarget.value.slice(
0,
hintContent.value.length + TYPE_STEP,
)
hintContent.value = hintTarget.value.slice(0, hintContent.value.length + TYPE_STEP)
}, TYPE_INTERVAL)
}
@@ -99,17 +94,13 @@ const msg = computed(() => {
// 编译错误或运行时错误时给出提示;
// SQL 题的运行错误多半是"查询题里写了增删改"这类被判题拒绝的语句,err_info 已说明原因,不套这句
if (
(result === SubmissionStatus.compile_error ||
result === SubmissionStatus.runtime_error) &&
(result === SubmissionStatus.compile_error || result === SubmissionStatus.runtime_error) &&
props.submission.language !== "SQL"
) {
msg += "请仔细检查,看看代码的格式是不是写错了!\n\n"
}
if (
result !== SubmissionStatus.ast_check_failed &&
props.submission.statisticInfo?.err_info
) {
if (result !== SubmissionStatus.ast_check_failed && props.submission.statisticInfo?.err_info) {
msg += props.submission.statisticInfo.err_info
}
@@ -258,9 +249,7 @@ const columns: DataTableColumn<JudgeCaseResult>[] = [
</n-alert>
<n-flex
vertical
v-if="
msg || infoTable.length || submission.statisticInfo?.ast_results?.length
"
v-if="msg || infoTable.length || submission.statisticInfo?.ast_results?.length"
>
<n-card v-if="submission.statisticInfo?.ast_results?.length" embedded>
<n-flex vertical :size="8">
@@ -270,9 +259,7 @@ const columns: DataTableColumn<JudgeCaseResult>[] = [
align="center"
:size="6"
>
<n-icon
:color="rule.passed ? theme.successColor : theme.errorColor"
>
<n-icon :color="rule.passed ? theme.successColor : theme.errorColor">
<Icon :icon="rule.passed ? 'ph:check-bold' : 'ph:x-bold'" />
</n-icon>
<span>{{ rule.description }}</span>
@@ -287,23 +274,13 @@ const columns: DataTableColumn<JudgeCaseResult>[] = [
</n-flex>
</n-card>
<n-card v-if="msg" embedded class="msg">{{ msg }}</n-card>
<n-data-table
v-if="infoTable.length"
striped
:data="infoTable"
:columns="columns"
/>
<n-data-table v-if="infoTable.length" striped :data="infoTable" :columns="columns" />
</n-flex>
<!-- AI 提示区域 -->
<template v-if="showAIHint">
<n-card size="small" style="margin-top: 12px; max-width: 480px">
<n-alert
v-if="hintError"
type="error"
:title="hintError"
class="mb-3"
/>
<n-alert v-if="hintError" type="error" :title="hintError" class="mb-3" />
<n-button
v-if="!hintTarget && !hintLoading"
type="primary"
@@ -14,20 +14,13 @@ import { useBreakpoints } from "shared/composables/breakpoints"
import { useUserStore } from "shared/store/user"
import { useCollabStore } from "shared/store/collab"
import { restartEditTrace, snapshotEditTrace } from "oj/problem/utils/editTrace"
import {
checkPythonSyntax,
prefetchPythonSyntaxChecker,
} from "oj/problem/utils/pythonSyntaxCheck"
import { checkPythonSyntax, prefetchPythonSyntaxChecker } from "oj/problem/utils/pythonSyntaxCheck"
// ==================== 异步组件 ====================
const ProblemReaction = defineAsyncComponent(
() => import("./ProblemReaction.vue"),
)
const ProblemReaction = defineAsyncComponent(() => import("./ProblemReaction.vue"))
// 结果面板第一次弹出(也就是第一次提交)时才加载:它带着 DataTable,而判题要等
// 好几秒,这点下载时间藏得住。进页面就加载的话,只看题不提交的人也要付这笔
const SubmissionResult = defineAsyncComponent(
() => import("./SubmissionResult.vue"),
)
const SubmissionResult = defineAsyncComponent(() => import("./SubmissionResult.vue"))
// ==================== 基础状态 ====================
const userStore = useUserStore()
@@ -53,8 +46,7 @@ const { isDesktop } = useBreakpoints()
const { celebrate } = useFireworks()
// ==================== 判题监控 ====================
const { submission, judging, pending, submitting, startMonitoring } =
useSubmissionMonitor()
const { submission, judging, pending, submitting, startMonitoring } = useSubmissionMonitor()
const showResult = ref(false)
const isFormatting = ref(false)
@@ -68,8 +60,7 @@ watch(
() => codeStore.code.language === "Python" && userStore.isAuthed,
(needed) => {
if (!needed) return
if ("requestIdleCallback" in window)
requestIdleCallback(prefetchPythonSyntaxChecker)
if ("requestIdleCallback" in window) requestIdleCallback(prefetchPythonSyntaxChecker)
else setTimeout(prefetchPythonSyntaxChecker, 1000)
},
{ immediate: true },
@@ -162,8 +153,7 @@ async function submit() {
code: codeStore.code.value,
// 编辑过程信号,见 utils/editTrace.ts。协作的判断和 ProblemEditor 的 collabHere 同一个口径
trace: snapshotEditTrace(
collabStore.room !== null &&
collabStore.room.problemId === problem.value!._id,
collabStore.room !== null && collabStore.room.problemId === problem.value!._id,
),
}
if (contestID) {
@@ -219,11 +209,7 @@ watch(
watch(
() => submission.value?.result,
async (result) => {
if (
result !== SubmissionStatus.accepted &&
result !== SubmissionStatus.ast_check_failed
)
return
if (result !== SubmissionStatus.accepted && result !== SubmissionStatus.ast_check_failed) return
// 1. 刷新题目状态
problem.value!.myStatus = 0
@@ -10,10 +10,7 @@ import { sortFlowchartCriteria } from "utils/constants"
import { useBreakpoints } from "shared/composables/breakpoints"
import { useMermaid } from "shared/composables/useMermaid"
import { useMermaidConverter } from "../composables/useMermaidConverter"
import {
useFlowchartWebSocket,
type FlowchartEvaluationUpdate,
} from "shared/composables/websocket"
import { useFlowchartWebSocket, type FlowchartEvaluationUpdate } from "shared/composables/websocket"
import { useMyFlowchartStore } from "shared/store/myFlowchart"
// API 和状态管理
@@ -46,8 +43,7 @@ interface FlowchartEditorInstance {
}
// 通过inject获取FlowchartEditor组件的引用
const flowchartEditorRef =
inject<Ref<FlowchartEditorInstance | null>>("flowchartEditorRef")
const flowchartEditorRef = inject<Ref<FlowchartEditorInstance | null>>("flowchartEditorRef")
const mermaidContainer = useTemplateRef<HTMLElement>("mermaidContainer")
// 基础组合式函数
@@ -77,14 +73,10 @@ const evaluation = ref<Evaluation>({
})
const page = ref(1)
const lastSubmittedMermaidCode = ref("")
const suggestionLines = computed(() =>
splitSuggestionLines(evaluation.value.suggestions),
)
const suggestionLines = computed(() => splitSuggestionLines(evaluation.value.suggestions))
// jsonb 不保留键序,直接遍历会把 40 分的「逻辑正确性」排到最后
const sortedCriteria = computed(() =>
sortFlowchartCriteria(evaluation.value.criteria_details),
)
const sortedCriteria = computed(() => sortFlowchartCriteria(evaluation.value.criteria_details))
function splitSuggestionLines(suggestions?: string | null) {
return suggestions
@@ -111,9 +103,7 @@ const POLL_INTERVAL = 3000
/** 到点还没结果就收手,别无限轮询下去 */
const POLL_TIMEOUT = 3 * 60 * 1000
type Outcome =
| { ok: true; score: number; grade: string }
| { ok: false; error?: string }
type Outcome = { ok: true; score: number; grade: string } | { ok: false; error?: string }
const { pause: pausePolling, resume: resumePolling } = useIntervalFn(
async () => {
@@ -175,21 +165,12 @@ function settle(submissionId: string, outcome: Outcome) {
loading.value = false
if (!outcome.ok) {
message.error(
outcome.error
? `流程图评分失败: ${outcome.error}`
: "流程图评分失败,请稍后重试",
)
message.error(outcome.error ? `流程图评分失败: ${outcome.error}` : "流程图评分失败,请稍后重试")
return
}
latestRating.value = { score: outcome.score, grade: outcome.grade }
message.success(
`流程图评分完成!得分: ${outcome.score}分 (${outcome.grade}级)`,
)
if (
(outcome.grade === "A" || outcome.grade === "S") &&
lastSubmittedMermaidCode.value
) {
message.success(`流程图评分完成!得分: ${outcome.score}分 (${outcome.grade}级)`)
if ((outcome.grade === "A" || outcome.grade === "S") && lastSubmittedMermaidCode.value) {
myFlowchartStore.show(lastSubmittedMermaidCode.value)
}
}
@@ -208,9 +189,8 @@ const handleWebSocketMessage = (data: FlowchartEvaluationUpdate) => {
}
// 创建 WebSocket 连接
const { connect, disconnect, subscribe, unsubscribe } = useFlowchartWebSocket(
handleWebSocketMessage,
)
const { connect, disconnect, subscribe, unsubscribe } =
useFlowchartWebSocket(handleWebSocketMessage)
// 订阅提交更新,同时开启轮询兜底
function subscribeToSubmission(submissionId: string) {
@@ -292,10 +272,7 @@ async function getCurrentSubmission() {
async function getSubmission(submissionPage = 0) {
if (!problem.value?.id) return
const data = await getFlowchartSubmissionDetail(
problem.value.id,
submissionPage,
)
const data = await getFlowchartSubmissionDetail(problem.value.id, submissionPage)
submissionCount.value = data.count
const submission = data.submission
// 翻到没有提交的页时后端返回 null(契约里 submission 是 nullable)——
@@ -324,8 +301,7 @@ async function getSubmission(submissionPage = 0) {
grade: (submission.aiGrade ?? "") as Rating["grade"],
feedback: submission.aiFeedback ?? "",
suggestions: submission.aiSuggestions ?? "",
criteria_details:
submission.aiCriteriaDetails as Evaluation["criteria_details"],
criteria_details: submission.aiCriteriaDetails as Evaluation["criteria_details"],
}
}
@@ -465,18 +441,12 @@ onUnmounted(() => {
<n-alert v-if="renderError" type="error" title="流程图渲染失败">
{{ renderError }}
</n-alert>
<div
class="flowchart"
v-show="!renderError"
ref="mermaidContainer"
></div>
<div class="flowchart" v-show="!renderError" ref="mermaidContainer"></div>
</n-spin>
</div>
<!-- 加载到编辑器按钮 -->
<n-flex style="margin-top: 16px" justify="center">
<n-button @click="loadToEditor" type="primary">
加载到流程图编辑器
</n-button>
<n-button @click="loadToEditor" type="primary"> 加载到流程图编辑器 </n-button>
</n-flex>
</n-gi>
@@ -511,23 +481,11 @@ onUnmounted(() => {
<!-- 详细评分 -->
<n-card v-if="sortedCriteria.length" size="small" title="详细评分">
<div
v-for="[key, detail] in sortedCriteria"
:key="key"
style="margin-bottom: 12px"
>
<div v-for="[key, detail] in sortedCriteria" :key="key" style="margin-bottom: 12px">
<!-- 评分项标题和分数 -->
<n-flex
justify="space-between"
align="center"
style="margin-bottom: 4px"
>
<n-flex justify="space-between" align="center" style="margin-bottom: 4px">
<n-text strong>{{ key }}</n-text>
<n-tag
:type="getPercentType(detail.score / detail.max)"
size="small"
round
>
<n-tag :type="getPercentType(detail.score / detail.max)" size="small" round>
{{ detail.score || 0 }}分 / {{ detail.max }}分
</n-tag>
</n-flex>
@@ -541,16 +499,8 @@ onUnmounted(() => {
</n-grid>
<!-- 分页组件 -->
<n-flex
justify="center"
style="margin-top: 24px"
v-if="submissionCount > 1"
>
<n-pagination
v-model:page="page"
:page-count="submissionCount"
@update-page="updatePage"
/>
<n-flex justify="center" style="margin-top: 24px" v-if="submissionCount > 1">
<n-pagination v-model:page="page" :page-count="submissionCount" @update-page="updatePage" />
</n-flex>
</n-modal>
</n-flex>
@@ -21,13 +21,7 @@ export function getSubmitButtonState({
isJudging,
isCooldown,
}: SubmitButtonStateInput): SubmitButtonState {
const disabled =
!isAuthed ||
!hasCode ||
isFormatting ||
isSubmitting ||
isJudging ||
isCooldown
const disabled = !isAuthed || !hasCode || isFormatting || isSubmitting || isJudging || isCooldown
let label = "提交代码"
if (!isAuthed) {
@@ -35,14 +35,7 @@ function svgToBitmapShape(svg: string): Promise<confetti.Shape> {
resolve({
type: "bitmap",
bitmap,
matrix: [
scale,
0,
0,
scale,
(-renderSize * scale) / 2,
(-renderSize * scale) / 2,
],
matrix: [scale, 0, 0, scale, (-renderSize * scale) / 2, (-renderSize * scale) / 2],
} as unknown as confetti.Shape)
} catch (e) {
reject(e)
@@ -176,14 +169,7 @@ export function useFireworks() {
() => {
const end = Date.now() + 2000
const colors = [
"#ff595e",
"#ff924c",
"#ffca3a",
"#8ac926",
"#1982c4",
"#6a4c93",
]
const colors = ["#ff595e", "#ff924c", "#ffca3a", "#8ac926", "#1982c4", "#6a4c93"]
const frame = () => {
confetti({
@@ -228,14 +214,7 @@ export function useFireworks() {
x: Math.random(),
y: Math.random() - 0.2,
},
colors: [
"#ff0000",
"#00ff00",
"#0000ff",
"#ffff00",
"#ff00ff",
"#00ffff",
],
colors: ["#ff0000", "#00ff00", "#0000ff", "#ffff00", "#ff00ff", "#00ffff"],
})
}, 200)
timers.add(interval)
@@ -380,8 +359,7 @@ export function useFireworks() {
]
// 随机选择一种效果
const randomEffect =
fireworkTypes[Math.floor(Math.random() * fireworkTypes.length)]
const randomEffect = fireworkTypes[Math.floor(Math.random() * fireworkTypes.length)]
randomEffect()
}
@@ -33,15 +33,12 @@ export function useMermaidConverter() {
nodes.forEach((node: any, index: number) => {
idMap.set(node.id, `node_${index}`)
})
const safeId = (id: string) =>
idMap.get(id) || id.replace(/[^a-zA-Z0-9_]/g, "_")
const safeId = (id: string) => idMap.get(id) || id.replace(/[^a-zA-Z0-9_]/g, "_")
// 处理节点 - 根据原始类型和自定义标签
nodes.forEach((node: any) => {
const nodeId = safeId(node.id)
const label = escapeLabel(
node.data?.customLabel || node.data?.label || "节点",
)
const label = escapeLabel(node.data?.customLabel || node.data?.label || "节点")
const originalType = node.data?.originalType || node.type
// 根据节点原始类型确定Mermaid语法
@@ -90,20 +87,13 @@ export function useMermaidConverter() {
// 添加样式定义来区分不同类型的节点
mermaid += "\n"
mermaid +=
" classDef startNode fill:#dcfce7,stroke:#16a34a,stroke-width:2px,color:#0f172a\n"
mermaid +=
" classDef endNode fill:#fee2e2,stroke:#dc2626,stroke-width:2px,color:#0f172a\n"
mermaid +=
" classDef input fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#0f172a\n"
mermaid +=
" classDef output fill:#ede9fe,stroke:#7c3aed,stroke-width:2px,color:#0f172a\n"
mermaid +=
" classDef process fill:#f0f9ff,stroke:#0284c7,stroke-width:2px,color:#0f172a\n"
mermaid +=
" classDef decision fill:#fef3c7,stroke:#d97706,stroke-width:2px,color:#0f172a\n"
mermaid +=
" classDef loop fill:#fae8ff,stroke:#c026d3,stroke-width:2px,color:#0f172a\n"
mermaid += " classDef startNode fill:#dcfce7,stroke:#16a34a,stroke-width:2px,color:#0f172a\n"
mermaid += " classDef endNode fill:#fee2e2,stroke:#dc2626,stroke-width:2px,color:#0f172a\n"
mermaid += " classDef input fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#0f172a\n"
mermaid += " classDef output fill:#ede9fe,stroke:#7c3aed,stroke-width:2px,color:#0f172a\n"
mermaid += " classDef process fill:#f0f9ff,stroke:#0284c7,stroke-width:2px,color:#0f172a\n"
mermaid += " classDef decision fill:#fef3c7,stroke:#d97706,stroke-width:2px,color:#0f172a\n"
mermaid += " classDef loop fill:#fae8ff,stroke:#c026d3,stroke-width:2px,color:#0f172a\n"
mermaid += "\n"
// 为节点应用样式
@@ -4,10 +4,7 @@ import { getSubmission } from "oj/api"
import { SubmissionStatus } from "utils/constants"
import type { PendingAchievement, Submission } from "utils/types"
import { useAchievementStore } from "shared/store/achievement"
import {
useSubmissionWebSocket,
type SubmissionUpdate,
} from "shared/composables/websocket"
import { useSubmissionWebSocket, type SubmissionUpdate } from "shared/composables/websocket"
/**
* 判题监控 Composable
@@ -29,10 +26,7 @@ export function useSubmissionMonitor() {
const result = res.result
// 判题完成,停止轮询
if (
result !== SubmissionStatus.judging &&
result !== SubmissionStatus.pending
) {
if (result !== SubmissionStatus.judging && result !== SubmissionStatus.pending) {
pausePolling()
}
} catch (error) {
@@ -75,9 +69,7 @@ export function useSubmissionMonitor() {
// 判题完成或出错,获取完整详情
if (data.status === "finished" || data.status === "error") {
console.log(
`[SubmissionMonitor] 判题${data.status === "finished" ? "完成" : "出错"}`,
)
console.log(`[SubmissionMonitor] 判题${data.status === "finished" ? "完成" : "出错"}`)
// 停止轮询(WebSocket已成功)
pausePolling()
@@ -93,13 +85,8 @@ export function useSubmissionMonitor() {
}
// 初始化 WebSocket
const {
connect,
subscribe,
unsubscribe,
scheduleDisconnect,
cancelScheduledDisconnect,
} = useSubmissionWebSocket(handleSubmissionUpdate)
const { connect, subscribe, unsubscribe, scheduleDisconnect, cancelScheduledDisconnect } =
useSubmissionWebSocket(handleSubmissionUpdate)
// ==================== 轮询保底启动 ====================
const { start: startPollingFallback } = useTimeoutFn(
@@ -143,17 +130,11 @@ export function useSubmissionMonitor() {
}
// ==================== 计算属性 ====================
const judging = computed(
() => submission.value?.result === SubmissionStatus.judging,
)
const judging = computed(() => submission.value?.result === SubmissionStatus.judging)
const pending = computed(
() => submission.value?.result === SubmissionStatus.pending,
)
const pending = computed(() => submission.value?.result === SubmissionStatus.pending)
const submitting = computed(
() => submission.value?.result === SubmissionStatus.submitting,
)
const submitting = computed(() => submission.value?.result === SubmissionStatus.submitting)
const isProcessing = computed(() => {
return judging.value || pending.value || submitting.value
+16 -67
View File
@@ -13,27 +13,13 @@ const loadProblemEditor = () => import("./components/ProblemEditor.vue")
const loadContestEditor = () => import("./components/BasicEditor.vue")
const ProblemEditor = defineAsyncComponent(loadProblemEditor)
const BasicEditor = defineAsyncComponent(loadContestEditor)
const EditorForTest = defineAsyncComponent(
() => import("./components/EditorForTest.vue"),
)
const ProblemContent = defineAsyncComponent(
() => import("./components/ProblemContent.vue"),
)
const ProblemInfo = defineAsyncComponent(
() => import("./components/ProblemInfo.vue"),
)
const ProblemSubmission = defineAsyncComponent(
() => import("./components/ProblemSubmission.vue"),
)
const ProblemReaction = defineAsyncComponent(
() => import("./components/ProblemReaction.vue"),
)
const ProblemFlowchart = defineAsyncComponent(
() => import("./components/ProblemFlowchart.vue"),
)
const MyFlowchartTab = defineAsyncComponent(
() => import("./components/MyFlowchartTab.vue"),
)
const EditorForTest = defineAsyncComponent(() => import("./components/EditorForTest.vue"))
const ProblemContent = defineAsyncComponent(() => import("./components/ProblemContent.vue"))
const ProblemInfo = defineAsyncComponent(() => import("./components/ProblemInfo.vue"))
const ProblemSubmission = defineAsyncComponent(() => import("./components/ProblemSubmission.vue"))
const ProblemReaction = defineAsyncComponent(() => import("./components/ProblemReaction.vue"))
const ProblemFlowchart = defineAsyncComponent(() => import("./components/ProblemFlowchart.vue"))
const MyFlowchartTab = defineAsyncComponent(() => import("./components/MyFlowchartTab.vue"))
interface Props {
problemID: string
@@ -92,9 +78,7 @@ watch(
([rawTab]) => {
const tabs = tabOptions.value
const fallback = tabs[0] ?? "content"
currentTab.value = tabs.includes(rawTab as string)
? (rawTab as string)
: fallback
currentTab.value = tabs.includes(rawTab as string) ? (rawTab as string) : fallback
},
{ immediate: true },
)
@@ -189,26 +173,13 @@ watch(
<n-tab-pane name="info" tab="题目统计" :disabled="!!problemSetId">
<ProblemInfo />
</n-tab-pane>
<n-tab-pane
v-if="!contestID"
name="comment"
tab="题目点评"
:disabled="!!problemSetId"
>
<n-tab-pane v-if="!contestID" name="comment" tab="题目点评" :disabled="!!problemSetId">
<ProblemReaction />
</n-tab-pane>
<n-tab-pane
v-if="myFlowchartStore.showing"
name="my-flowchart"
tab="我的流程图"
>
<n-tab-pane v-if="myFlowchartStore.showing" name="my-flowchart" tab="我的流程图">
<MyFlowchartTab />
</n-tab-pane>
<n-tab-pane
name="submission"
tab="我的提交"
:disabled="!!problemSetId"
>
<n-tab-pane name="submission" tab="我的提交" :disabled="!!problemSetId">
<ProblemSubmission />
</n-tab-pane>
</n-tabs>
@@ -237,26 +208,13 @@ watch(
<n-tab-pane name="info" tab="题目统计" :disabled="!!problemSetId">
<ProblemInfo />
</n-tab-pane>
<n-tab-pane
v-if="!contestID"
name="comment"
tab="题目点评"
:disabled="!!problemSetId"
>
<n-tab-pane v-if="!contestID" name="comment" tab="题目点评" :disabled="!!problemSetId">
<ProblemReaction />
</n-tab-pane>
<n-tab-pane
v-if="myFlowchartStore.showing"
name="my-flowchart"
tab="我的流程图"
>
<n-tab-pane v-if="myFlowchartStore.showing" name="my-flowchart" tab="我的流程图">
<MyFlowchartTab />
</n-tab-pane>
<n-tab-pane
name="submission"
tab="我的提交"
:disabled="!!problemSetId"
>
<n-tab-pane name="submission" tab="我的提交" :disabled="!!problemSetId">
<ProblemSubmission />
</n-tab-pane>
</n-tabs>
@@ -277,19 +235,10 @@ watch(
<n-tab-pane name="info" tab="统计" :disabled="!!problemSetId">
<ProblemInfo />
</n-tab-pane>
<n-tab-pane
v-if="!contestID"
name="comment"
tab="点评"
:disabled="!!problemSetId"
>
<n-tab-pane v-if="!contestID" name="comment" tab="点评" :disabled="!!problemSetId">
<ProblemReaction />
</n-tab-pane>
<n-tab-pane
v-if="myFlowchartStore.showing"
name="my-flowchart"
tab="我的流程图"
>
<n-tab-pane v-if="myFlowchartStore.showing" name="my-flowchart" tab="我的流程图">
<MyFlowchartTab />
</n-tab-pane>
<n-tab-pane name="submission" tab="提交" :disabled="!!problemSetId">
+9 -40
View File
@@ -122,14 +122,7 @@ watchDebounced(() => query.keyword, listProblems, {
// 监听其他查询条件变化
watch(
() => [
query.tag,
query.difficulty,
query.limit,
query.page,
query.author,
query.sort,
],
() => [query.tag, query.difficulty, query.limit, query.page, query.author, query.sort],
listProblems,
)
@@ -174,18 +167,12 @@ const baseColumns: DataTableColumn<ProblemRow>[] = [
render: (row) => h(ProblemStatus, { status: row.status }),
},
{
title: renderTableTitle(
"编号",
"streamline-ultimate-color:board-game-dice-1",
),
title: renderTableTitle("编号", "streamline-ultimate-color:board-game-dice-1"),
key: "_id",
width: 100,
},
{
title: renderTableTitle(
"题目",
"streamline-ultimate-color:fruit-watermelon",
),
title: renderTableTitle("题目", "streamline-ultimate-color:fruit-watermelon"),
key: "title",
minWidth: 200,
render: (row) => h(ProblemListTitle, { problem: row }),
@@ -195,16 +182,13 @@ const baseColumns: DataTableColumn<ProblemRow>[] = [
key: "difficulty",
width: 100,
render: (row) =>
row.difficulty
? h(NTag, { type: getTagColor(row.difficulty) }, () => row.difficulty)
: null,
row.difficulty ? h(NTag, { type: getTagColor(row.difficulty) }, () => row.difficulty) : null,
},
{
title: renderTableTitle("标签", "streamline-ultimate-color:attachment"),
key: "tags",
width: 260,
render: (row) =>
h(NFlex, () => row.tags.map((t) => h(NTag, { key: t }, () => t))),
render: (row) => h(NFlex, () => row.tags.map((t) => h(NTag, { key: t }, () => t))),
},
{
title: renderTableTitle("出题者", "streamline-emojis:man-raising-hand-2"),
@@ -226,9 +210,7 @@ const baseColumns: DataTableColumn<ProblemRow>[] = [
]
const columns = computed(() =>
userStore.isAuthed
? baseColumns
: baseColumns.filter((c: any) => c.key !== "status"),
userStore.isAuthed ? baseColumns : baseColumns.filter((c: any) => c.key !== "status"),
)
function rowProps(row: ProblemRow) {
@@ -280,11 +262,7 @@ function rowProps(row: ProblemRow) {
<n-button @click="clearQuery" quaternary>重置</n-button>
</n-form-item>
<n-form-item>
<n-button
@click="toggleShowTag()"
quaternary
icon-placement="right"
>
<n-button @click="toggleShowTag()" quaternary icon-placement="right">
<template #icon>
<Icon v-if="showTag" icon="ph:caret-down"></Icon>
<Icon v-else icon="ph:caret-up"></Icon>
@@ -310,18 +288,9 @@ function rowProps(row: ProblemRow) {
</n-tag>
</n-flex>
</n-collapse-transition>
<n-data-table
:bordered="false"
:data="problems"
:columns="columns"
:row-props="rowProps"
/>
<n-data-table :bordered="false" :data="problems" :columns="columns" :row-props="rowProps" />
</n-flex>
<Pagination
:total="total"
v-model:limit="query.limit"
v-model:page="query.page"
/>
<Pagination :total="total" v-model:limit="query.limit" v-model:page="query.page" />
</template>
<style scoped>
+3 -8
View File
@@ -82,8 +82,7 @@ export function restartEditTrace(len: number) {
function touch() {
const now = performance.now()
if (lastEditAt !== null && now - lastEditAt <= IDLE_MS)
activeMs += now - lastEditAt
if (lastEditAt !== null && now - lastEditAt <= IDLE_MS) activeMs += now - lastEditAt
lastEditAt = now
}
@@ -103,8 +102,7 @@ export const editTraceExtensions = [
for (const tr of update.transactions) {
if (!tr.docChanged) continue
// 顺序要紧:isUserEvent("input") 也会匹配 "input.paste"
const pasted =
tr.isUserEvent("input.paste") || tr.isUserEvent("input.drop")
const pasted = tr.isUserEvent("input.paste") || tr.isUserEvent("input.drop")
const typed = !pasted && tr.isUserEvent("input")
const deleted = tr.isUserEvent("delete")
if (!pasted && !typed && !deleted) continue
@@ -115,10 +113,7 @@ export const editTraceExtensions = [
// 原样替换不算:closeBrackets 越过已有的右括号 / 引号时,是把 `)` 替换成 `)`
// 而不是只挪光标(@codemirror/autocomplete 的 handleClose),不排掉的话
// 每敲一个右括号就多记一个键入加一个删除
if (
toA - fromA === text.length &&
tr.startState.sliceDoc(fromA, toA) === text.toString()
)
if (toA - fromA === text.length && tr.startState.sliceDoc(fromA, toA) === text.toString())
return
removed += toA - fromA
inserted += text.length
@@ -31,9 +31,7 @@ export function prefetchPythonSyntaxChecker() {
* 只编译不执行,不受 input() 等 IO 调用影响。
* 加载失败时返回 null(放行提交),交给后端判题兜底。
*/
export async function checkPythonSyntax(
code: string,
): Promise<PythonSyntaxError | null> {
export async function checkPythonSyntax(code: string): Promise<PythonSyntaxError | null> {
let Sk: any
try {
Sk = await loadSkulpt()