提交流程图
Some checks failed
Deploy / deploy (push) Has been cancelled

This commit is contained in:
2025-10-13 12:31:12 +08:00
parent 2c31acaba7
commit 41819b6d9b
8 changed files with 471 additions and 81 deletions

View File

@@ -19,6 +19,7 @@ function filterResult(result: Problem) {
rate: getACRate(result.accepted_number, result.submission_number), rate: getACRate(result.accepted_number, result.submission_number),
status: "", status: "",
author: result.created_by.username, author: result.created_by.username,
allow_flowchart: result.allow_flowchart,
} }
if (result.my_status === null || result.my_status === undefined) { if (result.my_status === null || result.my_status === undefined) {
newResult.status = "not_test" newResult.status = "not_test"
@@ -270,7 +271,7 @@ export function getAIHeatmapData() {
export function submitFlowchart(data: { export function submitFlowchart(data: {
problem_id: number problem_id: number
mermaid_code: string mermaid_code: string
flowchart_data?: any flowchart_data: any // 这个是压缩之后的,元数据太长了
}) { }) {
return http.post("flowchart/submission", data) return http.post("flowchart/submission", data)
} }
@@ -295,3 +296,9 @@ export function retryFlowchartSubmission(submissionId: string) {
submission_id: submissionId, submission_id: submissionId,
}) })
} }
export function getCurrentProblemFlowchartSubmission(problemId: number) {
return http.get("flowchart/submission/current", {
params: { problem_id: problemId },
})
}

View File

@@ -0,0 +1,17 @@
<script lang="ts" setup>
import { ProblemFiltered } from "utils/types"
import { Icon } from "@iconify/vue"
defineProps<{
problem: ProblemFiltered
}>()
</script>
<template>
<n-flex align="center">
<span>{{ problem.title }}</span>
<Icon
v-if="problem.allow_flowchart"
icon="streamline-freehand-color:programming-flowchart"
/>
</n-flex>
</template>

View File

