show flowchart submission detail
Some checks failed
Deploy / deploy (push) Has been cancelled

This commit is contained in:
2025-12-29 23:22:15 +08:00
parent 76994d42b3
commit 04838dd9dd
4 changed files with 229 additions and 3 deletions

View File

@@ -2,7 +2,9 @@
<n-button v-if="showLink" type="info" text @click="goto"> <n-button v-if="showLink" type="info" text @click="goto">
{{ flowchart.id.slice(0, 12) }} {{ flowchart.id.slice(0, 12) }}
</n-button> </n-button>
<n-text v-else>{{ flowchart.id.slice(0, 12) }}</n-text> <n-text v-else class="flowchart-id" @click="handleClick">
{{ flowchart.id.slice(0, 12) }}
</n-text>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useUserStore } from "shared/store/user" import { useUserStore } from "shared/store/user"
@@ -15,11 +17,24 @@ interface Props {
} }
const props = defineProps<Props>() const props = defineProps<Props>()
const emit = defineEmits<{
showDetail: [id: string]
}>()
const showLink = computed(() => { const showLink = computed(() => {
if (!userStore.isAuthed) return false if (!userStore.isAuthed) return false
if (userStore.isSuperAdmin) return true if (userStore.isSuperAdmin) return true
return props.flowchart.username === userStore.user?.username return props.flowchart.username === userStore.user?.username
}) })
function goto() {} function goto() {
emit("showDetail", props.flowchart.id)
}
function handleClick() {
emit("showDetail", props.flowchart.id)
}
</script> </script>
<style scoped>
</style>

View File

@@ -0,0 +1,148 @@
<template>
<n-grid v-if="submission" :cols="5" :x-gap="16">
<!-- 左侧流程图预览区域 -->
<n-gi :span="3">
<n-card title="流程图预览">
<div class="flowchart">
<n-alert v-if="renderError" type="error" title="流程图渲染失败">
{{ renderError }}
</n-alert>
<div class="flowchart" v-else ref="mermaidContainer"></div>
</div>
</n-card>
</n-gi>
<!-- 右侧评分详情区域 -->
<n-gi :span="2">
<!-- AI反馈 -->
<n-card
v-if="submission.ai_feedback"
size="small"
title="AI反馈"
style="margin-bottom: 16px"
>
<n-text>{{ submission.ai_feedback }}</n-text>
</n-card>
<!-- 改进建议 -->
<n-card
v-if="submission.ai_suggestions"
size="small"
title="改进建议"
style="margin-bottom: 16px"
>
<n-text>{{ submission.ai_suggestions }}</n-text>
</n-card>
<!-- 详细评分 -->
<n-card
v-if="
submission.ai_criteria_details &&
Object.keys(submission.ai_criteria_details).length > 0
"
size="small"
title="详细评分"
>
<div
v-for="(detail, key) in submission.ai_criteria_details"
:key="key"
style="margin-bottom: 12px"
>
<!-- 评分项标题和分数 -->
<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
>
{{ detail.score || 0 }}分 / {{ detail.max }}分
</n-tag>
</n-flex>
<!-- 评分项详细说明 -->
<n-text v-if="detail.comment" depth="3" style="font-size: 12px">
{{ detail.comment }}
</n-text>
</div>
</n-card>
</n-gi>
</n-grid>
<n-spin v-else :show="loading" class="loading-container">
</n-spin>
</template>
<script setup lang="ts">
import { FlowchartSubmission } from "utils/types"
import { useMermaid } from "shared/composables/useMermaid"
interface Props {
submissionId: string
}
const props = defineProps<Props>()
const mermaidContainer = useTemplateRef<HTMLElement>("mermaidContainer")
const { renderError, renderFlowchart } = useMermaid()
const submission = ref<FlowchartSubmission | null>(null)
const loading = ref(false)
const rendering = ref(false)
function getPercentType(percent: number) {
if (percent >= 0.8) return "primary"
else if (percent >= 0.6) return "info"
else if (percent >= 0.4) return "warning"
return "error"
}
async function loadSubmission() {
if (!props.submissionId) return
loading.value = true
try {
const { getFlowchartSubmission } = await import("oj/api")
const res = await getFlowchartSubmission(props.submissionId)
submission.value = res.data
// 渲染流程图
if (submission.value?.mermaid_code) {
rendering.value = true
await nextTick()
await renderFlowchart(
mermaidContainer.value,
submission.value.mermaid_code,
)
rendering.value = false
}
} catch (error) {
console.error("Failed to load submission:", error)
} finally {
loading.value = false
}
}
watch(() => props.submissionId, loadSubmission, { immediate: true })
</script>
<style scoped>
.flowchart {
height: 500px;
display: flex;
justify-content: center;
align-items: center;
}
/* 确保 SVG 图表占满容器 */
:deep(.flowchart > svg) {
height: 100%;
}
.loading-container {
min-height: 600px;
display: flex;
justify-content: center;
align-items: center;
}
</style>

View File

