1371
package-lock.json
generated
1371
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/lang-cpp": "^6.0.3",
|
"@codemirror/lang-cpp": "^6.0.3",
|
||||||
"@codemirror/lang-python": "^6.2.1",
|
"@codemirror/lang-python": "^6.2.1",
|
||||||
|
"@vue-flow/background": "^1.3.2",
|
||||||
|
"@vue-flow/controls": "^1.1.3",
|
||||||
|
"@vue-flow/core": "^1.47.0",
|
||||||
|
"@vue-flow/minimap": "^1.5.4",
|
||||||
"@vueuse/core": "^13.9.0",
|
"@vueuse/core": "^13.9.0",
|
||||||
"@vueuse/router": "^13.9.0",
|
"@vueuse/router": "^13.9.0",
|
||||||
"@wangeditor-next/editor": "^5.6.46",
|
"@wangeditor-next/editor": "^5.6.46",
|
||||||
@@ -25,7 +29,9 @@
|
|||||||
"fflate": "^0.8.2",
|
"fflate": "^0.8.2",
|
||||||
"highlight.js": "^11.11.1",
|
"highlight.js": "^11.11.1",
|
||||||
"md-editor-v3": "^6.1.0",
|
"md-editor-v3": "^6.1.0",
|
||||||
|
"mermaid": "^11.12.0",
|
||||||
"naive-ui": "^2.43.1",
|
"naive-ui": "^2.43.1",
|
||||||
|
"nanoid": "^5.1.6",
|
||||||
"normalize.css": "^8.0.1",
|
"normalize.css": "^8.0.1",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.3",
|
||||||
"vue": "^3.5.22",
|
"vue": "^3.5.22",
|
||||||
|
|||||||
@@ -118,6 +118,12 @@ export default defineConfig(({ envMode }) => {
|
|||||||
name: "vendor-charts",
|
name: "vendor-charts",
|
||||||
priority: 60,
|
priority: 60,
|
||||||
},
|
},
|
||||||
|
// Mermaid - 流程图库(按需加载)
|
||||||
|
mermaid: {
|
||||||
|
test: /[\\/]node_modules[\\/]mermaid[\\/]/,
|
||||||
|
name: "vendor-mermaid",
|
||||||
|
priority: 55,
|
||||||
|
},
|
||||||
// ===== 通用层 (10) =====
|
// ===== 通用层 (10) =====
|
||||||
// 其他常用库 - 兜底分组
|
// 其他常用库 - 兜底分组
|
||||||
common: {
|
common: {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
} from "utils/constants"
|
} from "utils/constants"
|
||||||
import download from "utils/download"
|
import download from "utils/download"
|
||||||
import { unique } from "utils/functions"
|
import { unique } from "utils/functions"
|
||||||
import { BlankProblem, LANGUAGE, Tag } from "utils/types"
|
import { BlankProblem, LANGUAGE, Tag, Testcase } from "utils/types"
|
||||||
import {
|
import {
|
||||||
createContestProblem,
|
createContestProblem,
|
||||||
createProblem,
|
createProblem,
|
||||||
@@ -22,6 +22,10 @@ const CodeEditor = defineAsyncComponent(
|
|||||||
() => import("shared/components/CodeEditor.vue"),
|
() => import("shared/components/CodeEditor.vue"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const MermaidEditor = defineAsyncComponent(
|
||||||
|
() => import("shared/components/MermaidEditor.vue"),
|
||||||
|
)
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
problemID?: string
|
problemID?: string
|
||||||
contestID?: string
|
contestID?: string
|
||||||
@@ -50,29 +54,35 @@ const problem = useLocalStorage<BlankProblem>(STORAGE_KEY.ADMIN_PROBLEM, {
|
|||||||
output_description: "",
|
output_description: "",
|
||||||
time_limit: 1000,
|
time_limit: 1000,
|
||||||
memory_limit: 64,
|
memory_limit: 64,
|
||||||
difficulty: "Low",
|
difficulty: "Low" as "Low" | "Mid" | "High",
|
||||||
visible: false,
|
visible: false,
|
||||||
share_submission: false,
|
share_submission: false,
|
||||||
tags: [],
|
tags: [],
|
||||||
languages: ["C", "Python3"],
|
languages: ["Python3", "C"] as LANGUAGE[],
|
||||||
template: {},
|
template: {} as { [key in LANGUAGE]?: string },
|
||||||
samples: [
|
samples: [
|
||||||
{ input: "", output: "" },
|
{ input: "", output: "" },
|
||||||
{ input: "", output: "" },
|
{ input: "", output: "" },
|
||||||
{ input: "", output: "" },
|
{ input: "", output: "" },
|
||||||
],
|
],
|
||||||
test_case_id: "",
|
test_case_id: "",
|
||||||
test_case_score: [],
|
test_case_score: [] as Testcase[],
|
||||||
rule_type: "ACM",
|
rule_type: "ACM",
|
||||||
hint: "",
|
hint: "",
|
||||||
source: "",
|
source: "",
|
||||||
prompt: "",
|
prompt: "",
|
||||||
answers: [],
|
answers: [] as { language: LANGUAGE; code: string }[],
|
||||||
io_mode: {
|
io_mode: {
|
||||||
io_mode: "Standard IO",
|
io_mode: "Standard IO",
|
||||||
input: "input.txt",
|
input: "input.txt",
|
||||||
output: "output.txt",
|
output: "output.txt",
|
||||||
},
|
},
|
||||||
|
contest_id: "",
|
||||||
|
allow_flowchart: false,
|
||||||
|
mermaid_code: "",
|
||||||
|
flowchart_data: {},
|
||||||
|
flowchart_hint: "",
|
||||||
|
show_flowchart: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
// 从服务器来的tag列表
|
// 从服务器来的tag列表
|
||||||
@@ -97,6 +107,9 @@ const currentActiveAnswer = ref<LANGUAGE>("Python3")
|
|||||||
// 给 TextEditor 用
|
// 给 TextEditor 用
|
||||||
const [ready, toggleReady] = useToggle(false)
|
const [ready, toggleReady] = useToggle(false)
|
||||||
|
|
||||||
|
// Mermaid 渲染状态
|
||||||
|
const mermaidRenderSuccess = ref(false)
|
||||||
|
|
||||||
const difficultyOptions: SelectOption[] = [
|
const difficultyOptions: SelectOption[] = [
|
||||||
{ label: "简单", value: "Low" },
|
{ label: "简单", value: "Low" },
|
||||||
{ label: "中等", value: "Mid" },
|
{ label: "中等", value: "Mid" },
|
||||||
@@ -144,6 +157,12 @@ async function getProblemDetail() {
|
|||||||
problem.value.hint = data.hint
|
problem.value.hint = data.hint
|
||||||
problem.value.source = data.source
|
problem.value.source = data.source
|
||||||
problem.value.prompt = data.prompt
|
problem.value.prompt = data.prompt
|
||||||
|
// 流程图相关字段
|
||||||
|
problem.value.allow_flowchart = data.allow_flowchart
|
||||||
|
problem.value.show_flowchart = data.show_flowchart
|
||||||
|
problem.value.mermaid_code = data.mermaid_code ?? ""
|
||||||
|
problem.value.flowchart_hint = data.flowchart_hint ?? ""
|
||||||
|
problem.value.flowchart_data = data.flowchart_data
|
||||||
if (data.answers && data.answers.length) {
|
if (data.answers && data.answers.length) {
|
||||||
problem.value.answers = data.answers
|
problem.value.answers = data.answers
|
||||||
} else {
|
} else {
|
||||||
@@ -229,18 +248,23 @@ function downloadTestcases() {
|
|||||||
download("test_case?problem_id=" + problem.value.id)
|
download("test_case?problem_id=" + problem.value.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mermaid 渲染事件处理
|
||||||
|
function onMermaidRenderSuccess() {
|
||||||
|
mermaidRenderSuccess.value = true
|
||||||
|
}
|
||||||
|
|
||||||
// 题目是否有漏写的
|
// 题目是否有漏写的
|
||||||
function detectProblemCompletion() {
|
async function validateProblem() {
|
||||||
let flag = false
|
let hasErrors = false
|
||||||
// 标题
|
// 标题
|
||||||
if (!problem.value._id || !problem.value.title) {
|
if (!problem.value._id || !problem.value.title) {
|
||||||
message.error("编号或标题没有填写")
|
message.error("编号或标题没有填写")
|
||||||
flag = true
|
hasErrors = true
|
||||||
}
|
}
|
||||||
// 标签
|
// 标签
|
||||||
else if (tags.value.upload.length === 0 && tags.value.select.length === 0) {
|
else if (tags.value.upload.length === 0 && tags.value.select.length === 0) {
|
||||||
message.error("标签没有填写")
|
message.error("标签没有填写")
|
||||||
flag = true
|
hasErrors = true
|
||||||
}
|
}
|
||||||
// 题目
|
// 题目
|
||||||
else if (
|
else if (
|
||||||
@@ -249,12 +273,12 @@ function detectProblemCompletion() {
|
|||||||
!problem.value.output_description
|
!problem.value.output_description
|
||||||
) {
|
) {
|
||||||
message.error("题目或输入或输出没有填写")
|
message.error("题目或输入或输出没有填写")
|
||||||
flag = true
|
hasErrors = true
|
||||||
}
|
}
|
||||||
// 样例
|
// 样例
|
||||||
else if (problem.value.samples.length == 0) {
|
else if (problem.value.samples.length == 0) {
|
||||||
message.error("样例没有填写")
|
message.error("样例没有填写")
|
||||||
flag = true
|
hasErrors = true
|
||||||
}
|
}
|
||||||
// 样例是空的
|
// 样例是空的
|
||||||
else if (
|
else if (
|
||||||
@@ -263,21 +287,34 @@ function detectProblemCompletion() {
|
|||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
message.error("空样例没有删干净")
|
message.error("空样例没有删干净")
|
||||||
flag = true
|
hasErrors = true
|
||||||
}
|
}
|
||||||
// 测试用例
|
// 测试用例
|
||||||
else if (problem.value.test_case_score.length === 0) {
|
else if (problem.value.test_case_score.length === 0) {
|
||||||
message.error("测试用例没有上传")
|
message.error("测试用例没有上传")
|
||||||
flag = true
|
hasErrors = true
|
||||||
} else if (problem.value.languages.length === 0) {
|
} else if (problem.value.languages.length === 0) {
|
||||||
message.error("编程语言没有选择")
|
message.error("编程语言没有选择")
|
||||||
flag = true
|
hasErrors = true
|
||||||
|
}
|
||||||
|
// 流程图验证
|
||||||
|
else if (problem.value.show_flowchart || problem.value.allow_flowchart) {
|
||||||
|
if (
|
||||||
|
!problem.value.mermaid_code ||
|
||||||
|
problem.value.mermaid_code.trim() === ""
|
||||||
|
) {
|
||||||
|
message.error("启用了流程图功能,但流程图代码为空")
|
||||||
|
hasErrors = true
|
||||||
|
} else if (!mermaidRenderSuccess.value) {
|
||||||
|
message.error("Mermaid 代码尚未成功渲染,请检查代码语法")
|
||||||
|
hasErrors = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// 通过了
|
// 通过了
|
||||||
else {
|
else {
|
||||||
flag = false
|
hasErrors = false
|
||||||
}
|
}
|
||||||
return flag
|
return hasErrors
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTemplate() {
|
function getTemplate() {
|
||||||
@@ -308,8 +345,8 @@ function filterAnswers() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function submit() {
|
async function submit() {
|
||||||
const notCompleted = detectProblemCompletion()
|
const hasValidationErrors = await validateProblem()
|
||||||
if (notCompleted) return
|
if (hasValidationErrors) return
|
||||||
filterHint()
|
filterHint()
|
||||||
getTemplate()
|
getTemplate()
|
||||||
filterAnswers()
|
filterAnswers()
|
||||||
@@ -488,14 +525,50 @@ watch(
|
|||||||
placeholder="比如来自某道题的改编等,或者网上的资料"
|
placeholder="比如来自某道题的改编等,或者网上的资料"
|
||||||
/>
|
/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="本题的考察知识点(选填)">
|
<n-form-item label="本题的考察知识点(选填,用于 AI 分析)">
|
||||||
<n-input
|
<n-input
|
||||||
v-model:value="problem.prompt"
|
v-model:value="problem.prompt"
|
||||||
placeholder="这里的内容是方便喂给 AI 进行辅助分析的"
|
placeholder="比如考察选择、循环、算法等知识点"
|
||||||
/>
|
/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</n-form>
|
</n-form>
|
||||||
|
|
||||||
|
<n-divider />
|
||||||
|
|
||||||
|
<h2 class="title">代码区域</h2>
|
||||||
|
|
||||||
|
<n-form inline label-placement="left">
|
||||||
|
<n-form-item label="编程语言">
|
||||||
|
<n-checkbox-group v-model:value="problem.languages">
|
||||||
|
<n-flex align="center">
|
||||||
|
<n-checkbox
|
||||||
|
v-for="(language, index) in languageOptions"
|
||||||
|
:key="index"
|
||||||
|
:value="language.value"
|
||||||
|
:label="language.label"
|
||||||
|
/>
|
||||||
|
</n-flex>
|
||||||
|
</n-checkbox-group>
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item>
|
||||||
|
<n-checkbox
|
||||||
|
v-model:checked="needTemplate"
|
||||||
|
label="预制代码(显示在编辑器中,帮助快速上手)"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item>
|
||||||
|
<n-button
|
||||||
|
v-if="needTemplate"
|
||||||
|
size="small"
|
||||||
|
tertiary
|
||||||
|
type="warning"
|
||||||
|
@click="resetTemplate(currentActiveTemplate)"
|
||||||
|
>
|
||||||
|
重置 {{ LANGUAGE_SHOW_VALUE[currentActiveTemplate] }} 的预制代码
|
||||||
|
</n-button>
|
||||||
|
</n-form-item>
|
||||||
|
</n-form>
|
||||||
|
|
||||||
<n-grid :cols="2" x-gap="20">
|
<n-grid :cols="2" x-gap="20">
|
||||||
<n-gi>
|
<n-gi>
|
||||||
<n-form>
|
<n-form>
|
||||||
@@ -514,7 +587,7 @@ watch(
|
|||||||
v-model:value="answer.code"
|
v-model:value="answer.code"
|
||||||
:language="answer.language"
|
:language="answer.language"
|
||||||
:font-size="16"
|
:font-size="16"
|
||||||
height="200px"
|
height="300px"
|
||||||
/>
|
/>
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
</n-tabs>
|
</n-tabs>
|
||||||
@@ -523,7 +596,7 @@ watch(
|
|||||||
</n-gi>
|
</n-gi>
|
||||||
<n-gi>
|
<n-gi>
|
||||||
<n-form v-if="needTemplate">
|
<n-form v-if="needTemplate">
|
||||||
<n-form-item label="编写代码模板">
|
<n-form-item label="编写预制代码">
|
||||||
<n-tabs
|
<n-tabs
|
||||||
type="segment"
|
type="segment"
|
||||||
default-value="Python3"
|
default-value="Python3"
|
||||||
@@ -538,7 +611,7 @@ watch(
|
|||||||
v-model:value="template[lang]"
|
v-model:value="template[lang]"
|
||||||
:language="lang"
|
:language="lang"
|
||||||
:font-size="16"
|
:font-size="16"
|
||||||
height="200px"
|
height="300px"
|
||||||
/>
|
/>
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
</n-tabs>
|
</n-tabs>
|
||||||
@@ -546,6 +619,36 @@ watch(
|
|||||||
</n-form>
|
</n-form>
|
||||||
</n-gi>
|
</n-gi>
|
||||||
</n-grid>
|
</n-grid>
|
||||||
|
|
||||||
|
<n-divider />
|
||||||
|
|
||||||
|
<h2 class="title">流程图区域</h2>
|
||||||
|
|
||||||
|
<!-- 流程图相关设置 -->
|
||||||
|
<n-form inline label-placement="left">
|
||||||
|
<n-form-item label="允许提交流程图">
|
||||||
|
<n-switch v-model:value="problem.allow_flowchart" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="显示标准流程图">
|
||||||
|
<n-switch v-model:value="problem.show_flowchart" />
|
||||||
|
</n-form-item>
|
||||||
|
</n-form>
|
||||||
|
|
||||||
|
<n-form>
|
||||||
|
<n-form-item label="流程图">
|
||||||
|
<MermaidEditor
|
||||||
|
v-model="problem.mermaid_code"
|
||||||
|
@render-success="onMermaidRenderSuccess"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="流程图提示信息(选填)">
|
||||||
|
<n-input
|
||||||
|
v-model:value="problem.flowchart_hint"
|
||||||
|
placeholder="请输入流程图相关的提示信息,帮助学生理解题目要求"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
</n-form>
|
||||||
|
<n-divider />
|
||||||
<n-alert
|
<n-alert
|
||||||
class="box"
|
class="box"
|
||||||
v-if="problem.test_case_score.length"
|
v-if="problem.test_case_score.length"
|
||||||
@@ -571,39 +674,7 @@ watch(
|
|||||||
</n-flex>
|
</n-flex>
|
||||||
</template>
|
</template>
|
||||||
</n-alert>
|
</n-alert>
|
||||||
<n-space style="margin-bottom: 100px" justify="space-between">
|
<n-flex style="margin-bottom: 120px" align="center" justify="end">
|
||||||
<n-form inline label-placement="left" :show-feedback="false">
|
|
||||||
<n-form-item label="编程语言">
|
|
||||||
<n-checkbox-group v-model:value="problem.languages">
|
|
||||||
<n-flex align="center">
|
|
||||||
<n-checkbox
|
|
||||||
v-for="(language, index) in languageOptions"
|
|
||||||
:key="index"
|
|
||||||
:value="language.value"
|
|
||||||
:label="language.label"
|
|
||||||
/>
|
|
||||||
</n-flex>
|
|
||||||
</n-checkbox-group>
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item>
|
|
||||||
<n-checkbox
|
|
||||||
v-model:checked="needTemplate"
|
|
||||||
label="代码模板(一般用不到)"
|
|
||||||
/>
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item>
|
|
||||||
<n-button
|
|
||||||
v-if="needTemplate"
|
|
||||||
size="small"
|
|
||||||
tertiary
|
|
||||||
type="warning"
|
|
||||||
@click="resetTemplate(currentActiveTemplate)"
|
|
||||||
>
|
|
||||||
重置 {{ LANGUAGE_SHOW_VALUE[currentActiveTemplate] }} 代码模板
|
|
||||||
</n-button>
|
|
||||||
</n-form-item>
|
|
||||||
</n-form>
|
|
||||||
<n-flex align="center">
|
|
||||||
<n-tooltip placement="left">
|
<n-tooltip placement="left">
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<n-button text>温馨提醒</n-button>
|
<n-button text>温馨提醒</n-button>
|
||||||
@@ -621,7 +692,6 @@ watch(
|
|||||||
</div>
|
</div>
|
||||||
<n-button type="primary" @click="submit">提交</n-button>
|
<n-button type="primary" @click="submit">提交</n-button>
|
||||||
</n-flex>
|
</n-flex>
|
||||||
</n-space>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -264,3 +264,34 @@ export function getAIDurationData(end: string, duration: string) {
|
|||||||
export function getAIHeatmapData() {
|
export function getAIHeatmapData() {
|
||||||
return http.get("ai/heatmap")
|
return http.get("ai/heatmap")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 流程图相关API ====================
|
||||||
|
|
||||||
|
export function submitFlowchart(data: {
|
||||||
|
problem_id: number
|
||||||
|
mermaid_code: string
|
||||||
|
flowchart_data?: any
|
||||||
|
}) {
|
||||||
|
return http.post("flowchart/submission", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFlowchartSubmission(id: string) {
|
||||||
|
return http.get("flowchart/submission", {
|
||||||
|
params: { id },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFlowchartSubmissions(params: {
|
||||||
|
user_id?: number
|
||||||
|
problem_id?: number
|
||||||
|
offset?: number
|
||||||
|
limit?: number
|
||||||
|
}) {
|
||||||
|
return http.get("flowchart/submissions", { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function retryFlowchartSubmission(submissionId: string) {
|
||||||
|
return http.post("flowchart/submission/retry", {
|
||||||
|
submission_id: submissionId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
467
src/oj/flowchart/detail.vue
Normal file
467
src/oj/flowchart/detail.vue
Normal file
@@ -0,0 +1,467 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flowchart-submission-detail">
|
||||||
|
<div class="detail-header">
|
||||||
|
<h2>流程图提交详情</h2>
|
||||||
|
<n-space>
|
||||||
|
<n-button @click="refreshDetail" :loading="loading">
|
||||||
|
<Icon icon="mdi:refresh" />
|
||||||
|
刷新
|
||||||
|
</n-button>
|
||||||
|
<n-button
|
||||||
|
v-if="submission?.status === FlowchartSubmissionStatus.FAILED"
|
||||||
|
type="warning"
|
||||||
|
@click="retrySubmission"
|
||||||
|
>
|
||||||
|
<Icon icon="mdi:restart" />
|
||||||
|
重试评分
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="loading" class="loading-container">
|
||||||
|
<n-spin size="large" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="submission" class="detail-content">
|
||||||
|
<!-- 基本信息 -->
|
||||||
|
<n-card title="基本信息" class="info-card">
|
||||||
|
<n-descriptions :column="2" bordered>
|
||||||
|
<n-descriptions-item label="提交ID">
|
||||||
|
{{ submission.id }}
|
||||||
|
</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="用户ID">
|
||||||
|
{{ submission.user }}
|
||||||
|
</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="题目ID">
|
||||||
|
{{ submission.problem }}
|
||||||
|
</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="状态">
|
||||||
|
<n-tag :type="getStatusType(submission.status)">
|
||||||
|
{{ getStatusText(submission.status) }}
|
||||||
|
</n-tag>
|
||||||
|
</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="提交时间">
|
||||||
|
{{ formatTime(submission.create_time) }}
|
||||||
|
</n-descriptions-item>
|
||||||
|
<n-descriptions-item
|
||||||
|
label="评分时间"
|
||||||
|
v-if="submission.evaluation_time"
|
||||||
|
>
|
||||||
|
{{ formatTime(submission.evaluation_time) }}
|
||||||
|
</n-descriptions-item>
|
||||||
|
</n-descriptions>
|
||||||
|
</n-card>
|
||||||
|
|
||||||
|
<!-- AI评分结果 -->
|
||||||
|
<n-card
|
||||||
|
v-if="submission.status === FlowchartSubmissionStatus.COMPLETED"
|
||||||
|
title="AI评分结果"
|
||||||
|
class="score-card"
|
||||||
|
>
|
||||||
|
<div class="score-content">
|
||||||
|
<div class="score-main">
|
||||||
|
<div class="score-value">
|
||||||
|
<span class="score-number">{{
|
||||||
|
submission.ai_score?.toFixed(1) || 0
|
||||||
|
}}</span>
|
||||||
|
<span class="score-unit">分</span>
|
||||||
|
</div>
|
||||||
|
<div class="score-grade">
|
||||||
|
<n-tag :type="getGradeType(submission.ai_grade)" size="large">
|
||||||
|
{{ submission.ai_grade || "N/A" }}
|
||||||
|
</n-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="score-details">
|
||||||
|
<n-descriptions :column="1" size="small">
|
||||||
|
<n-descriptions-item label="AI提供商">
|
||||||
|
{{ submission.ai_provider || "deepseek" }}
|
||||||
|
</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="AI模型">
|
||||||
|
{{ submission.ai_model || "deepseek-chat" }}
|
||||||
|
</n-descriptions-item>
|
||||||
|
<n-descriptions-item
|
||||||
|
label="处理时间"
|
||||||
|
v-if="submission.processing_time"
|
||||||
|
>
|
||||||
|
{{ submission.processing_time.toFixed(2) }}秒
|
||||||
|
</n-descriptions-item>
|
||||||
|
</n-descriptions>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-card>
|
||||||
|
|
||||||
|
<!-- AI反馈 -->
|
||||||
|
<n-card
|
||||||
|
v-if="submission.ai_feedback"
|
||||||
|
title="AI反馈"
|
||||||
|
class="feedback-card"
|
||||||
|
>
|
||||||
|
<div class="feedback-content">
|
||||||
|
<p>{{ submission.ai_feedback }}</p>
|
||||||
|
</div>
|
||||||
|
</n-card>
|
||||||
|
|
||||||
|
<!-- AI建议 -->
|
||||||
|
<n-card
|
||||||
|
v-if="submission.ai_suggestions"
|
||||||
|
title="AI建议"
|
||||||
|
class="suggestions-card"
|
||||||
|
>
|
||||||
|
<div class="suggestions-content">
|
||||||
|
<p>{{ submission.ai_suggestions }}</p>
|
||||||
|
</div>
|
||||||
|
</n-card>
|
||||||
|
|
||||||
|
<!-- 详细评分标准 -->
|
||||||
|
<n-card
|
||||||
|
v-if="
|
||||||
|
submission.ai_criteria_details &&
|
||||||
|
Object.keys(submission.ai_criteria_details).length > 0
|
||||||
|
"
|
||||||
|
title="详细评分标准"
|
||||||
|
class="criteria-card"
|
||||||
|
>
|
||||||
|
<div class="criteria-content">
|
||||||
|
<n-collapse>
|
||||||
|
<n-collapse-item
|
||||||
|
v-for="(detail, key) in submission.ai_criteria_details"
|
||||||
|
:key="key"
|
||||||
|
:title="key"
|
||||||
|
:name="key"
|
||||||
|
>
|
||||||
|
<div class="criteria-detail">
|
||||||
|
<pre>{{ JSON.stringify(detail, null, 2) }}</pre>
|
||||||
|
</div>
|
||||||
|
</n-collapse-item>
|
||||||
|
</n-collapse>
|
||||||
|
</div>
|
||||||
|
</n-card>
|
||||||
|
|
||||||
|
<!-- 流程图预览 -->
|
||||||
|
<n-card title="流程图预览" class="flowchart-card">
|
||||||
|
<div class="flowchart-preview">
|
||||||
|
<div class="mermaid-container" ref="mermaidContainer"></div>
|
||||||
|
</div>
|
||||||
|
</n-card>
|
||||||
|
|
||||||
|
<!-- Mermaid代码 -->
|
||||||
|
<n-card title="Mermaid代码" class="code-card">
|
||||||
|
<div class="code-content">
|
||||||
|
<n-input
|
||||||
|
v-model:value="mermaidCode"
|
||||||
|
type="textarea"
|
||||||
|
:rows="10"
|
||||||
|
readonly
|
||||||
|
placeholder="Mermaid代码将在这里显示"
|
||||||
|
/>
|
||||||
|
<div class="code-actions">
|
||||||
|
<n-space>
|
||||||
|
<n-button @click="copyCode">
|
||||||
|
<Icon icon="mdi:content-copy" />
|
||||||
|
复制代码
|
||||||
|
</n-button>
|
||||||
|
<n-button @click="downloadCode">
|
||||||
|
<Icon icon="mdi:download" />
|
||||||
|
下载文件
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="error-container">
|
||||||
|
<n-result status="error" title="加载失败" description="无法加载提交详情">
|
||||||
|
<template #footer>
|
||||||
|
<n-button @click="refreshDetail">重试</n-button>
|
||||||
|
</template>
|
||||||
|
</n-result>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, nextTick, computed } from "vue"
|
||||||
|
import { useRoute } from "vue-router"
|
||||||
|
import {
|
||||||
|
NCard,
|
||||||
|
NButton,
|
||||||
|
NSpace,
|
||||||
|
NDescriptions,
|
||||||
|
NDescriptionsItem,
|
||||||
|
NTag,
|
||||||
|
NInput,
|
||||||
|
NCollapse,
|
||||||
|
NCollapseItem,
|
||||||
|
NSpin,
|
||||||
|
NResult,
|
||||||
|
useMessage,
|
||||||
|
} from "naive-ui"
|
||||||
|
import { Icon } from "@iconify/vue"
|
||||||
|
import { getFlowchartSubmission, retryFlowchartSubmission } from "../api"
|
||||||
|
import { FlowchartSubmission, FlowchartSubmissionStatus } from "utils/types"
|
||||||
|
import mermaid from "mermaid"
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
const submissionId = computed(() => route.params.id as string)
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const loading = ref(false)
|
||||||
|
const submission = ref<FlowchartSubmission | null>(null)
|
||||||
|
const mermaidCode = ref("")
|
||||||
|
const mermaidContainer = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
|
const message = useMessage()
|
||||||
|
|
||||||
|
// 方法
|
||||||
|
const loadSubmission = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const response = await getFlowchartSubmission(submissionId.value)
|
||||||
|
submission.value = response.data
|
||||||
|
|
||||||
|
// 设置 Mermaid 代码
|
||||||
|
if (submission.value) {
|
||||||
|
mermaidCode.value = submission.value.mermaid_code
|
||||||
|
|
||||||
|
// 渲染 Mermaid 图表
|
||||||
|
await nextTick()
|
||||||
|
if (mermaidContainer.value && submission.value.mermaid_code) {
|
||||||
|
mermaidContainer.value.innerHTML = ""
|
||||||
|
mermaid
|
||||||
|
.render("detail-mermaid", submission.value.mermaid_code)
|
||||||
|
.then(({ svg }) => {
|
||||||
|
if (mermaidContainer.value) {
|
||||||
|
mermaidContainer.value.innerHTML = svg
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("加载提交详情失败:", error)
|
||||||
|
message.error("加载提交详情失败")
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshDetail = () => {
|
||||||
|
loadSubmission()
|
||||||
|
}
|
||||||
|
|
||||||
|
const retrySubmission = async () => {
|
||||||
|
if (!submission.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
await retryFlowchartSubmission(submission.value.id)
|
||||||
|
message.success("重试请求已发送")
|
||||||
|
loadSubmission()
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("重试失败:", error)
|
||||||
|
message.error("重试失败")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusType = (status: number) => {
|
||||||
|
const statusMap: Record<
|
||||||
|
number,
|
||||||
|
"default" | "primary" | "success" | "info" | "warning" | "error"
|
||||||
|
> = {
|
||||||
|
[FlowchartSubmissionStatus.PENDING]: "warning",
|
||||||
|
[FlowchartSubmissionStatus.PROCESSING]: "info",
|
||||||
|
[FlowchartSubmissionStatus.COMPLETED]: "success",
|
||||||
|
[FlowchartSubmissionStatus.FAILED]: "error",
|
||||||
|
}
|
||||||
|
return statusMap[status] || "default"
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusText = (status: number) => {
|
||||||
|
const statusMap: Record<number, string> = {
|
||||||
|
[FlowchartSubmissionStatus.PENDING]: "等待评分",
|
||||||
|
[FlowchartSubmissionStatus.PROCESSING]: "评分中",
|
||||||
|
[FlowchartSubmissionStatus.COMPLETED]: "评分完成",
|
||||||
|
[FlowchartSubmissionStatus.FAILED]: "评分失败",
|
||||||
|
}
|
||||||
|
return statusMap[status] || "未知"
|
||||||
|
}
|
||||||
|
|
||||||
|
const getGradeType = (grade: string | undefined) => {
|
||||||
|
if (!grade) return "default"
|
||||||
|
const gradeMap: Record<
|
||||||
|
string,
|
||||||
|
"default" | "primary" | "success" | "info" | "warning" | "error"
|
||||||
|
> = {
|
||||||
|
S: "success",
|
||||||
|
A: "info",
|
||||||
|
B: "warning",
|
||||||
|
C: "error",
|
||||||
|
}
|
||||||
|
return gradeMap[grade] || "default"
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatTime = (time: string) => {
|
||||||
|
return new Date(time).toLocaleString()
|
||||||
|
}
|
||||||
|
|
||||||
|
const copyCode = async () => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(mermaidCode.value)
|
||||||
|
message.success("代码已复制到剪贴板")
|
||||||
|
} catch (err) {
|
||||||
|
message.error("复制失败")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const downloadCode = () => {
|
||||||
|
const blob = new Blob([mermaidCode.value], { type: "text/plain" })
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
const a = document.createElement("a")
|
||||||
|
a.href = url
|
||||||
|
a.download = `flowchart-${submissionId.value}.mmd`
|
||||||
|
a.click()
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
message.success("文件下载成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生命周期
|
||||||
|
onMounted(() => {
|
||||||
|
loadSubmission()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.flowchart-submission-detail {
|
||||||
|
padding: 20px;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card,
|
||||||
|
.score-card,
|
||||||
|
.feedback-card,
|
||||||
|
.suggestions-card,
|
||||||
|
.criteria-card,
|
||||||
|
.flowchart-card,
|
||||||
|
.code-card {
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-content {
|
||||||
|
display: flex;
|
||||||
|
gap: 30px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-main {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-value {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-number {
|
||||||
|
font-size: 48px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-unit {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-grade {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-details {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-content,
|
||||||
|
.suggestions-content {
|
||||||
|
padding: 15px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-radius: 6px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.criteria-content {
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.criteria-detail {
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: "Courier New", monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flowchart-preview {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px;
|
||||||
|
background: #fafafa;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mermaid-container {
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 300px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
357
src/oj/flowchart/list.vue
Normal file
357
src/oj/flowchart/list.vue
Normal file
@@ -0,0 +1,357 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flowchart-submission-list">
|
||||||
|
<div class="list-header">
|
||||||
|
<h2>流程图提交记录</h2>
|
||||||
|
<n-space>
|
||||||
|
<n-button @click="refreshList" :loading="loading">
|
||||||
|
<Icon icon="mdi:refresh" />
|
||||||
|
刷新
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 筛选器 -->
|
||||||
|
<div class="filters">
|
||||||
|
<n-space>
|
||||||
|
<n-select
|
||||||
|
v-model:value="filters.problem_id"
|
||||||
|
placeholder="选择题目"
|
||||||
|
clearable
|
||||||
|
:options="problemOptions"
|
||||||
|
style="width: 200px"
|
||||||
|
/>
|
||||||
|
<n-select
|
||||||
|
v-model:value="filters.user_id"
|
||||||
|
placeholder="选择用户"
|
||||||
|
clearable
|
||||||
|
:options="userOptions"
|
||||||
|
style="width: 200px"
|
||||||
|
/>
|
||||||
|
<n-select
|
||||||
|
v-model:value="filters.status"
|
||||||
|
placeholder="选择状态"
|
||||||
|
clearable
|
||||||
|
:options="statusOptions"
|
||||||
|
style="width: 150px"
|
||||||
|
/>
|
||||||
|
<n-button type="primary" @click="applyFilters">
|
||||||
|
<Icon icon="mdi:filter" />
|
||||||
|
筛选
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 提交列表 -->
|
||||||
|
<div class="submission-list">
|
||||||
|
<n-data-table
|
||||||
|
:columns="columns"
|
||||||
|
:data="submissions"
|
||||||
|
:loading="loading"
|
||||||
|
:pagination="pagination"
|
||||||
|
@update:page="handlePageChange"
|
||||||
|
@update:page-size="handlePageSizeChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, onMounted, computed, h } from "vue"
|
||||||
|
import { useRouter } from "vue-router"
|
||||||
|
import {
|
||||||
|
NButton,
|
||||||
|
NSpace,
|
||||||
|
NSelect,
|
||||||
|
NDataTable,
|
||||||
|
NTag,
|
||||||
|
useMessage,
|
||||||
|
} from "naive-ui"
|
||||||
|
import { Icon } from "@iconify/vue"
|
||||||
|
import { getFlowchartSubmissions, retryFlowchartSubmission } from "../api"
|
||||||
|
import {
|
||||||
|
FlowchartSubmissionListItem,
|
||||||
|
FlowchartSubmissionStatus,
|
||||||
|
} from "utils/types"
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const loading = ref(false)
|
||||||
|
const submissions = ref<FlowchartSubmissionListItem[]>([])
|
||||||
|
const total = ref(0)
|
||||||
|
const currentPage = ref(1)
|
||||||
|
const pageSize = ref(20)
|
||||||
|
|
||||||
|
// 筛选器
|
||||||
|
const filters = reactive({
|
||||||
|
problem_id: null as number | null,
|
||||||
|
user_id: null as number | null,
|
||||||
|
status: null as number | null,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 选项数据
|
||||||
|
const problemOptions = ref<Array<{ label: string; value: number }>>([])
|
||||||
|
const userOptions = ref<Array<{ label: string; value: number }>>([])
|
||||||
|
const statusOptions = [
|
||||||
|
{ label: "等待评分", value: FlowchartSubmissionStatus.PENDING },
|
||||||
|
{ label: "评分中", value: FlowchartSubmissionStatus.PROCESSING },
|
||||||
|
{ label: "评分完成", value: FlowchartSubmissionStatus.COMPLETED },
|
||||||
|
{ label: "评分失败", value: FlowchartSubmissionStatus.FAILED },
|
||||||
|
]
|
||||||
|
|
||||||
|
const message = useMessage()
|
||||||
|
|
||||||
|
// 分页配置
|
||||||
|
const pagination = computed(() => ({
|
||||||
|
page: currentPage.value,
|
||||||
|
pageSize: pageSize.value,
|
||||||
|
itemCount: total.value,
|
||||||
|
showSizePicker: true,
|
||||||
|
pageSizes: [10, 20, 50, 100],
|
||||||
|
showQuickJumper: true,
|
||||||
|
}))
|
||||||
|
|
||||||
|
// 表格列配置
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: "ID",
|
||||||
|
key: "id",
|
||||||
|
width: 120,
|
||||||
|
render: (row: FlowchartSubmissionListItem) => {
|
||||||
|
return row.id.substring(0, 8) + "..."
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "用户",
|
||||||
|
key: "username",
|
||||||
|
width: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "题目",
|
||||||
|
key: "problem_title",
|
||||||
|
width: 200,
|
||||||
|
ellipsis: {
|
||||||
|
tooltip: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "状态",
|
||||||
|
key: "status",
|
||||||
|
width: 100,
|
||||||
|
render: (row: FlowchartSubmissionListItem) => {
|
||||||
|
const statusMap: Record<
|
||||||
|
number,
|
||||||
|
{
|
||||||
|
text: string
|
||||||
|
type: "default" | "primary" | "success" | "info" | "warning" | "error"
|
||||||
|
}
|
||||||
|
> = {
|
||||||
|
[FlowchartSubmissionStatus.PENDING]: {
|
||||||
|
text: "等待评分",
|
||||||
|
type: "warning",
|
||||||
|
},
|
||||||
|
[FlowchartSubmissionStatus.PROCESSING]: {
|
||||||
|
text: "评分中",
|
||||||
|
type: "info",
|
||||||
|
},
|
||||||
|
[FlowchartSubmissionStatus.COMPLETED]: {
|
||||||
|
text: "评分完成",
|
||||||
|
type: "success",
|
||||||
|
},
|
||||||
|
[FlowchartSubmissionStatus.FAILED]: { text: "评分失败", type: "error" },
|
||||||
|
}
|
||||||
|
const status = statusMap[row.status] || { text: "未知", type: "default" }
|
||||||
|
return h(NTag, { type: status.type }, { default: () => status.text })
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "AI评分",
|
||||||
|
key: "ai_score",
|
||||||
|
width: 100,
|
||||||
|
render: (row: FlowchartSubmissionListItem) => {
|
||||||
|
if (row.ai_score !== null && row.ai_score !== undefined) {
|
||||||
|
return `${row.ai_score.toFixed(1)}分`
|
||||||
|
}
|
||||||
|
return "-"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "等级",
|
||||||
|
key: "ai_grade",
|
||||||
|
width: 80,
|
||||||
|
render: (row: FlowchartSubmissionListItem) => {
|
||||||
|
if (row.ai_grade) {
|
||||||
|
const gradeColors: Record<
|
||||||
|
string,
|
||||||
|
"default" | "primary" | "success" | "info" | "warning" | "error"
|
||||||
|
> = {
|
||||||
|
S: "success",
|
||||||
|
A: "info",
|
||||||
|
B: "warning",
|
||||||
|
C: "error",
|
||||||
|
}
|
||||||
|
return h(
|
||||||
|
NTag,
|
||||||
|
{ type: gradeColors[row.ai_grade] || "default" },
|
||||||
|
{ default: () => row.ai_grade },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return "-"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "处理时间",
|
||||||
|
key: "processing_time",
|
||||||
|
width: 100,
|
||||||
|
render: (row: FlowchartSubmissionListItem) => {
|
||||||
|
if (row.processing_time) {
|
||||||
|
return `${row.processing_time.toFixed(2)}s`
|
||||||
|
}
|
||||||
|
return "-"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "提交时间",
|
||||||
|
key: "create_time",
|
||||||
|
width: 160,
|
||||||
|
render: (row: FlowchartSubmissionListItem) => {
|
||||||
|
return new Date(row.create_time).toLocaleString()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "操作",
|
||||||
|
key: "actions",
|
||||||
|
width: 120,
|
||||||
|
render: (row: FlowchartSubmissionListItem) => {
|
||||||
|
return h(NSpace, null, {
|
||||||
|
default: () => [
|
||||||
|
h(
|
||||||
|
NButton,
|
||||||
|
{
|
||||||
|
size: "small",
|
||||||
|
type: "primary",
|
||||||
|
onClick: () => viewSubmission(row),
|
||||||
|
},
|
||||||
|
{ default: () => "查看" },
|
||||||
|
),
|
||||||
|
row.status === FlowchartSubmissionStatus.FAILED &&
|
||||||
|
h(
|
||||||
|
NButton,
|
||||||
|
{
|
||||||
|
size: "small",
|
||||||
|
type: "warning",
|
||||||
|
onClick: () => retrySubmission(row),
|
||||||
|
},
|
||||||
|
{ default: () => "重试" },
|
||||||
|
),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
// 方法
|
||||||
|
const loadSubmissions = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const params: any = {
|
||||||
|
offset: (currentPage.value - 1) * pageSize.value,
|
||||||
|
limit: pageSize.value,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.problem_id !== null) {
|
||||||
|
params.problem_id = filters.problem_id
|
||||||
|
}
|
||||||
|
if (filters.user_id !== null) {
|
||||||
|
params.user_id = filters.user_id
|
||||||
|
}
|
||||||
|
if (filters.status !== null) {
|
||||||
|
params.status = filters.status
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await getFlowchartSubmissions(params)
|
||||||
|
submissions.value = response.data.results
|
||||||
|
total.value = response.data.total
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("加载提交列表失败:", error)
|
||||||
|
message.error("加载提交列表失败")
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshList = () => {
|
||||||
|
currentPage.value = 1
|
||||||
|
loadSubmissions()
|
||||||
|
}
|
||||||
|
|
||||||
|
const applyFilters = () => {
|
||||||
|
currentPage.value = 1
|
||||||
|
loadSubmissions()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePageChange = (page: number) => {
|
||||||
|
currentPage.value = page
|
||||||
|
loadSubmissions()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePageSizeChange = (size: number) => {
|
||||||
|
pageSize.value = size
|
||||||
|
currentPage.value = 1
|
||||||
|
loadSubmissions()
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewSubmission = (submission: FlowchartSubmissionListItem) => {
|
||||||
|
router.push({
|
||||||
|
name: "flowchart-detail",
|
||||||
|
params: { id: submission.id },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const retrySubmission = async (submission: FlowchartSubmissionListItem) => {
|
||||||
|
try {
|
||||||
|
await retryFlowchartSubmission(submission.id)
|
||||||
|
message.success("重试请求已发送")
|
||||||
|
loadSubmissions()
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("重试失败:", error)
|
||||||
|
message.error("重试失败")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生命周期
|
||||||
|
onMounted(() => {
|
||||||
|
loadSubmissions()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.flowchart-submission-list {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submission-list {
|
||||||
|
background: white;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
64
src/oj/problem/components/ProblemFlowchart.vue
Normal file
64
src/oj/problem/components/ProblemFlowchart.vue
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useProblemStore } from "oj/store/problem"
|
||||||
|
import { nanoid } from "nanoid"
|
||||||
|
|
||||||
|
const problemStore = useProblemStore()
|
||||||
|
const { problem } = storeToRefs(problemStore)
|
||||||
|
const mermaidContainer = useTemplateRef<HTMLElement>("mermaidContainer")
|
||||||
|
|
||||||
|
// 动态导入 mermaid
|
||||||
|
let mermaid: any = null
|
||||||
|
|
||||||
|
// 动态加载 Mermaid
|
||||||
|
const loadMermaid = async () => {
|
||||||
|
if (!mermaid) {
|
||||||
|
const mermaidModule = await import("mermaid")
|
||||||
|
mermaid = mermaidModule.default
|
||||||
|
mermaid.initialize({
|
||||||
|
startOnLoad: false,
|
||||||
|
securityLevel: "loose",
|
||||||
|
theme: "default",
|
||||||
|
flowchart: {
|
||||||
|
useMaxWidth: true,
|
||||||
|
htmlLabels: true,
|
||||||
|
curve: "basis",
|
||||||
|
},
|
||||||
|
sequence: {
|
||||||
|
useMaxWidth: true,
|
||||||
|
},
|
||||||
|
gantt: {
|
||||||
|
useMaxWidth: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return mermaid
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化Mermaid并渲染
|
||||||
|
onMounted(async () => {
|
||||||
|
// 确保 mermaid 已加载
|
||||||
|
await loadMermaid()
|
||||||
|
|
||||||
|
// 渲染流程图
|
||||||
|
if (mermaidContainer.value && problem.value?.mermaid_code) {
|
||||||
|
const id = `mermaid-${nanoid()}`
|
||||||
|
const { svg } = await mermaid.render(id, problem.value.mermaid_code)
|
||||||
|
mermaidContainer.value.innerHTML = svg
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div ref="mermaidContainer" class="container"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
min-height: 300px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -26,6 +26,9 @@ const ProblemSubmission = defineAsyncComponent(
|
|||||||
const ProblemComment = defineAsyncComponent(
|
const ProblemComment = defineAsyncComponent(
|
||||||
() => import("./components/ProblemComment.vue"),
|
() => import("./components/ProblemComment.vue"),
|
||||||
)
|
)
|
||||||
|
const ProblemFlowchart = defineAsyncComponent(
|
||||||
|
() => import("./components/ProblemFlowchart.vue"),
|
||||||
|
)
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
problemID: string
|
problemID: string
|
||||||
@@ -49,6 +52,9 @@ const { isMobile, isDesktop } = useBreakpoints()
|
|||||||
|
|
||||||
const tabOptions = computed(() => {
|
const tabOptions = computed(() => {
|
||||||
const options: string[] = ["content"]
|
const options: string[] = ["content"]
|
||||||
|
if (problem.value?.show_flowchart) {
|
||||||
|
options.push("flowchart")
|
||||||
|
}
|
||||||
if (isMobile.value) {
|
if (isMobile.value) {
|
||||||
options.push("editor")
|
options.push("editor")
|
||||||
}
|
}
|
||||||
@@ -115,6 +121,13 @@ watch(isMobile, (value) => {
|
|||||||
<n-tab-pane name="content" tab="题目描述">
|
<n-tab-pane name="content" tab="题目描述">
|
||||||
<ProblemContent />
|
<ProblemContent />
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
|
<n-tab-pane
|
||||||
|
v-if="problem.show_flowchart"
|
||||||
|
name="flowchart"
|
||||||
|
tab="流程图表"
|
||||||
|
>
|
||||||
|
<ProblemFlowchart />
|
||||||
|
</n-tab-pane>
|
||||||
<n-tab-pane name="info" tab="题目统计">
|
<n-tab-pane name="info" tab="题目统计">
|
||||||
<ProblemInfo />
|
<ProblemInfo />
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
@@ -130,7 +143,10 @@ watch(isMobile, (value) => {
|
|||||||
<n-tab-pane name="content" tab="描述">
|
<n-tab-pane name="content" tab="描述">
|
||||||
<ProblemContent />
|
<ProblemContent />
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
<n-tab-pane name="editor" tab="编辑">
|
<n-tab-pane v-if="problem.show_flowchart" name="flowchart" tab="流程">
|
||||||
|
<ProblemFlowchart />
|
||||||
|
</n-tab-pane>
|
||||||
|
<n-tab-pane name="editor" tab="代码">
|
||||||
<ProblemEditor v-if="shouldUseProblemEditor" />
|
<ProblemEditor v-if="shouldUseProblemEditor" />
|
||||||
<ContestEditor v-else />
|
<ContestEditor v-else />
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
|
|||||||
161
src/shared/components/MermaidEditor.vue
Normal file
161
src/shared/components/MermaidEditor.vue
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { nanoid } from "nanoid"
|
||||||
|
import { copyToClipboard } from "utils/functions"
|
||||||
|
|
||||||
|
// 动态导入 mermaid
|
||||||
|
let mermaid: any = null
|
||||||
|
|
||||||
|
const modelValue = defineModel<string>({ default: "" })
|
||||||
|
const mermaidContainer = useTemplateRef<HTMLElement>("mermaidContainer")
|
||||||
|
const codeEditor = useTemplateRef<HTMLTextAreaElement>("codeEditor")
|
||||||
|
|
||||||
|
// 渲染状态
|
||||||
|
const renderSuccess = ref(false)
|
||||||
|
|
||||||
|
// 定义事件
|
||||||
|
const emit = defineEmits<{
|
||||||
|
renderSuccess: []
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// 动态加载 Mermaid
|
||||||
|
const loadMermaid = async () => {
|
||||||
|
if (!mermaid) {
|
||||||
|
const mermaidModule = await import("mermaid")
|
||||||
|
mermaid = mermaidModule.default
|
||||||
|
mermaid.initialize({
|
||||||
|
startOnLoad: false,
|
||||||
|
securityLevel: "loose",
|
||||||
|
theme: "default",
|
||||||
|
flowchart: {
|
||||||
|
useMaxWidth: true,
|
||||||
|
htmlLabels: true,
|
||||||
|
curve: "basis",
|
||||||
|
},
|
||||||
|
sequence: {
|
||||||
|
useMaxWidth: true,
|
||||||
|
},
|
||||||
|
gantt: {
|
||||||
|
useMaxWidth: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return mermaid
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
onMounted(async () => {
|
||||||
|
await loadMermaid()
|
||||||
|
nextTick(() => {
|
||||||
|
renderMermaid()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听代码变化
|
||||||
|
watch(modelValue, () => {
|
||||||
|
renderMermaid()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 渲染Mermaid图表
|
||||||
|
const renderMermaid = async () => {
|
||||||
|
if (!mermaidContainer.value) {
|
||||||
|
renderSuccess.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 总是先清空容器
|
||||||
|
mermaidContainer.value.innerHTML = ""
|
||||||
|
|
||||||
|
// 如果没有内容,直接返回
|
||||||
|
if (!modelValue.value.trim()) {
|
||||||
|
renderSuccess.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 确保 mermaid 已加载
|
||||||
|
const mermaidInstance = await loadMermaid()
|
||||||
|
const id = `mermaid-${nanoid()}`
|
||||||
|
const { svg } = await mermaidInstance.render(id, modelValue.value)
|
||||||
|
mermaidContainer.value.innerHTML = svg
|
||||||
|
|
||||||
|
// 渲染成功
|
||||||
|
renderSuccess.value = true
|
||||||
|
emit("renderSuccess")
|
||||||
|
} catch (error: any) {
|
||||||
|
const errorMessage = error?.message || "请检查代码语法"
|
||||||
|
renderSuccess.value = false
|
||||||
|
|
||||||
|
mermaidContainer.value.innerHTML = `
|
||||||
|
<div style="color: #ff4d4f; padding: 20px; text-align: center; border: 1px dashed #ff4d4f; border-radius: 4px;">
|
||||||
|
<p>❌ Mermaid语法错误</p>
|
||||||
|
<p style="font-size: 12px; color: #666;">${errorMessage}</p>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空代码
|
||||||
|
const clearCode = () => {
|
||||||
|
modelValue.value = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// 复制代码
|
||||||
|
const copyCode = async () => {
|
||||||
|
copyToClipboard(modelValue.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组件卸载时清空容器
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (mermaidContainer.value) {
|
||||||
|
mermaidContainer.value.innerHTML = ""
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<n-flex>
|
||||||
|
<n-flex vertical>
|
||||||
|
<n-flex align="center">
|
||||||
|
<span>Mermaid 代码</span>
|
||||||
|
<n-flex align="center">
|
||||||
|
<n-button text @click="copyCode" size="small" type="primary"
|
||||||
|
>复制</n-button
|
||||||
|
>
|
||||||
|
<n-button text @click="clearCode" type="error" size="small"
|
||||||
|
>清空</n-button
|
||||||
|
>
|
||||||
|
</n-flex>
|
||||||
|
</n-flex>
|
||||||
|
<n-input
|
||||||
|
class="code-editor"
|
||||||
|
ref="codeEditor"
|
||||||
|
v-model:value="modelValue"
|
||||||
|
type="textarea"
|
||||||
|
:autosize="{ minRows: 10, maxRows: 20 }"
|
||||||
|
/>
|
||||||
|
</n-flex>
|
||||||
|
<n-flex vertical>
|
||||||
|
<n-flex align="center" justify="space-between">
|
||||||
|
<span>图表预览</span>
|
||||||
|
<n-tag v-if="modelValue && renderSuccess" type="success" size="small">
|
||||||
|
✓ 渲染成功
|
||||||
|
</n-tag>
|
||||||
|
</n-flex>
|
||||||
|
<div ref="mermaidContainer" class="mermaid-container"></div>
|
||||||
|
</n-flex>
|
||||||
|
</n-flex>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.code-editor {
|
||||||
|
flex: 1;
|
||||||
|
width: 400px;
|
||||||
|
}
|
||||||
|
.mermaid-container {
|
||||||
|
width: 400px;
|
||||||
|
min-height: 400px;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -127,6 +127,13 @@ export interface Problem {
|
|||||||
contest: number
|
contest: number
|
||||||
my_status: number
|
my_status: number
|
||||||
visible: boolean
|
visible: boolean
|
||||||
|
|
||||||
|
// 流程图相关字段
|
||||||
|
allow_flowchart: boolean
|
||||||
|
mermaid_code?: string
|
||||||
|
flowchart_data?: Record<string, any>
|
||||||
|
flowchart_hint?: string
|
||||||
|
show_flowchart?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AdminProblem = Problem & AlterProblem
|
export type AdminProblem = Problem & AlterProblem
|
||||||
@@ -185,6 +192,45 @@ export interface SubmitCodePayload {
|
|||||||
contest_id?: number
|
contest_id?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 流程图相关类型 ====================
|
||||||
|
|
||||||
|
export const FlowchartSubmissionStatus = {
|
||||||
|
PENDING: 0, // 等待AI评分
|
||||||
|
PROCESSING: 1, // AI评分中
|
||||||
|
COMPLETED: 2, // 评分完成
|
||||||
|
FAILED: 3, // 评分失败
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export interface FlowchartSubmission {
|
||||||
|
id: string
|
||||||
|
user: number
|
||||||
|
problem: number
|
||||||
|
mermaid_code: string
|
||||||
|
flowchart_data: Record<string, any>
|
||||||
|
status: number
|
||||||
|
create_time: string
|
||||||
|
ai_score?: number
|
||||||
|
ai_grade?: string
|
||||||
|
ai_feedback?: string
|
||||||
|
ai_suggestions?: string
|
||||||
|
ai_criteria_details: Record<string, any>
|
||||||
|
ai_provider?: string
|
||||||
|
ai_model?: string
|
||||||
|
processing_time?: number
|
||||||
|
evaluation_time?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 列表接口返回的字段(包含 username 和 problem_title)
|
||||||
|
export interface FlowchartSubmissionListItem extends FlowchartSubmission {
|
||||||
|
username: string
|
||||||
|
problem_title: string
|
||||||
|
}
|
||||||
|
export interface SubmitFlowchartPayload {
|
||||||
|
problem_id: number
|
||||||
|
mermaid_code: string
|
||||||
|
flowchart_data?: Record<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
interface Info {
|
interface Info {
|
||||||
err: string | null
|
err: string | null
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
Reference in New Issue
Block a user