@@ -1,12 +1,108 @@
<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 { useProblemStore } from "oj/store/problem"
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 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
grade?: string
feedback?: string
suggestions?: string
criteriaDetails?: any
} | null>(null)
const submissionStatus = ref<{
status: string
submission_id: string
created_time?: string
} | 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 并检查状态
onMounted(() => {
connect()
checkCurrentSubmissionStatus()
})
// 组件卸载时断开连接
onUnmounted(() => {
disconnect()
})
// 将流程图JSON数据转换为Mermaid格式 // 将流程图JSON数据转换为Mermaid格式
const convertToMermaid = (flowchartData: any) => { const convertToMermaid = (flowchartData: any) => {
const { nodes, edges } = flowchartData const { nodes, edges } = flowchartData
@@ -26,16 +122,18 @@ const convertToMermaid = (flowchartData: any) => {
// 根据节点原始类型确定Mermaid语法 // 根据节点原始类型确定Mermaid语法
switch (originalType) { switch (originalType) {
case "start": case "start":
mermaid += ` ${nodeId}[${label}]\n` mermaid += ` ${nodeId}((${label}))\n`
break break
case "end": case "end":
mermaid += ` ${nodeId}[${label}]\n` mermaid += ` ${nodeId}((${label}))\n`
break break
case "input": case "input":
mermaid += ` ${nodeId}[${label}]\n` // 输入框使用平行四边形
mermaid += ` ${nodeId}[/${label}/]\n`
break break
case "output": case "output":
mermaid += ` ${nodeId}[${label}]\n` // 输出框使用平行四边形
mermaid += ` ${nodeId}[/${label}/]\n`
break break
case "default": case "default":
mermaid += ` ${nodeId}[${label}]\n` mermaid += ` ${nodeId}[${label}]\n`
@@ -44,7 +142,8 @@ const convertToMermaid = (flowchartData: any) => {
mermaid += ` ${nodeId}{${label}}\n` mermaid += ` ${nodeId}{${label}}\n`
break break
case "loop": case "loop":
mermaid += ` ${nodeId}[${label}]\n` // 循环使用菱形
mermaid += ` ${nodeId}{${label}}\n`
break break
default: default:
mermaid += ` ${nodeId}[${label}]\n` mermaid += ` ${nodeId}[${label}]\n`
@@ -55,19 +154,57 @@ const convertToMermaid = (flowchartData: any) => {
edges.forEach((edge: any) => { edges.forEach((edge: any) => {
const source = edge.source const source = edge.source
const target = edge.target const target = edge.target
const label = edge.label || edge.data?.label || "" const label = edge.label ?? ""
if (label) { if (label && label.trim() !== "") {
mermaid += ` ${source} -->|${label}| ${target}\n` mermaid += ` ${source} -->|${label}| ${target}\n`
} else { } else {
mermaid += ` ${source} --> ${target}\n` 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 mermaid
} }
const submit = () => { async function submit() {
if (!flowchartEditorRef?.value) return if (!flowchartEditorRef?.value) return
// 获取流程图的JSON数据 // 获取流程图的JSON数据
const flowchartData = flowchartEditorRef.value.getFlowchartData() const flowchartData = flowchartEditorRef.value.getFlowchartData()
@@ -76,26 +213,162 @@ const submit = () => {
message.error("流程图节点或边不能为空") message.error("流程图节点或边不能为空")
return return
} }
// 打印JSON数据到控制台
console.log("流程图JSON数据:", JSON.stringify(flowchartData, null, 2))
// 转换为Mermaid格式
const mermaidCode = convertToMermaid(flowchartData) const mermaidCode = convertToMermaid(flowchartData)
console.log("Mermaid代码:") const compressed = utoa(JSON.stringify(flowchartData))
console.log(mermaidCode)
// 显示成功消息 loading.value = true
message.success("敬请期待,快了~") 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) => {
if (score >= 90) return "success"
if (score >= 80) return "info"
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>
<n-flex align="center">
<n-button <n-button
:size="isDesktop ? 'medium' : 'small'" :size="isDesktop ? 'medium' : 'small'"
type="primary" type="primary"
:loading="loading"
:disabled="loading"
@click="submit" @click="submit"
> >
提交流程图 {{ loading ? "评分中..." : "提交流程图" }}
</n-button> </n-button>
<!-- 显示提交次数 -->
<n-button secondary v-if="submissionCount > 0" type="info">
{{ submissionCount }}
</n-button>
<!-- 评分结果显示 -->
<n-button
secondary
v-if="evaluationResult"
@click="showDetailModal = true"
:type="getScoreType(evaluationResult.score!)"
>
{{ 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>
</template> </template>

View File

@@ -14,6 +14,7 @@ import { useUserStore } from "shared/store/user"
import { renderTableTitle } from "utils/renders" import { renderTableTitle } from "utils/renders"
import ProblemStatus from "./components/ProblemStatus.vue" import ProblemStatus from "./components/ProblemStatus.vue"
import AuthorSelect from "shared/components/AuthorSelect.vue" import AuthorSelect from "shared/components/AuthorSelect.vue"
import ProblemListTitle from "./components/ProblemListTitle.vue"
interface Tag { interface Tag {
id: number id: number
@@ -146,6 +147,7 @@ const baseColumns: DataTableColumn<ProblemFiltered>[] = [
title: renderTableTitle("题目", "streamline-emojis:watermelon-2"), title: renderTableTitle("题目", "streamline-emojis:watermelon-2"),
key: "title", key: "title",
minWidth: 200, minWidth: 200,
render: (row) => h(ProblemListTitle, { problem: row }),
}, },
{ {
title: renderTableTitle("难度", "streamline-emojis:lady-beetle"), title: renderTableTitle("难度", "streamline-emojis:lady-beetle"),

View File

@@ -17,40 +17,11 @@ export const useProblemStore = defineStore("problem", () => {
return problem.value?.languages ?? [] return problem.value?.languages ?? []
}) })
// ==================== 操作 ====================
/**
* 设置当前题目
*/
function setProblem(newProblem: Problem | null) {
problem.value = newProblem
}
/**
* 清空当前题目
*/
function clearProblem() {
problem.value = null
}
/**
* 更新题目的部分字段
*/
function updateProblem(updates: Partial<Problem>) {
if (problem.value) {
problem.value = { ...problem.value, ...updates }
}
}
return { return {
// 状态 // 状态
problem, problem,
// 计算属性 // 计算属性
languages, languages,
// 操作
setProblem,
clearProblem,
updateProblem,
} }
}) })

View File

@@ -1,10 +1,7 @@
import { nanoid } from "nanoid" import { nanoid } from "nanoid"
import type { Ref } from 'vue' import type { Ref } from "vue"
import type { Node, Edge } from '@vue-flow/core' import type { Node, Edge } from "@vue-flow/core"
/**
* 简化的流程操作
*/
export function useFlowOperations( export function useFlowOperations(
nodes: Ref<Node[]>, nodes: Ref<Node[]>,
edges: Ref<Edge[]>, edges: Ref<Edge[]>,
@@ -12,24 +9,77 @@ export function useFlowOperations(
addEdges: (edges: Edge[]) => void, addEdges: (edges: Edge[]) => void,
removeNodes: (nodeIds: string[]) => void, removeNodes: (nodeIds: string[]) => void,
removeEdges: (edgeIds: string[]) => void, removeEdges: (edgeIds: string[]) => void,
saveState: (nodes: Node[], edges: Edge[]) => void saveState: (nodes: Node[], edges: Edge[]) => void,
) { ) {
// 根据节点类型和handle自动推断标签
const getAutoLabel = (
sourceNode: any,
targetNode: any,
sourceHandle: string,
targetHandle: string,
) => {
const sourceType = sourceNode?.data?.originalType || sourceNode?.type
const targetType = targetNode?.data?.originalType || targetNode?.type
// 如果是判断节点
if (sourceType === "decision") {
// 根据handle ID推断标签
if (sourceHandle === "yes") {
return "是"
} else if (sourceHandle === "no") {
return "否"
}
}
// 如果是循环节点
if (sourceType === "loop") {
// 根据handle ID推断标签
if (sourceHandle === "continue") {
return "继续"
} else if (sourceHandle === "exit") {
return "退出"
}
}
// 如果是循环体回到循环节点
if (targetType === "loop") {
if (targetHandle === "return") {
return "返回"
}
}
// 默认情况
return ""
}
// 连接处理 // 连接处理
const handleConnect = (params: any) => { const handleConnect = (params: any) => {
// 获取源节点和目标节点
const sourceNode = nodes.value.find((node) => node.id === params.source)
const targetNode = nodes.value.find((node) => node.id === params.target)
// 自动推断标签
const autoLabel = getAutoLabel(
sourceNode,
targetNode,
params.sourceHandle,
params.targetHandle,
)
const newEdge: Edge = { const newEdge: Edge = {
id: `edge-${nanoid()}`, id: `edge-${nanoid()}`,
source: params.source, source: params.source,
target: params.target, target: params.target,
sourceHandle: params.sourceHandle, sourceHandle: params.sourceHandle,
targetHandle: params.targetHandle, targetHandle: params.targetHandle,
type: 'default' type: "default",
label: autoLabel,
} }
addEdges([newEdge]) addEdges([newEdge])
saveState(nodes.value, edges.value) saveState(nodes.value, edges.value)
} }
// 边点击删除 // 边点击处理 - 单击删除
const handleEdgeClick = (event: any) => { const handleEdgeClick = (event: any) => {
removeEdges([event.edge.id]) removeEdges([event.edge.id])
saveState(nodes.value, edges.value) saveState(nodes.value, edges.value)
@@ -38,11 +88,11 @@ export function useFlowOperations(
// 节点删除 // 节点删除
const handleNodeDelete = (nodeId: string) => { const handleNodeDelete = (nodeId: string) => {
// 删除相关边 // 删除相关边
const relatedEdges = edges.value.filter(edge => const relatedEdges = edges.value.filter(
edge.source === nodeId || edge.target === nodeId (edge) => edge.source === nodeId || edge.target === nodeId,
) )
if (relatedEdges.length > 0) { if (relatedEdges.length > 0) {
removeEdges(relatedEdges.map(edge => edge.id)) removeEdges(relatedEdges.map((edge) => edge.id))
} }
removeNodes([nodeId]) removeNodes([nodeId])
@@ -51,7 +101,7 @@ export function useFlowOperations(
// 节点更新 // 节点更新
const handleNodeUpdate = (nodeId: string, newLabel: string) => { const handleNodeUpdate = (nodeId: string, newLabel: string) => {
const nodeIndex = nodes.value.findIndex(node => node.id === nodeId) const nodeIndex = nodes.value.findIndex((node) => node.id === nodeId)
if (nodeIndex !== -1) { if (nodeIndex !== -1) {
const oldNode = nodes.value[nodeIndex] const oldNode = nodes.value[nodeIndex]
@@ -61,8 +111,8 @@ export function useFlowOperations(
...oldNode, ...oldNode,
data: { data: {
...oldNode.data, ...oldNode.data,
customLabel: newLabel customLabel: newLabel,
} },
} }
// 使用 Vue Flow 的更新方法 // 使用 Vue Flow 的更新方法
@@ -84,14 +134,14 @@ export function useFlowOperations(
// 删除选中的节点和边 // 删除选中的节点和边
const deleteSelected = () => { const deleteSelected = () => {
const selectedNodes = nodes.value.filter(node => (node as any).selected) const selectedNodes = nodes.value.filter((node) => (node as any).selected)
const selectedEdges = edges.value.filter(edge => (edge as any).selected) const selectedEdges = edges.value.filter((edge) => (edge as any).selected)
if (selectedNodes.length > 0) { if (selectedNodes.length > 0) {
removeNodes(selectedNodes.map(node => node.id)) removeNodes(selectedNodes.map((node) => node.id))
} }
if (selectedEdges.length > 0) { if (selectedEdges.length > 0) {
removeEdges(selectedEdges.map(edge => edge.id)) removeEdges(selectedEdges.map((edge) => edge.id))
} }
saveState(nodes.value, edges.value) saveState(nodes.value, edges.value)
} }
@@ -102,6 +152,6 @@ export function useFlowOperations(
handleNodeDelete, handleNodeDelete,
handleNodeUpdate, handleNodeUpdate,
clearCanvas, clearCanvas,
deleteSelected deleteSelected,
} }
} }

View File

@@ -410,6 +410,75 @@ export function createWebSocketComposable<T extends WebSocketMessage>(
} }
} }
/**
* 流程图评分更新消息类型
*/
export interface FlowchartEvaluationUpdate extends WebSocketMessage {
type: "flowchart_evaluation_completed" | "flowchart_evaluation_failed" | "flowchart_evaluation_update"
submission_id: string
score?: number
grade?: string
feedback?: string
error?: string
}
/**
* 流程图 WebSocket 连接管理类
*/
class FlowchartWebSocket extends BaseWebSocket<FlowchartEvaluationUpdate> {
constructor() {
super({
path: "flowchart", // 使用专门的 flowchart WebSocket 路径
})
}
/**
* 订阅特定流程图提交的更新
*/
subscribe(submissionId: string) {
const success = this.send({
type: "subscribe",
submission_id: submissionId,
})
if (!success) {
console.error("[Flowchart WebSocket] 订阅失败: 连接未就绪")
}
}
}
/**
* 用于组件中使用流程图 WebSocket 的 Composable
*/
export function useFlowchartWebSocket(
handler?: MessageHandler<FlowchartEvaluationUpdate>,
) {
const ws = new FlowchartWebSocket()
// 如果提供了处理器,添加到实例中
if (handler) {
ws.addHandler(handler)
}
// 组件卸载时清理资源
onUnmounted(() => {
if (handler) {
ws.removeHandler(handler)
}
ws.disconnect()
})
return {
connect: () => ws.connect(),
disconnect: () => ws.disconnect(),
subscribe: (submissionId: string) => ws.subscribe(submissionId),
scheduleDisconnect: (delay?: number) => ws.scheduleDisconnect(delay),
cancelScheduledDisconnect: () => ws.cancelScheduledDisconnect(),
status: ws.status,
addHandler: (h: MessageHandler<FlowchartEvaluationUpdate>) => ws.addHandler(h),
removeHandler: (h: MessageHandler<FlowchartEvaluationUpdate>) => ws.removeHandler(h),
}
}
/** /**
* 配置更新消息类型 * 配置更新消息类型
*/ */

View File

@@ -170,6 +170,7 @@ export interface ProblemFiltered {
rate: string rate: string
status: "not_test" | "passed" | "failed" status: "not_test" | "passed" | "failed"
author: string author: string
allow_flowchart: boolean
} }
export interface AdminProblemFiltered { export interface AdminProblemFiltered {