179
src/oj/problem/components/FlowchartEvaluationDisplay.vue
Normal file
179
src/oj/problem/components/FlowchartEvaluationDisplay.vue
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { VueFlow } from "@vue-flow/core"
|
||||||
|
import { atou } from "utils/functions"
|
||||||
|
import "@vue-flow/core/dist/style.css"
|
||||||
|
import "@vue-flow/core/dist/theme-default.css"
|
||||||
|
|
||||||
|
interface EvaluationResult {
|
||||||
|
score?: number
|
||||||
|
grade?: string
|
||||||
|
feedback?: string
|
||||||
|
suggestions?: string
|
||||||
|
criteriaDetails?: any
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
evaluationResult: EvaluationResult | null
|
||||||
|
myFlowchartZippedStr: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
close: []
|
||||||
|
loadToEditor: [data: any]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const showDetailModal = ref(false)
|
||||||
|
const nodes = ref<any[]>([])
|
||||||
|
const edges = ref<any[]>([])
|
||||||
|
|
||||||
|
// 根据分数获取标签类型
|
||||||
|
const getScoreType = (score: number) => {
|
||||||
|
if (score >= 90) return "success"
|
||||||
|
if (score >= 80) return "info"
|
||||||
|
if (score >= 70) return "warning"
|
||||||
|
return "error"
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDetailModal() {
|
||||||
|
showDetailModal.value = true
|
||||||
|
if (props.myFlowchartZippedStr) {
|
||||||
|
const str = atou(props.myFlowchartZippedStr)
|
||||||
|
const json = JSON.parse(str)
|
||||||
|
nodes.value = json.nodes.map((node: any) => ({
|
||||||
|
...node,
|
||||||
|
position: node.position || { x: 0, y: 0 },
|
||||||
|
}))
|
||||||
|
edges.value = json.edges
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeModal() {
|
||||||
|
showDetailModal.value = false
|
||||||
|
emit("close")
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadToEditor() {
|
||||||
|
if (props.myFlowchartZippedStr) {
|
||||||
|
const str = atou(props.myFlowchartZippedStr)
|
||||||
|
const json = JSON.parse(str)
|
||||||
|
const processedData = {
|
||||||
|
nodes: json.nodes || [],
|
||||||
|
edges: json.edges || [],
|
||||||
|
}
|
||||||
|
emit("loadToEditor", processedData)
|
||||||
|
}
|
||||||
|
closeModal()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<!-- 评分结果显示 -->
|
||||||
|
<n-button
|
||||||
|
secondary
|
||||||
|
v-if="evaluationResult"
|
||||||
|
@click="openDetailModal"
|
||||||
|
:type="getScoreType(evaluationResult.score!)"
|
||||||
|
>
|
||||||
|
{{ evaluationResult.score }}分 {{ evaluationResult.grade }}级
|
||||||
|
</n-button>
|
||||||
|
|
||||||
|
<!-- 详情弹框 -->
|
||||||
|
<n-modal
|
||||||
|
v-model:show="showDetailModal"
|
||||||
|
preset="card"
|
||||||
|
title="评分详情"
|
||||||
|
style="width: 1000px"
|
||||||
|
>
|
||||||
|
<n-grid :cols="5" :x-gap="16">
|
||||||
|
<n-gi :span="3">
|
||||||
|
<n-card title="大致缩略图">
|
||||||
|
<div class="flowchart">
|
||||||
|
<VueFlow
|
||||||
|
:nodes="nodes"
|
||||||
|
:edges="edges"
|
||||||
|
:fit-view-on-init="true"
|
||||||
|
:nodes-draggable="false"
|
||||||
|
:nodes-connectable="false"
|
||||||
|
:elements-selectable="false"
|
||||||
|
:zoom-on-scroll="false"
|
||||||
|
:pan-on-scroll="false"
|
||||||
|
:pan-on-drag="false"
|
||||||
|
:select-nodes-on-drag="false"
|
||||||
|
:delete-key-code="null"
|
||||||
|
:multi-selection-key-code="null"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</n-card>
|
||||||
|
<n-flex style="margin-top: 16px" justify="center">
|
||||||
|
<n-button @click="loadToEditor" type="primary">
|
||||||
|
加载到流程图编辑器
|
||||||
|
</n-button>
|
||||||
|
</n-flex>
|
||||||
|
</n-gi>
|
||||||
|
<n-gi :span="2">
|
||||||
|
<n-card
|
||||||
|
v-if="evaluationResult?.feedback"
|
||||||
|
size="small"
|
||||||
|
title="AI反馈"
|
||||||
|
style="margin-bottom: 16px"
|
||||||
|
>
|
||||||
|
<n-text>{{ evaluationResult.feedback }}</n-text>
|
||||||
|
</n-card>
|
||||||
|
|
||||||
|
<!-- 建议部分 -->
|
||||||
|
<n-card
|
||||||
|
v-if="evaluationResult?.suggestions"
|
||||||
|
size="small"
|
||||||
|
title="改进建议"
|
||||||
|
style="margin-bottom: 16px"
|
||||||
|
>
|
||||||
|
<n-text>{{ evaluationResult.suggestions }}</n-text>
|
||||||
|
</n-card>
|
||||||
|
|
||||||
|
<!-- 详细评分部分 -->
|
||||||
|
<n-card
|
||||||
|
v-if="evaluationResult?.criteriaDetails"
|
||||||
|
size="small"
|
||||||
|
title="详细评分"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(detail, key) in evaluationResult.criteriaDetails"
|
||||||
|
: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="getScoreType(detail.score || 0)"
|
||||||
|
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-modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.flowchart {
|
||||||
|
height: 400px;
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,96 +1,24 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useBreakpoints } from "shared/composables/breakpoints"
|
import { useBreakpoints } from "shared/composables/breakpoints"
|
||||||
import { submitFlowchart, getCurrentProblemFlowchartSubmission } from "oj/api"
|
import { useFlowchartSubmit } from "../composables/useFlowchartSubmit"
|
||||||
import { useProblemStore } from "oj/store/problem"
|
import FlowchartEvaluationDisplay from "./FlowchartEvaluationDisplay.vue"
|
||||||
import { utoa } from "utils/functions"
|
|
||||||
import {
|
|
||||||
useFlowchartWebSocket,
|
|
||||||
type FlowchartEvaluationUpdate,
|
|
||||||
} from "shared/composables/websocket"
|
|
||||||
|
|
||||||
const loading = ref(false)
|
|
||||||
const { isDesktop } = useBreakpoints()
|
const { isDesktop } = useBreakpoints()
|
||||||
const message = useMessage()
|
|
||||||
const problemStore = useProblemStore()
|
|
||||||
const { problem } = toRefs(problemStore)
|
|
||||||
|
|
||||||
// 通过inject获取FlowchartEditor组件的引用
|
// 通过inject获取FlowchartEditor组件的引用
|
||||||
const flowchartEditorRef = inject<any>("flowchartEditorRef")
|
const flowchartEditorRef = inject<any>("flowchartEditorRef")
|
||||||
|
|
||||||
const evaluationResult = ref<{
|
// 使用拆分后的逻辑
|
||||||
score?: number
|
const {
|
||||||
grade?: string
|
evaluationResult,
|
||||||
feedback?: string
|
loading,
|
||||||
suggestions?: string
|
submissionCount,
|
||||||
criteriaDetails?: any
|
myFlowchartZippedStr,
|
||||||
} | null>(null)
|
connect,
|
||||||
const submissionStatus = ref<{
|
disconnect,
|
||||||
status: string
|
checkCurrentSubmissionStatus,
|
||||||
submission_id: string
|
submitFlowchartData,
|
||||||
created_time?: string
|
} = useFlowchartSubmit()
|
||||||
} | null>(null)
|
|
||||||
|
|
||||||
// 弹框状态
|
|
||||||
const showDetailModal = ref(false)
|
|
||||||
|
|
||||||
// 提交次数
|
|
||||||
const submissionCount = ref(0)
|
|
||||||
|
|
||||||
// 处理 WebSocket 消息
|
|
||||||
const handleWebSocketMessage = (data: FlowchartEvaluationUpdate) => {
|
|
||||||
console.log("收到流程图评分更新:", data)
|
|
||||||
|
|
||||||
if (data.type === "flowchart_evaluation_completed") {
|
|
||||||
console.log("处理评分完成消息")
|
|
||||||
loading.value = false
|
|
||||||
evaluationResult.value = {
|
|
||||||
score: data.score,
|
|
||||||
grade: data.grade,
|
|
||||||
feedback: data.feedback,
|
|
||||||
}
|
|
||||||
submissionStatus.value = null // 清除状态
|
|
||||||
message.success(`流程图评分完成!得分: ${data.score}分 (${data.grade}级)`)
|
|
||||||
} else if (data.type === "flowchart_evaluation_failed") {
|
|
||||||
console.log("处理评分失败消息")
|
|
||||||
loading.value = false
|
|
||||||
submissionStatus.value = null // 清除状态
|
|
||||||
message.error(`流程图评分失败: ${data.error}`)
|
|
||||||
} else {
|
|
||||||
console.log("未知的消息类型:", data.type)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建 WebSocket 连接
|
|
||||||
const { connect, disconnect, subscribe, status } = useFlowchartWebSocket(
|
|
||||||
handleWebSocketMessage,
|
|
||||||
)
|
|
||||||
|
|
||||||
// 监听WebSocket状态变化
|
|
||||||
watch(
|
|
||||||
status,
|
|
||||||
(newStatus) => {
|
|
||||||
console.log("WebSocket状态变化:", newStatus)
|
|
||||||
},
|
|
||||||
{ immediate: true },
|
|
||||||
)
|
|
||||||
|
|
||||||
// 检查当前问题的流程图提交状态
|
|
||||||
const checkCurrentSubmissionStatus = async () => {
|
|
||||||
if (!problem.value?.id) return
|
|
||||||
|
|
||||||
const { data } = await getCurrentProblemFlowchartSubmission(problem.value.id)
|
|
||||||
const submission = data.submission
|
|
||||||
submissionCount.value = data.count
|
|
||||||
if (submission && submission.status === 2) {
|
|
||||||
evaluationResult.value = {
|
|
||||||
score: submission.ai_score,
|
|
||||||
grade: submission.ai_grade,
|
|
||||||
feedback: submission.ai_feedback,
|
|
||||||
suggestions: submission.ai_suggestions,
|
|
||||||
criteriaDetails: submission.ai_criteria_details,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 组件挂载时连接 WebSocket 并检查状态
|
// 组件挂载时连接 WebSocket 并检查状态
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@@ -103,191 +31,17 @@ onUnmounted(() => {
|
|||||||
disconnect()
|
disconnect()
|
||||||
})
|
})
|
||||||
|
|
||||||
// 将流程图JSON数据转换为Mermaid格式
|
// 提交函数
|
||||||
const convertToMermaid = (flowchartData: any) => {
|
|
||||||
const { nodes, edges } = flowchartData
|
|
||||||
|
|
||||||
if (!nodes || nodes.length === 0) {
|
|
||||||
return "graph TD\n A[空流程图]"
|
|
||||||
}
|
|
||||||
|
|
||||||
let mermaid = "graph TD\n"
|
|
||||||
|
|
||||||
// 处理节点 - 根据原始类型和自定义标签
|
|
||||||
nodes.forEach((node: any) => {
|
|
||||||
const nodeId = node.id
|
|
||||||
const label = node.data?.customLabel || node.data?.label || "节点"
|
|
||||||
const originalType = node.data?.originalType || node.type
|
|
||||||
|
|
||||||
// 根据节点原始类型确定Mermaid语法
|
|
||||||
switch (originalType) {
|
|
||||||
case "start":
|
|
||||||
mermaid += ` ${nodeId}((${label}))\n`
|
|
||||||
break
|
|
||||||
case "end":
|
|
||||||
mermaid += ` ${nodeId}((${label}))\n`
|
|
||||||
break
|
|
||||||
case "input":
|
|
||||||
// 输入框使用平行四边形
|
|
||||||
mermaid += ` ${nodeId}[/${label}/]\n`
|
|
||||||
break
|
|
||||||
case "output":
|
|
||||||
// 输出框使用平行四边形
|
|
||||||
mermaid += ` ${nodeId}[/${label}/]\n`
|
|
||||||
break
|
|
||||||
case "default":
|
|
||||||
mermaid += ` ${nodeId}[${label}]\n`
|
|
||||||
break
|
|
||||||
case "decision":
|
|
||||||
mermaid += ` ${nodeId}{${label}}\n`
|
|
||||||
break
|
|
||||||
case "loop":
|
|
||||||
// 循环使用菱形
|
|
||||||
mermaid += ` ${nodeId}{${label}}\n`
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
mermaid += ` ${nodeId}[${label}]\n`
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 处理边
|
|
||||||
edges.forEach((edge: any) => {
|
|
||||||
const source = edge.source
|
|
||||||
const target = edge.target
|
|
||||||
const label = edge.label ?? ""
|
|
||||||
|
|
||||||
if (label && label.trim() !== "") {
|
|
||||||
mermaid += ` ${source} -->|${label}| ${target}\n`
|
|
||||||
} else {
|
|
||||||
mermaid += ` ${source} --> ${target}\n`
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 添加样式定义来区分不同类型的节点
|
|
||||||
mermaid += "\n"
|
|
||||||
mermaid +=
|
|
||||||
" classDef startEnd fill:#e1f5fe,stroke:#01579b,stroke-width:2px\n"
|
|
||||||
mermaid += " classDef input fill:#e3f2fd,stroke:#1976d2,stroke-width:2px\n"
|
|
||||||
mermaid +=
|
|
||||||
" classDef output fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px\n"
|
|
||||||
mermaid +=
|
|
||||||
" classDef process fill:#e8f5e8,stroke:#1b5e20,stroke-width:2px\n"
|
|
||||||
mermaid +=
|
|
||||||
" classDef decision fill:#fff3e0,stroke:#e65100,stroke-width:2px\n"
|
|
||||||
mermaid += "\n"
|
|
||||||
|
|
||||||
// 为节点应用样式
|
|
||||||
nodes.forEach((node: any) => {
|
|
||||||
const nodeId = node.id
|
|
||||||
const originalType = node.data?.originalType || node.type
|
|
||||||
|
|
||||||
switch (originalType) {
|
|
||||||
case "start":
|
|
||||||
case "end":
|
|
||||||
mermaid += ` class ${nodeId} startEnd\n`
|
|
||||||
break
|
|
||||||
case "input":
|
|
||||||
mermaid += ` class ${nodeId} input\n`
|
|
||||||
break
|
|
||||||
case "output":
|
|
||||||
mermaid += ` class ${nodeId} output\n`
|
|
||||||
break
|
|
||||||
case "decision":
|
|
||||||
case "loop":
|
|
||||||
mermaid += ` class ${nodeId} decision\n`
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
mermaid += ` class ${nodeId} process\n`
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return mermaid
|
|
||||||
}
|
|
||||||
|
|
||||||
async function submit() {
|
async function submit() {
|
||||||
if (!flowchartEditorRef?.value) return
|
await submitFlowchartData(flowchartEditorRef)
|
||||||
// 获取流程图的JSON数据
|
|
||||||
const flowchartData = flowchartEditorRef.value.getFlowchartData()
|
|
||||||
|
|
||||||
if (flowchartData.nodes.length === 0 || flowchartData.edges.length === 0) {
|
|
||||||
message.error("流程图节点或边不能为空")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const mermaidCode = convertToMermaid(flowchartData)
|
|
||||||
const compressed = utoa(JSON.stringify(flowchartData))
|
|
||||||
|
|
||||||
loading.value = true
|
|
||||||
evaluationResult.value = null // 清除之前的结果
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await submitFlowchart({
|
|
||||||
problem_id: problem.value!.id,
|
|
||||||
mermaid_code: mermaidCode,
|
|
||||||
flowchart_data: {
|
|
||||||
compressed: true,
|
|
||||||
data: compressed,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// 获取提交ID并订阅更新
|
|
||||||
const submissionId = response.data.submission_id
|
|
||||||
|
|
||||||
if (submissionId) {
|
|
||||||
console.log("开始订阅WebSocket更新")
|
|
||||||
subscribe(submissionId)
|
|
||||||
|
|
||||||
// 设置评分状态显示
|
|
||||||
submissionStatus.value = {
|
|
||||||
status: "processing",
|
|
||||||
submission_id: submissionId,
|
|
||||||
created_time: new Date().toISOString(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
message.success("流程图已提交,请耐心等待评分")
|
|
||||||
} catch (error) {
|
|
||||||
loading.value = false
|
|
||||||
message.error("流程图提交失败")
|
|
||||||
console.error("提交流程图失败:", error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据分数获取标签类型
|
// 处理加载到编辑器
|
||||||
const getScoreType = (score: number) => {
|
function handleLoadToEditor(data: any) {
|
||||||
if (score >= 90) return "success"
|
if (flowchartEditorRef?.value) {
|
||||||
if (score >= 80) return "info"
|
flowchartEditorRef.value.setFlowchartData(data)
|
||||||
if (score >= 70) return "warning"
|
|
||||||
return "error"
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据等级获取标签类型
|
|
||||||
const getGradeType = (grade: string) => {
|
|
||||||
switch (grade) {
|
|
||||||
case "S":
|
|
||||||
return "success"
|
|
||||||
case "A":
|
|
||||||
return "info"
|
|
||||||
case "B":
|
|
||||||
return "warning"
|
|
||||||
case "C":
|
|
||||||
return "error"
|
|
||||||
default:
|
|
||||||
return "default"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 格式化时间
|
|
||||||
const formatTime = (timeString: string) => {
|
|
||||||
const date = new Date(timeString)
|
|
||||||
return date.toLocaleString("zh-CN", {
|
|
||||||
year: "numeric",
|
|
||||||
month: "2-digit",
|
|
||||||
day: "2-digit",
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
second: "2-digit",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -301,74 +55,18 @@ const formatTime = (timeString: string) => {
|
|||||||
>
|
>
|
||||||
{{ loading ? "评分中..." : "提交流程图" }}
|
{{ loading ? "评分中..." : "提交流程图" }}
|
||||||
</n-button>
|
</n-button>
|
||||||
|
|
||||||
<!-- 显示提交次数 -->
|
<!-- 显示提交次数 -->
|
||||||
<n-button secondary v-if="submissionCount > 0" type="info">
|
<n-button secondary v-if="submissionCount > 0" type="info">
|
||||||
{{ submissionCount }} 次
|
{{ submissionCount }} 次
|
||||||
</n-button>
|
</n-button>
|
||||||
|
|
||||||
<!-- 评分结果显示 -->
|
<!-- 评分结果显示组件 -->
|
||||||
<n-button
|
<FlowchartEvaluationDisplay
|
||||||
secondary
|
:evaluation-result="evaluationResult"
|
||||||
v-if="evaluationResult"
|
:my-flowchart-zipped-str="myFlowchartZippedStr"
|
||||||
@click="showDetailModal = true"
|
@close="() => {}"
|
||||||
:type="getScoreType(evaluationResult.score!)"
|
@load-to-editor="handleLoadToEditor"
|
||||||
>
|
/>
|
||||||
{{ evaluationResult.score }}分 {{ evaluationResult.grade }}级
|
|
||||||
</n-button>
|
|
||||||
|
|
||||||
<!-- 详情弹框 -->
|
|
||||||
<n-modal
|
|
||||||
v-model:show="showDetailModal"
|
|
||||||
preset="card"
|
|
||||||
title="评分详情"
|
|
||||||
style="width: 500px"
|
|
||||||
>
|
|
||||||
<!-- 反馈部分 -->
|
|
||||||
<n-card
|
|
||||||
v-if="evaluationResult?.feedback"
|
|
||||||
size="small"
|
|
||||||
title="AI反馈"
|
|
||||||
style="margin-bottom: 16px"
|
|
||||||
>
|
|
||||||
<n-text>{{ evaluationResult.feedback }}</n-text>
|
|
||||||
</n-card>
|
|
||||||
|
|
||||||
<!-- 建议部分 -->
|
|
||||||
<n-card
|
|
||||||
v-if="evaluationResult?.suggestions"
|
|
||||||
size="small"
|
|
||||||
title="改进建议"
|
|
||||||
style="margin-bottom: 16px"
|
|
||||||
>
|
|
||||||
<n-text>{{ evaluationResult.suggestions }}</n-text>
|
|
||||||
</n-card>
|
|
||||||
|
|
||||||
<!-- 详细评分部分 -->
|
|
||||||
<n-card
|
|
||||||
v-if="evaluationResult?.criteriaDetails"
|
|
||||||
size="small"
|
|
||||||
title="详细评分"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-for="(detail, key) in evaluationResult.criteriaDetails"
|
|
||||||
: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="getScoreType(detail.score || 0)" 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-modal>
|
|
||||||
</n-flex>
|
</n-flex>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
106
src/oj/problem/composables/useFlowchartSubmission.ts
Normal file
106
src/oj/problem/composables/useFlowchartSubmission.ts
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import { ref, watch } from 'vue'
|
||||||
|
import { useMessage } from 'naive-ui'
|
||||||
|
import {
|
||||||
|
useFlowchartWebSocket,
|
||||||
|
type FlowchartEvaluationUpdate,
|
||||||
|
} from 'shared/composables/websocket'
|
||||||
|
|
||||||
|
export interface EvaluationResult {
|
||||||
|
score?: number
|
||||||
|
grade?: string
|
||||||
|
feedback?: string
|
||||||
|
suggestions?: string
|
||||||
|
criteriaDetails?: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubmissionStatus {
|
||||||
|
status: string
|
||||||
|
submission_id: string
|
||||||
|
created_time?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useFlowchartSubmission() {
|
||||||
|
const message = useMessage()
|
||||||
|
|
||||||
|
const evaluationResult = ref<EvaluationResult | null>(null)
|
||||||
|
const submissionStatus = ref<SubmissionStatus | null>(null)
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
// 处理 WebSocket 消息
|
||||||
|
const handleWebSocketMessage = (data: FlowchartEvaluationUpdate) => {
|
||||||
|
console.log("收到流程图评分更新:", data)
|
||||||
|
|
||||||
|
if (data.type === "flowchart_evaluation_completed") {
|
||||||
|
loading.value = false
|
||||||
|
evaluationResult.value = {
|
||||||
|
score: data.score,
|
||||||
|
grade: data.grade,
|
||||||
|
feedback: data.feedback,
|
||||||
|
}
|
||||||
|
submissionStatus.value = null // 清除状态
|
||||||
|
message.success(`流程图评分完成!得分: ${data.score}分 (${data.grade}级)`)
|
||||||
|
} else if (data.type === "flowchart_evaluation_failed") {
|
||||||
|
console.log("处理评分失败消息")
|
||||||
|
loading.value = false
|
||||||
|
submissionStatus.value = null // 清除状态
|
||||||
|
message.error(`流程图评分失败: ${data.error}`)
|
||||||
|
} else {
|
||||||
|
console.log("未知的消息类型:", data.type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建 WebSocket 连接
|
||||||
|
const { connect, disconnect, subscribe, status } = useFlowchartWebSocket(
|
||||||
|
handleWebSocketMessage,
|
||||||
|
)
|
||||||
|
|
||||||
|
// 监听WebSocket状态变化
|
||||||
|
watch(
|
||||||
|
status,
|
||||||
|
(newStatus) => {
|
||||||
|
console.log("WebSocket状态变化:", newStatus)
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
// 订阅提交更新
|
||||||
|
const subscribeToSubmission = (submissionId: string) => {
|
||||||
|
console.log("开始订阅WebSocket更新")
|
||||||
|
subscribe(submissionId)
|
||||||
|
|
||||||
|
// 设置评分状态显示
|
||||||
|
submissionStatus.value = {
|
||||||
|
status: "processing",
|
||||||
|
submission_id: submissionId,
|
||||||
|
created_time: new Date().toISOString(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除结果
|
||||||
|
const clearResult = () => {
|
||||||
|
evaluationResult.value = null
|
||||||
|
submissionStatus.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置加载状态
|
||||||
|
const setLoading = (value: boolean) => {
|
||||||
|
loading.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置评估结果
|
||||||
|
const setEvaluationResult = (result: EvaluationResult) => {
|
||||||
|
evaluationResult.value = result
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
evaluationResult,
|
||||||
|
submissionStatus,
|
||||||
|
loading,
|
||||||
|
connect,
|
||||||
|
disconnect,
|
||||||
|
subscribeToSubmission,
|
||||||
|
clearResult,
|
||||||
|
setLoading,
|
||||||
|
setEvaluationResult,
|
||||||
|
}
|
||||||
|
}
|
||||||
106
src/oj/problem/composables/useFlowchartSubmit.ts
Normal file
106
src/oj/problem/composables/useFlowchartSubmit.ts
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import { ref } from 'vue'
|
||||||
|
import { useMessage } from 'naive-ui'
|
||||||
|
import { submitFlowchart, getCurrentProblemFlowchartSubmission } from 'oj/api'
|
||||||
|
import { useProblemStore } from 'oj/store/problem'
|
||||||
|
import { atou, utoa } from 'utils/functions'
|
||||||
|
import { useMermaidConverter } from './useMermaidConverter'
|
||||||
|
import { useFlowchartSubmission } from './useFlowchartSubmission'
|
||||||
|
|
||||||
|
export function useFlowchartSubmit() {
|
||||||
|
const message = useMessage()
|
||||||
|
const problemStore = useProblemStore()
|
||||||
|
const { problem } = toRefs(problemStore)
|
||||||
|
|
||||||
|
const { convertToMermaid } = useMermaidConverter()
|
||||||
|
const {
|
||||||
|
evaluationResult,
|
||||||
|
submissionStatus,
|
||||||
|
loading,
|
||||||
|
connect,
|
||||||
|
disconnect,
|
||||||
|
subscribeToSubmission,
|
||||||
|
clearResult,
|
||||||
|
setLoading,
|
||||||
|
setEvaluationResult,
|
||||||
|
} = useFlowchartSubmission()
|
||||||
|
|
||||||
|
// 提交次数
|
||||||
|
const submissionCount = ref(0)
|
||||||
|
|
||||||
|
// 存储流程图数据
|
||||||
|
const myFlowchartZippedStr = ref("")
|
||||||
|
|
||||||
|
// 检查当前问题的流程图提交状态
|
||||||
|
const checkCurrentSubmissionStatus = async () => {
|
||||||
|
if (!problem.value?.id) return
|
||||||
|
|
||||||
|
const { data } = await getCurrentProblemFlowchartSubmission(problem.value.id)
|
||||||
|
const submission = data.submission
|
||||||
|
submissionCount.value = data.count
|
||||||
|
if (submission && submission.status === 2) {
|
||||||
|
myFlowchartZippedStr.value = data.submission.flowchart_data.data
|
||||||
|
setEvaluationResult({
|
||||||
|
score: submission.ai_score,
|
||||||
|
grade: submission.ai_grade,
|
||||||
|
feedback: submission.ai_feedback,
|
||||||
|
suggestions: submission.ai_suggestions,
|
||||||
|
criteriaDetails: submission.ai_criteria_details,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交流程图
|
||||||
|
const submitFlowchartData = async (flowchartEditorRef: any) => {
|
||||||
|
if (!flowchartEditorRef?.value) return
|
||||||
|
|
||||||
|
// 获取流程图的JSON数据
|
||||||
|
const flowchartData = flowchartEditorRef.value.getFlowchartData()
|
||||||
|
|
||||||
|
if (flowchartData.nodes.length === 0 || flowchartData.edges.length === 0) {
|
||||||
|
message.error("流程图节点或边不能为空")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const mermaidCode = convertToMermaid(flowchartData)
|
||||||
|
const compressed = utoa(JSON.stringify(flowchartData))
|
||||||
|
|
||||||
|
setLoading(true)
|
||||||
|
clearResult() // 清除之前的结果
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await submitFlowchart({
|
||||||
|
problem_id: problem.value!.id,
|
||||||
|
mermaid_code: mermaidCode,
|
||||||
|
flowchart_data: {
|
||||||
|
compressed: true,
|
||||||
|
data: compressed,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取提交ID并订阅更新
|
||||||
|
const submissionId = response.data.submission_id
|
||||||
|
|
||||||
|
if (submissionId) {
|
||||||
|
subscribeToSubmission(submissionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
message.success("流程图已提交,请耐心等待评分")
|
||||||
|
} catch (error) {
|
||||||
|
setLoading(false)
|
||||||
|
message.error("流程图提交失败")
|
||||||
|
console.error("提交流程图失败:", error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
evaluationResult,
|
||||||
|
submissionStatus,
|
||||||
|
loading,
|
||||||
|
submissionCount,
|
||||||
|
myFlowchartZippedStr,
|
||||||
|
connect,
|
||||||
|
disconnect,
|
||||||
|
checkCurrentSubmissionStatus,
|
||||||
|
submitFlowchartData,
|
||||||
|
}
|
||||||
|
}
|
||||||
108
src/oj/problem/composables/useMermaidConverter.ts
Normal file
108
src/oj/problem/composables/useMermaidConverter.ts
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
/**
|
||||||
|
* 将流程图JSON数据转换为Mermaid格式
|
||||||
|
*/
|
||||||
|
export function useMermaidConverter() {
|
||||||
|
const convertToMermaid = (flowchartData: any) => {
|
||||||
|
const { nodes, edges } = flowchartData
|
||||||
|
|
||||||
|
if (!nodes || nodes.length === 0) {
|
||||||
|
return "graph TD\n A[空流程图]"
|
||||||
|
}
|
||||||
|
|
||||||
|
let mermaid = "graph TD\n"
|
||||||
|
|
||||||
|
// 处理节点 - 根据原始类型和自定义标签
|
||||||
|
nodes.forEach((node: any) => {
|
||||||
|
const nodeId = node.id
|
||||||
|
const label = node.data?.customLabel || node.data?.label || "节点"
|
||||||
|
const originalType = node.data?.originalType || node.type
|
||||||
|
|
||||||
|
// 根据节点原始类型确定Mermaid语法
|
||||||
|
switch (originalType) {
|
||||||
|
case "start":
|
||||||
|
mermaid += ` ${nodeId}((${label}))\n`
|
||||||
|
break
|
||||||
|
case "end":
|
||||||
|
mermaid += ` ${nodeId}((${label}))\n`
|
||||||
|
break
|
||||||
|
case "input":
|
||||||
|
// 输入框使用平行四边形
|
||||||
|
mermaid += ` ${nodeId}[/${label}/]\n`
|
||||||
|
break
|
||||||
|
case "output":
|
||||||
|
// 输出框使用平行四边形
|
||||||
|
mermaid += ` ${nodeId}[/${label}/]\n`
|
||||||
|
break
|
||||||
|
case "default":
|
||||||
|
mermaid += ` ${nodeId}[${label}]\n`
|
||||||
|
break
|
||||||
|
case "decision":
|
||||||
|
mermaid += ` ${nodeId}{${label}}\n`
|
||||||
|
break
|
||||||
|
case "loop":
|
||||||
|
// 循环使用菱形
|
||||||
|
mermaid += ` ${nodeId}{${label}}\n`
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
mermaid += ` ${nodeId}[${label}]\n`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 处理边
|
||||||
|
edges.forEach((edge: any) => {
|
||||||
|
const source = edge.source
|
||||||
|
const target = edge.target
|
||||||
|
const label = edge.label ?? ""
|
||||||
|
|
||||||
|
if (label && label.trim() !== "") {
|
||||||
|
mermaid += ` ${source} -->|${label}| ${target}\n`
|
||||||
|
} else {
|
||||||
|
mermaid += ` ${source} --> ${target}\n`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 添加样式定义来区分不同类型的节点
|
||||||
|
mermaid += "\n"
|
||||||
|
mermaid +=
|
||||||
|
" classDef startEnd fill:#e1f5fe,stroke:#01579b,stroke-width:2px\n"
|
||||||
|
mermaid += " classDef input fill:#e3f2fd,stroke:#1976d2,stroke-width:2px\n"
|
||||||
|
mermaid +=
|
||||||
|
" classDef output fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px\n"
|
||||||
|
mermaid +=
|
||||||
|
" classDef process fill:#e8f5e8,stroke:#1b5e20,stroke-width:2px\n"
|
||||||
|
mermaid +=
|
||||||
|
" classDef decision fill:#fff3e0,stroke:#e65100,stroke-width:2px\n"
|
||||||
|
mermaid += "\n"
|
||||||
|
|
||||||
|
// 为节点应用样式
|
||||||
|
nodes.forEach((node: any) => {
|
||||||
|
const nodeId = node.id
|
||||||
|
const originalType = node.data?.originalType || node.type
|
||||||
|
|
||||||
|
switch (originalType) {
|
||||||
|
case "start":
|
||||||
|
case "end":
|
||||||
|
mermaid += ` class ${nodeId} startEnd\n`
|
||||||
|
break
|
||||||
|
case "input":
|
||||||
|
mermaid += ` class ${nodeId} input\n`
|
||||||
|
break
|
||||||
|
case "output":
|
||||||
|
mermaid += ` class ${nodeId} output\n`
|
||||||
|
break
|
||||||
|
case "decision":
|
||||||
|
case "loop":
|
||||||
|
mermaid += ` class ${nodeId} decision\n`
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
mermaid += ` class ${nodeId} process\n`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return mermaid
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
convertToMermaid,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
<template v-if="nodeType === 'start'">
|
<template v-if="nodeType === 'start'">
|
||||||
<Handle
|
<Handle
|
||||||
type="source"
|
type="source"
|
||||||
|
id="output"
|
||||||
:position="Position.Bottom"
|
:position="Position.Bottom"
|
||||||
:style="getHandleStyle('#10b981', { bottom: '-10px' })"
|
:style="getHandleStyle('#10b981', { bottom: '-10px' })"
|
||||||
/>
|
/>
|
||||||
@@ -12,6 +13,7 @@
|
|||||||
<template v-else-if="nodeType === 'end'">
|
<template v-else-if="nodeType === 'end'">
|
||||||
<Handle
|
<Handle
|
||||||
type="target"
|
type="target"
|
||||||
|
id="input"
|
||||||
:position="Position.Top"
|
:position="Position.Top"
|
||||||
:style="getHandleStyle('#ef4444', { top: '-10px' })"
|
:style="getHandleStyle('#ef4444', { top: '-10px' })"
|
||||||
/>
|
/>
|
||||||
@@ -21,6 +23,7 @@
|
|||||||
<template v-else-if="nodeType === 'decision'">
|
<template v-else-if="nodeType === 'decision'">
|
||||||
<Handle
|
<Handle
|
||||||
type="target"
|
type="target"
|
||||||
|
id="input"
|
||||||
:position="Position.Top"
|
:position="Position.Top"
|
||||||
:style="getHandleStyle('#f59e0b', { top: '-16px' })"
|
:style="getHandleStyle('#f59e0b', { top: '-16px' })"
|
||||||
/>
|
/>
|
||||||
@@ -122,11 +125,13 @@
|
|||||||
<template v-else>
|
<template v-else>
|
||||||
<Handle
|
<Handle
|
||||||
type="target"
|
type="target"
|
||||||
|
id="input"
|
||||||
:position="Position.Top"
|
:position="Position.Top"
|
||||||
:style="getHandleStyle(nodeConfig.color, { top: '-10px' })"
|
:style="getHandleStyle(nodeConfig.color, { top: '-10px' })"
|
||||||
/>
|
/>
|
||||||
<Handle
|
<Handle
|
||||||
type="source"
|
type="source"
|
||||||
|
id="output"
|
||||||
:position="Position.Bottom"
|
:position="Position.Bottom"
|
||||||
:style="getHandleStyle(nodeConfig.color, { bottom: '-10px' })"
|
:style="getHandleStyle(nodeConfig.color, { bottom: '-10px' })"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -122,6 +122,28 @@ onUnmounted(() => {
|
|||||||
document.removeEventListener('keydown', handleKeyDown)
|
document.removeEventListener('keydown', handleKeyDown)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 加载外部数据到编辑器
|
||||||
|
const setFlowchartData = (data: { nodes: Node[], edges: Edge[] }) => {
|
||||||
|
if (data && data.nodes && data.edges) {
|
||||||
|
// 确保节点数据包含必要的位置信息
|
||||||
|
const processedNodes = data.nodes.map(node => ({
|
||||||
|
...node,
|
||||||
|
position: node.position || { x: 0, y: 0 }
|
||||||
|
}))
|
||||||
|
|
||||||
|
// 确保边数据包含必要的 handle 信息
|
||||||
|
const processedEdges = data.edges.map(edge => ({
|
||||||
|
...edge,
|
||||||
|
sourceHandle: edge.sourceHandle || null,
|
||||||
|
targetHandle: edge.targetHandle || null
|
||||||
|
}))
|
||||||
|
|
||||||
|
nodes.value = processedNodes
|
||||||
|
edges.value = processedEdges
|
||||||
|
saveState(nodes.value, edges.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 暴露节点和边数据给父组件
|
// 暴露节点和边数据给父组件
|
||||||
defineExpose({
|
defineExpose({
|
||||||
nodes,
|
nodes,
|
||||||
@@ -129,7 +151,8 @@ defineExpose({
|
|||||||
getFlowchartData: () => ({
|
getFlowchartData: () => ({
|
||||||
nodes: nodes.value,
|
nodes: nodes.value,
|
||||||
edges: edges.value
|
edges: edges.value
|
||||||
})
|
}),
|
||||||
|
setFlowchartData
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user