@@ -30,12 +30,15 @@ const message = useMessage()
const { isMobile, isDesktop } = useBreakpoints() const { isMobile, isDesktop } = useBreakpoints()
const submission = ref<Submission>() const submission = ref<Submission>()
const loading = ref(false)
async function init() { async function init() {
submission.value = props.submission submission.value = props.submission
if (submission.value) return if (submission.value) return
loading.value = true
const res = await getSubmission(props.submissionID) const res = await getSubmission(props.submissionID)
submission.value = res.data submission.value = res.data
loading.value = false
} }
const columns: DataTableColumn<Submission["info"]["data"][number]>[] = [ const columns: DataTableColumn<Submission["info"]["data"][number]>[] = [
@@ -144,6 +147,8 @@ onMounted(init)
:data="submission.info.data" :data="submission.info.data"
/> />
</n-flex> </n-flex>
<n-spin v-else :show="loading" class="loading-container">
</n-spin>
</template> </template>
<style scoped> <style scoped>
@@ -151,4 +156,10 @@ onMounted(init)
font-size: 20px; font-size: 20px;
overflow: auto; overflow: auto;
} }
.loading-container {
min-height: 200px;
display: flex;
justify-content: center;
align-items: center;
}
</style> </style>

View File

@@ -26,6 +26,7 @@ import SubmissionLink from "./components/SubmissionLink.vue"
import SubmissionDetail from "./detail.vue" import SubmissionDetail from "./detail.vue"
import Grade from "./components/Grade.vue" import Grade from "./components/Grade.vue"
import FlowchartLink from "./components/FlowchartLink.vue" import FlowchartLink from "./components/FlowchartLink.vue"
import FlowchartScoreDetail from "./components/FlowchartScoreDetail.vue"
interface SubmissionQuery { interface SubmissionQuery {
username: string username: string
@@ -61,6 +62,11 @@ const [statisticPanel, toggleStatisticPanel] = useToggle(false)
const [flowchartStatisticPanel, toggleFlowchartStatisticPanel] = const [flowchartStatisticPanel, toggleFlowchartStatisticPanel] =
useToggle(false) useToggle(false)
const [codePanel, toggleCodePanel] = useToggle(false) const [codePanel, toggleCodePanel] = useToggle(false)
const [scoreDetailPanel, toggleScoreDetailPanel] = useToggle(false)
const selectedFlowchartId = ref("")
const selectedFlowchart = computed(() => {
return flowcharts.value.find((f) => f.id === selectedFlowchartId.value)
})
const resultOptions: SelectOption[] = [ const resultOptions: SelectOption[] = [
{ label: "全部", value: "" }, { label: "全部", value: "" },
@@ -150,6 +156,28 @@ function showCodePanel(id: string, problem: string) {
problemDisplayID.value = problem problemDisplayID.value = problem
} }
function showScoreDetail(id: string) {
selectedFlowchartId.value = id
toggleScoreDetailPanel(true)
}
function getGradeType(grade?: string) {
if (!grade) return "default"
if (grade === "S") return "primary"
if (grade === "A") return "info"
if (grade === "B") return "warning"
return "error"
}
function flowchartRowProps(row: FlowchartSubmissionListItem) {
return {
style: "cursor: pointer",
onClick() {
showScoreDetail(row.id)
},
}
}
// 监听用户名和题号变化(防抖) // 监听用户名和题号变化(防抖)
watchDebounced(() => [query.username, query.problem], listSubmissions, { watchDebounced(() => [query.username, query.problem], listSubmissions, {
debounce: 500, debounce: 500,
@@ -260,7 +288,11 @@ const flowchartColumns: DataTableColumn<FlowchartSubmissionListItem>[] = [
{ {
title: renderTableTitle("提交编号", "fluent-emoji-flat:input-numbers"), title: renderTableTitle("提交编号", "fluent-emoji-flat:input-numbers"),
key: "id", key: "id",
render: (row) => h(FlowchartLink, { flowchart: row }), render: (row) =>
h(FlowchartLink, {
flowchart: row,
onShowDetail: (id: string) => showScoreDetail(id),
}),
}, },
{ {
title: renderTableTitle("题目", "streamline-emojis:blossom"), title: renderTableTitle("题目", "streamline-emojis:blossom"),
@@ -397,6 +429,7 @@ const flowchartColumns: DataTableColumn<FlowchartSubmissionListItem>[] = [
:bordered="false" :bordered="false"
:columns="flowchartColumns" :columns="flowchartColumns"
:data="flowcharts" :data="flowcharts"
:row-props="flowchartRowProps"
/> />
<n-data-table <n-data-table
v-else v-else
@@ -433,6 +466,25 @@ const flowchartColumns: DataTableColumn<FlowchartSubmissionListItem>[] = [
hideList hideList
/> />
</n-modal> </n-modal>
<n-modal
v-model:show="scoreDetailPanel"
preset="card"
:style="{ maxWidth: isDesktop && '1000px', maxHeight: '80vh' }"
:content-style="{ overflow: 'auto' }"
>
<template #header>
<n-flex align="center">
<n-text>流程图评分详情</n-text>
<n-text
v-if="selectedFlowchart"
:type="getGradeType(selectedFlowchart.ai_grade)"
>
{{ selectedFlowchart.ai_score }} {{ selectedFlowchart.ai_grade }}
</n-text>
</n-flex>
</template>
<FlowchartScoreDetail :submissionId="selectedFlowchartId" />
</n-modal>
</template> </template>
<style scoped> <style scoped>
.select { .select {