This commit is contained in:
@@ -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>
|
||||||
|
|||||||
148
src/oj/submission/components/FlowchartScoreDetail.vue
Normal file
148
src/oj/submission/components/FlowchartScoreDetail.vue
Normal 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>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user