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

@@ -42,12 +42,7 @@
<!-- 右侧:评分详情区域 -->
<n-gi :span="2">
<!-- AI反馈 -->
<n-card
v-if="submission.aiFeedback"
size="small"
title="AI反馈"
style="margin-bottom: 16px"
>
<n-card v-if="submission.aiFeedback" size="small" title="AI反馈" style="margin-bottom: 16px">
<n-text>{{ submission.aiFeedback }}</n-text>
</n-card>
@@ -59,10 +54,7 @@
style="margin-bottom: 16px"
>
<n-flex vertical :size="6">
<n-text
v-for="(suggestion, index) in suggestionLines"
:key="`${index}-${suggestion}`"
>
<n-text v-for="(suggestion, index) in suggestionLines" :key="`${index}-${suggestion}`">
{{ suggestion }}
</n-text>
</n-flex>
@@ -70,23 +62,11 @@
<!-- 详细评分 -->
<n-card v-if="sortedCriteria.length > 0" 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>
@@ -121,39 +101,35 @@ const submission = ref<FlowchartSubmission | null>(null)
* 评分项明细。契约里是 `Record<string, unknown>` —— 内容是 AI 模型原样吐出的 JSON,
* 后端不校验形状,所以这里只能按约定断言,字段缺失时用 0 / 空串兜底。
*/
const criteriaDetails = computed<
Record<string, { score: number; max: number; comment: string }>
>(() => {
const raw = submission.value?.aiCriteriaDetails ?? {}
return Object.fromEntries(
Object.entries(raw).map(([key, value]) => {
const item = (value ?? {}) as Partial<{
score: number
max: number
comment: string
}>
return [
key,
{
score: item.score ?? 0,
max: item.max ?? 0,
comment: item.comment ?? "",
},
]
}),
)
})
// jsonb 不保留键序,直接遍历会把 40 分的「逻辑正确性」排到最后
const sortedCriteria = computed(() =>
sortFlowchartCriteria(criteriaDetails.value),
const criteriaDetails = computed<Record<string, { score: number; max: number; comment: string }>>(
() => {
const raw = submission.value?.aiCriteriaDetails ?? {}
return Object.fromEntries(
Object.entries(raw).map(([key, value]) => {
const item = (value ?? {}) as Partial<{
score: number
max: number
comment: string
}>
return [
key,
{
score: item.score ?? 0,
max: item.max ?? 0,
comment: item.comment ?? "",
},
]
}),
)
},
)
// jsonb 不保留键序,直接遍历会把 40 分的「逻辑正确性」排到最后
const sortedCriteria = computed(() => sortFlowchartCriteria(criteriaDetails.value))
const loading = ref(false)
const rendering = ref(false)
const showLargeImage = ref(false)
const suggestionLines = computed(() =>
splitSuggestionLines(submission.value?.aiSuggestions),
)
const suggestionLines = computed(() => splitSuggestionLines(submission.value?.aiSuggestions))
function splitSuggestionLines(suggestions?: string | null) {
return suggestions
@@ -184,10 +160,7 @@ async function loadSubmission() {
if (submission.value?.mermaidCode) {
rendering.value = true
await nextTick()
await renderFlowchart(
mermaidContainer.value,
submission.value.mermaidCode,
)
await renderFlowchart(mermaidContainer.value, submission.value.mermaidCode)
rendering.value = false
}
} catch (error) {
@@ -42,9 +42,7 @@ const props = defineProps<Props>()
defineEmits(["showCode"])
const userStore = useUserStore()
const isOwnSubmission = computed(
() => userStore.profile?.user?.id === props.submission.userId,
)
const isOwnSubmission = computed(() => userStore.profile?.user?.id === props.submission.userId)
function goto() {
window.open("/submission/" + props.submission.id, "_blank")
@@ -94,9 +94,7 @@ onMounted(async () => {
class="hour-bars"
:style="{
borderBottomColor:
hour === currentHour
? themeVars.primaryColor
: themeVars.dividerColor,
hour === currentHour ? themeVars.primaryColor : themeVars.dividerColor,
}"
>
<div
@@ -118,9 +116,7 @@ onMounted(async () => {
<n-divider style="margin: 16px 0">按语言</n-divider>
<div class="rows">
<div class="row" v-for="row in stats.languages" :key="row.language">
<n-text class="row-name">{{
LANGUAGE_SHOW_VALUE[row.language]
}}</n-text>
<n-text class="row-name">{{ LANGUAGE_SHOW_VALUE[row.language] }}</n-text>
<n-progress
class="row-bar"
type="line"
@@ -134,12 +130,7 @@ onMounted(async () => {
<n-divider style="margin: 16px 0">按状态</n-divider>
<n-flex align="center">
<n-flex
align="center"
:size="4"
v-for="row in stats.results"
:key="row.result"
>
<n-flex align="center" :size="4" v-for="row in stats.results" :key="row.result">
<SubmissionResultTag :result="row.result" />
<n-text>{{ row.count }}</n-text>
</n-flex>
@@ -148,11 +139,7 @@ onMounted(async () => {
<template v-if="stats.problems.length">
<n-divider style="margin: 16px 0">今天最热的题</n-divider>
<div class="rows">
<div
class="row"
v-for="row in stats.problems"
:key="row.problemDisplayId"
>
<div class="row" v-for="row in stats.problems" :key="row.problemDisplayId">
<n-button
class="problem"
text
@@ -169,11 +156,7 @@ onMounted(async () => {
</template>
</template>
<!-- 请求失败时 stats 还是 null,转圈停下来总得留句话 -->
<n-empty
v-else-if="!loading"
description="统计拉取失败"
style="margin: 40px 0"
/>
<n-empty v-else-if="!loading" description="统计拉取失败" style="margin: 40px 0" />
<div v-else style="height: 200px"></div>
</n-spin>
</template>
+4 -18
View File
@@ -1,11 +1,7 @@
<script setup lang="ts">
import { getSubmission } from "oj/api"
import type { JudgeCaseResult } from "@oj2/contract"
import {
JUDGE_STATUS,
LANGUAGE_FORMAT_VALUE,
LANGUAGE_SHOW_VALUE,
} from "utils/constants"
import { JUDGE_STATUS, LANGUAGE_FORMAT_VALUE, LANGUAGE_SHOW_VALUE } from "utils/constants"
import {
parseTime,
submissionCaseResults,
@@ -43,9 +39,7 @@ const loading = ref(false)
* 测试点明细。`info` 在契约里是「完整形状或空对象」的联合(非管理员拿到的是空对象),
* `data` 本身也可能为 null —— 两种情况都由这个访问器归成空数组,模板里不再直接取。
*/
const caseResults = computed(() =>
submissionCaseResults(submission.value?.info),
)
const caseResults = computed(() => submissionCaseResults(submission.value?.info))
async function init() {
submission.value = props.submission
@@ -141,11 +135,7 @@ onMounted(init)
</n-flex>
</n-alert>
<n-flex :vertical="isDesktop" justify="center">
<n-button
v-if="submission.language !== 'SQL'"
secondary
@click="copyToCat"
>
<n-button v-if="submission.language !== 'SQL'" secondary @click="copyToCat">
复制到自测猫
</n-button>
<n-button secondary @click="copyToProblem">复制回到题目</n-button>
@@ -159,11 +149,7 @@ onMounted(init)
show-line-numbers
/>
</n-card>
<n-data-table
v-if="!hideList && caseResults.length"
:columns="columns"
:data="caseResults"
/>
<n-data-table v-if="!hideList && caseResults.length" :columns="columns" :data="caseResults" />
</n-flex>
<n-spin v-else :show="loading" class="loading-container"> </n-spin>
</template>
+33 -100
View File
@@ -11,11 +11,7 @@ import {
} from "oj/api"
import { parseTime } from "utils/functions"
import type { Grade as GradeValue } from "utils/types"
import type {
FlowchartSubmissionListItem,
LANGUAGE,
SubmissionListItem,
} from "utils/types"
import type { FlowchartSubmissionListItem, LANGUAGE, SubmissionListItem } from "utils/types"
import Pagination from "shared/components/Pagination.vue"
import SubmissionResultTag from "shared/components/SubmissionResultTag.vue"
import { useBreakpoints } from "shared/composables/breakpoints"
@@ -44,15 +40,11 @@ import FlowchartLink from "./components/FlowchartLink.vue"
// 下面四个组件只在默认关闭的 n-modal 里用,其中两个统计面板还只有老师看得见。
// 静态 import 会把它们拖进本路由的关键路径——光 chart.js 就 197KB,进页面前必须先下完。
// 改成异步后本路由增量下载从 675KB / 59 个文件降到 360KB 出头。
const StatisticsPanel = defineAsyncComponent(
() => import("shared/components/StatisticsPanel.vue"),
)
const StatisticsPanel = defineAsyncComponent(() => import("shared/components/StatisticsPanel.vue"))
const FlowchartStatisticsPanel = defineAsyncComponent(
() => import("shared/components/FlowchartStatisticsPanel.vue"),
)
const TodayStatistics = defineAsyncComponent(
() => import("./components/TodayStatistics.vue"),
)
const TodayStatistics = defineAsyncComponent(() => import("./components/TodayStatistics.vue"))
const SubmissionDetail = defineAsyncComponent(() => import("./detail.vue"))
const FlowchartScoreDetail = defineAsyncComponent(
() => import("./components/FlowchartScoreDetail.vue"),
@@ -267,9 +259,7 @@ function showCodePanel(id: string, problem: string) {
* 只在**能看代码**的行之间走 —— showLink 是后端按题单规则算出来的,
* 跳到一条看不了的上面只会得到一个空弹框。
*/
const viewableSubmissions = computed(() =>
submissions.value.filter((row) => row.showLink),
)
const viewableSubmissions = computed(() => submissions.value.filter((row) => row.showLink))
const currentCodeIndex = computed(() =>
viewableSubmissions.value.findIndex((row) => row.id === submissionID.value),
)
@@ -308,24 +298,19 @@ function moveCodePanel(step: 1 | -1) {
// 上下(和左右)翻阅代码详情。Esc 关弹框是 n-modal 自带的,不用管。
// 弹框没开就什么都不做,页面正常滚动
onKeyStroke(
["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"],
(e: KeyboardEvent) => {
if (!codePanel.value) return
// 焦点在输入框里(弹框底下那几个筛选框)时不抢方向键
const target = e.target as HTMLElement | null
if (
target &&
(target.tagName === "INPUT" ||
target.tagName === "TEXTAREA" ||
target.isContentEditable)
) {
return
}
e.preventDefault()
moveCodePanel(e.key === "ArrowUp" || e.key === "ArrowLeft" ? -1 : 1)
},
)
onKeyStroke(["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"], (e: KeyboardEvent) => {
if (!codePanel.value) return
// 焦点在输入框里(弹框底下那几个筛选框)时不抢方向键
const target = e.target as HTMLElement | null
if (
target &&
(target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable)
) {
return
}
e.preventDefault()
moveCodePanel(e.key === "ArrowUp" || e.key === "ArrowLeft" ? -1 : 1)
})
function showScoreDetail(id: string) {
selectedFlowchartId.value = id
@@ -348,14 +333,7 @@ watchDebounced(() => [query.username, query.problem], listSubmissions, {
// 监听其他查询条件变化
watch(
() => [
query.page,
query.limit,
query.myself,
query.result,
query.language,
query.today,
],
() => [query.page, query.limit, query.myself, query.result, query.language, query.today],
listSubmissions,
)
@@ -427,8 +405,7 @@ const columns = computed(() => {
type: "info",
bordered: false,
style: { cursor: "pointer", flexShrink: 0 },
onClick: () =>
window.open("/problemset/" + problemSet.id, "_blank"),
onClick: () => window.open("/problemset/" + problemSet.id, "_blank"),
},
() => "题单 " + problemSet.title,
),
@@ -442,10 +419,7 @@ const columns = computed(() => {
render: (row) => LANGUAGE_SHOW_VALUE[row.language],
},
{
title: renderTableTitle(
"用户",
"streamline-emojis:smiling-face-with-sunglasses",
),
title: renderTableTitle("用户", "streamline-emojis:smiling-face-with-sunglasses"),
key: "username",
minWidth: 200,
render: (row) =>
@@ -525,10 +499,7 @@ const flowchartColumns = computed(() => {
},
},
{
title: renderTableTitle(
"评分",
"streamline-ultimate-color:analytics-bars-3d",
),
title: renderTableTitle("评分", "streamline-ultimate-color:analytics-bars-3d"),
key: "ai_score",
// 只有评完的才有分数。没评完也渲染 Grade 的话会显示成 0 分,
// 看着像「评了但得了 0 分」
@@ -541,10 +512,7 @@ const flowchartColumns = computed(() => {
: h(NText, { depth: 3 }, () => "—"),
},
{
title: renderTableTitle(
"用户",
"streamline-emojis:smiling-face-with-sunglasses",
),
title: renderTableTitle("用户", "streamline-emojis:smiling-face-with-sunglasses"),
key: "username",
minWidth: 200,
render: (row) =>
@@ -591,26 +559,16 @@ const flowchartColumns = computed(() => {
<n-space>
<n-form :show-feedback="false" inline label-placement="left">
<n-form-item v-if="isDesktop && userStore.isAuthed" label="只看自己">
<n-switch
v-model:value="query.myself"
checked-value="1"
unchecked-value="0"
/>
<n-switch v-model:value="query.myself" checked-value="1" unchecked-value="0" />
</n-form-item>
<n-form-item label="语言" v-if="route.name !== 'contest submissions'">
<n-select
class="select"
v-model:value="query.language"
:options="languageOptions"
/>
<n-select class="select" v-model:value="query.language" :options="languageOptions" />
</n-form-item>
<n-form-item :label="query.language === 'Flowchart' ? '等级' : '状态'">
<n-select
class="select"
v-model:value="query.result"
:options="
query.language === 'Flowchart' ? gradeOptions : resultOptions
"
:options="query.language === 'Flowchart' ? gradeOptions : resultOptions"
/>
</n-form-item>
</n-form>
@@ -635,28 +593,16 @@ const flowchartColumns = computed(() => {
</n-form>
<n-form :show-feedback="false" inline label-placement="left">
<n-form-item v-if="isMobile && userStore.isAuthed" label="只看自己">
<n-switch
v-model:value="query.myself"
checked-value="1"
unchecked-value="0"
/>
<n-switch v-model:value="query.myself" checked-value="1" unchecked-value="0" />
</n-form-item>
<n-form-item>
<n-button @click="search(query.username, query.problem)">
搜索
</n-button>
<n-button @click="search(query.username, query.problem)"> 搜索 </n-button>
</n-form-item>
<n-form-item>
<n-button @click="clear" quaternary>重置</n-button>
</n-form-item>
<n-form-item
v-if="userStore.isTeacherOrAbove && route.name === 'submissions'"
>
<n-button
quaternary
type="warning"
@click="toggleStatisticPanel(true)"
>
<n-form-item v-if="userStore.isTeacherOrAbove && route.name === 'submissions'">
<n-button quaternary type="warning" @click="toggleStatisticPanel(true)">
数据统计
</n-button>
</n-form-item>
@@ -704,31 +650,21 @@ const flowchartColumns = computed(() => {
:loading="showLoading"
/>
</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" />
<n-modal
v-if="userStore.isTeacherOrAbove"
v-model:show="statisticPanel"
preset="card"
:style="{ maxWidth: isDesktop && '800px', maxHeight: '80vh' }"
:content-style="{ overflow: 'auto' }"
:title="
query.language === 'Flowchart' ? '流程图提交的统计' : '提交记录的统计'
"
:title="query.language === 'Flowchart' ? '流程图提交的统计' : '提交记录的统计'"
>
<FlowchartStatisticsPanel
v-if="query.language === 'Flowchart'"
:problem="query.problem"
:username="query.username"
/>
<StatisticsPanel
v-else
:problem="query.problem"
:username="query.username"
/>
<StatisticsPanel v-else :problem="query.problem" :username="query.username" />
</n-modal>
<n-modal
v-model:show="todayPanel"
@@ -777,10 +713,7 @@ const flowchartColumns = computed(() => {
<template #header>
<n-flex align="center">
<n-text>流程图评分详情</n-text>
<n-text
v-if="selectedFlowchart"
:type="getGradeType(selectedFlowchart.aiGrade ?? '')"
>
<n-text v-if="selectedFlowchart" :type="getGradeType(selectedFlowchart.aiGrade ?? '')">
{{ selectedFlowchart.aiScore }}分 {{ selectedFlowchart.aiGrade }}级
</n-text>
</n-flex>