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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user