fix flowchart
Some checks failed
Deploy / deploy (build, debian, 22, /root/OJDeploy/data/clientnext) (push) Has been cancelled
Deploy / deploy (build:staging, school, 8822, /root/OJ/data/dist) (push) Has been cancelled

This commit is contained in:
2026-05-07 06:09:05 -06:00
parent c11c3cf226
commit 6a31a47c5d
10 changed files with 151 additions and 77 deletions

View File

@@ -98,4 +98,4 @@ const config: ReturnType<typeof defineConfig> = defineConfig(({ envMode }) => {
}
})
export default config
export default config

View File

@@ -16,14 +16,6 @@ body {
overflow: auto;
border: 1px solid rgba(148, 163, 184, 0.24);
border-radius: 8px;
background:
linear-gradient(rgba(148, 163, 184, 0.08) 1px, transparent 1px),
linear-gradient(90deg, rgba(148, 163, 184, 0.08) 1px, transparent 1px),
linear-gradient(135deg, #ffffff 0%, #f8fafc 52%, #eef6ff 100%);
background-size:
24px 24px,
24px 24px,
auto;
}
.oj-mermaid-surface > svg {

View File

@@ -69,8 +69,6 @@ const page = ref(1)
// ==================== WebSocket 相关函数 ====================
// 处理 WebSocket 消息
const handleWebSocketMessage = (data: FlowchartEvaluationUpdate) => {
console.log("收到流程图评分更新:", data)
if (data.type === "flowchart_evaluation_completed") {
loading.value = false
latestRating.value = {
@@ -79,11 +77,8 @@ const handleWebSocketMessage = (data: FlowchartEvaluationUpdate) => {
}
message.success(`流程图评分完成!得分: ${data.score}分 (${data.grade}级)`)
} else if (data.type === "flowchart_evaluation_failed") {
console.log("处理评分失败消息")
loading.value = false
message.error(`流程图评分失败: ${data.error}`)
} else {
console.log("未知的消息类型:", data.type)
}
}
@@ -94,7 +89,6 @@ const { connect, disconnect, subscribe } = useFlowchartWebSocket(
// 订阅提交更新
function subscribeToSubmission(submissionId: string) {
console.log("开始订阅WebSocket更新")
subscribe(submissionId)
}
@@ -287,16 +281,14 @@ onUnmounted(() => {
<n-grid :cols="5" :x-gap="16">
<!-- 左侧流程图预览区域 -->
<n-gi :span="3">
<n-card title="流程图预览">
<div class="flowchart">
<n-spin :show="rendering">
<n-alert v-if="renderError" type="error" title="流程图渲染失败">
{{ renderError }}
</n-alert>
<div class="flowchart" v-else ref="mermaidContainer"></div>
</n-spin>
</div>
</n-card>
<div class="flowchart">
<n-spin :show="rendering">
<n-alert v-if="renderError" type="error" title="流程图渲染失败">
{{ renderError }}
</n-alert>
<div class="flowchart" v-else ref="mermaidContainer"></div>
</n-spin>
</div>
<!-- 加载到编辑器按钮 -->
<n-flex style="margin-top: 16px" justify="center">
<n-button @click="loadToEditor" type="primary">

View File

@@ -1,5 +1,5 @@
<template>
<n-button v-if="showLink" type="info" text @click="goto">
<n-button v-if="showLink" type="info" text @click="handleClick">
{{ flowchart.id.slice(0, 12) }}
</n-button>
<n-text v-else class="flowchart-id" @click="handleClick">
@@ -7,8 +7,8 @@
</n-text>
</template>
<script setup lang="ts">
import type { FlowchartSubmissionListItem } from "utils/types"
import { useUserStore } from "shared/store/user"
import { FlowchartSubmissionListItem } from "utils/types"
const userStore = useUserStore()
@@ -27,10 +27,6 @@ const showLink = computed(() => {
return props.flowchart.username === userStore.user?.username
})
function goto() {
emit("showDetail", props.flowchart.id)
}
function handleClick() {
emit("showDetail", props.flowchart.id)
}

View File

@@ -4,7 +4,7 @@
:class="{ 'is-hovered': isHovered, 'is-editing': isEditing }"
:data-node-type="nodeType"
:draggable="!isEditing"
@mouseenter="isHovered = true"
@mouseenter="handleMouseEnter"
@mouseleave="handleMouseLeave"
@dblclick="handleDoubleClick"
@dragstart="handleDragStart"
@@ -53,11 +53,17 @@ import { getNodeTypeConfig } from "./useNodeStyles"
import NodeHandles from "./NodeHandles.vue"
import NodeActions from "./NodeActions.vue"
// 类型定义
interface NodeData {
label: string
color: string
originalType: string
customLabel?: string
}
interface Props {
id: string
type: string
data: any
data: NodeData
}
interface Emits {
@@ -147,6 +153,7 @@ const handleCancelEdit = () => {
}
const handleMouseEnter = () => {
isHovered.value = true
if (hideTimeout) {
clearTimeout(hideTimeout)
hideTimeout = null

View File

@@ -1,6 +1,7 @@
<script setup lang="ts">
import { computed } from "vue"
import { getNodeTypeConfig } from "./useNodeStyles"
import { currentDragNodeType } from "./useDnD"
// 拖拽开始处理
const onDragStart = (event: DragEvent, type: string) => {
@@ -8,6 +9,17 @@ const onDragStart = (event: DragEvent, type: string) => {
event.dataTransfer.setData("application/vueflow", type)
event.dataTransfer.effectAllowed = "move"
currentDragNodeType.value = type
// 隐藏浏览器默认拖影,改用 canvas 跟随预览
const emptyImg = new Image(1, 1)
emptyImg.src =
"data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
event.dataTransfer.setDragImage(emptyImg, 0, 0)
}
const onDragEnd = () => {
currentDragNodeType.value = null
}
// Props
@@ -42,8 +54,7 @@ const nodeTypes = computed(() =>
),
)
// 获取保存状态标题
const getSaveStatusTitle = () => {
const saveStatusTitle = computed(() => {
if (props.isSaving) {
return "正在保存..."
} else if (props.hasUnsavedChanges) {
@@ -53,7 +64,7 @@ const getSaveStatusTitle = () => {
} else {
return "已保存"
}
}
})
</script>
<template>
<div class="toolbar">
@@ -68,7 +79,7 @@ const getSaveStatusTitle = () => {
unsaved: props.hasUnsavedChanges && !props.isSaving,
saved: !props.hasUnsavedChanges && !props.isSaving,
}"
:title="getSaveStatusTitle()"
:title="saveStatusTitle"
>
<span v-if="props.isSaving" class="spinner"></span>
<span v-else-if="props.hasUnsavedChanges"></span>
@@ -86,6 +97,7 @@ const getSaveStatusTitle = () => {
class="node-item"
:draggable="true"
@dragstart="onDragStart($event, nodeType.type)"
@dragend="onDragEnd"
:style="{ borderColor: nodeType.color }"
:title="`${nodeType.label} - ${nodeType.description}`"
>

View File

@@ -14,7 +14,8 @@ import {
import { Controls } from "@vue-flow/controls"
import { Background } from "@vue-flow/background"
import { useDnD } from "./useDnD"
import { useDnD, currentDragNodeType } from "./useDnD"
import { getNodeTypeConfig } from "./useNodeStyles"
import { useHistory } from "./useHistory"
import { useFlowOperations } from "./useFlowOperations"
import { useCache } from "./useCache"
@@ -42,17 +43,35 @@ const { canUndo, canRedo, saveState, undo, redo } = useHistory()
const problemStore = useProblemStore()
const { problem } = storeToRefs(problemStore)
// 缓存管理
const { isSaving, lastSaved, hasUnsavedChanges, loadFromCache, clearCache } =
useCache(
nodes,
edges,
problem.value?._id
? `flowchart-editor-data-problem-${problem.value!._id}`
: "flowchart-editor-data",
)
const {
isSaving,
lastSaved,
hasUnsavedChanges,
saveToCache,
loadFromCache,
clearCache,
} = useCache(
nodes,
edges,
problem.value?._id
? `flowchart-editor-data-problem-${problem.value!._id}`
: "flowchart-editor-data",
)
// 拖拽处理
const { onDragOver, onDragLeave, onDrop } = useDnD()
const { onDragOver, onDragLeave, onDrop, isDragOver, screenDragPos } = useDnD()
const dragPreviewStyle = computed(() => {
if (!screenDragPos.value || !currentDragNodeType.value) return null
const config = getNodeTypeConfig(currentDragNodeType.value)
const type = currentDragNodeType.value
return {
left: `${screenDragPos.value.x}px`,
top: `${screenDragPos.value.y}px`,
background: config.color,
borderRadius: type === "start" || type === "end" ? "20px" : "8px",
}
})
// 流程操作
const {
@@ -93,16 +112,18 @@ const handleDrop = (event: DragEvent) => {
const handleUndo = () => {
const state = undo()
if (state) {
nodes.value = [...state.nodes]
edges.value = [...state.edges]
nodes.value = state.nodes
edges.value = state.edges
saveToCache()
}
}
const handleRedo = () => {
const state = redo()
if (state) {
nodes.value = [...state.nodes]
edges.value = [...state.edges]
nodes.value = state.nodes
edges.value = state.edges
saveToCache()
}
}
@@ -182,6 +203,19 @@ defineExpose({
<template>
<div class="container" :style="{ height }">
<!-- 拖拽时跟随鼠标的节点预览 -->
<Transition name="drag-preview">
<div
v-if="isDragOver && dragPreviewStyle && currentDragNodeType"
class="drag-node-preview"
:style="dragPreviewStyle"
>
<span class="preview-icon">{{
getNodeTypeConfig(currentDragNodeType).icon
}}</span>
<span>{{ getNodeTypeConfig(currentDragNodeType).label }}</span>
</div>
</Transition>
<VueFlow
v-model:nodes="nodes"
v-model:edges="edges"
@@ -191,7 +225,7 @@ defineExpose({
@connect="handleConnect"
@edge-click="handleEdgeClick"
:default-edge-options="{
type: 'step',
type: 'default',
style: {
stroke: '#6366f1',
strokeWidth: 2.5,
@@ -269,4 +303,36 @@ defineExpose({
height: 100%;
position: relative;
}
.drag-node-preview {
position: fixed;
transform: translate(-50%, -50%);
pointer-events: none;
z-index: 9999;
padding: 8px 18px;
color: white;
font-size: 14px;
font-weight: 500;
display: flex;
align-items: center;
gap: 8px;
opacity: 0.55;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
white-space: nowrap;
border: 2px dashed rgba(255, 255, 255, 0.6);
user-select: none;
}
.preview-icon {
font-size: 16px;
}
.drag-preview-enter-active,
.drag-preview-leave-active {
transition: opacity 0.1s ease;
}
.drag-preview-enter-from,
.drag-preview-leave-to {
opacity: 0;
}
</style>

View File

@@ -7,28 +7,36 @@ import {
} from "./useNodeStyles"
import { getRandomId } from "utils/functions"
// 模块级共享当前拖拽的节点类型Toolbar 写入canvas 读取)
export const currentDragNodeType = ref<string | null>(null)
/**
* 简化的拖拽处理
*/
export function useDnD() {
const { addNodes, screenToFlowCoordinate } = useVueFlow()
const isDragOver = ref(false)
const screenDragPos = ref<{ x: number; y: number } | null>(null)
// 拖拽悬停处理
const onDragOver = (event: DragEvent) => {
event.preventDefault()
isDragOver.value = true
screenDragPos.value = { x: event.clientX, y: event.clientY }
}
// 拖拽离开处理
const onDragLeave = () => {
isDragOver.value = false
screenDragPos.value = null
}
// 拖拽放置处理
const onDrop = (event: DragEvent) => {
event.preventDefault()
isDragOver.value = false
screenDragPos.value = null
currentDragNodeType.value = null
const type = event.dataTransfer?.getData("application/vueflow")
if (!type) return
@@ -68,6 +76,7 @@ export function useDnD() {
return {
isDragOver,
screenDragPos,
onDragOver,
onDragLeave,
onDrop,

View File

@@ -1,5 +1,5 @@
import type { Ref } from "vue"
import type { Node, Edge } from "@vue-flow/core"
import type { Node, Edge, Connection } from "@vue-flow/core"
import { getRandomId } from "utils/functions"
export function useFlowOperations(
@@ -11,12 +11,11 @@ export function useFlowOperations(
removeEdges: (edgeIds: string[]) => void,
saveState: (nodes: Node[], edges: Edge[]) => void,
) {
// 根据节点类型和handle自动推断标签
const getAutoLabel = (
sourceNode: any,
targetNode: any,
sourceHandle: string,
targetHandle: string,
sourceNode: Node | undefined,
targetNode: Node | undefined,
sourceHandle: string | null | undefined,
targetHandle: string | null | undefined,
) => {
const sourceType = sourceNode?.data?.originalType || sourceNode?.type
const targetType = targetNode?.data?.originalType || targetNode?.type
@@ -51,9 +50,7 @@ export function useFlowOperations(
return ""
}
// 连接处理
const handleConnect = (params: any) => {
// 获取源节点和目标节点
const handleConnect = (params: Connection) => {
const sourceNode = nodes.value.find((node) => node.id === params.source)
const targetNode = nodes.value.find((node) => node.id === params.target)
@@ -79,9 +76,8 @@ export function useFlowOperations(
saveState(nodes.value, edges.value)
}
// 边点击处理 - 单击删除
const handleEdgeClick = (event: any) => {
removeEdges([event.edge.id])
const handleEdgeClick = ({ edge }: { edge: Edge }) => {
removeEdges([edge.id])
saveState(nodes.value, edges.value)
}
@@ -115,12 +111,7 @@ export function useFlowOperations(
},
}
// 使用 Vue Flow 的更新方法
nodes.value[nodeIndex] = updatedNode
// 强制触发响应式更新
nodes.value = [...nodes.value]
saveState(nodes.value, edges.value)
}
}

View File

@@ -1,11 +1,11 @@
import { ref, computed } from "vue"
import { shallowRef, computed } from "vue"
import type { Node, Edge } from "@vue-flow/core"
/**
* 简化的历史记录管理
*/
export function useHistory() {
const history = ref<{ nodes: Node[]; edges: Edge[] }[]>([])
const history = shallowRef<{ nodes: Node[]; edges: Edge[] }[]>([])
const historyIndex = ref(-1)
// 是否可以撤销
@@ -14,21 +14,30 @@ export function useHistory() {
// 是否可以重做
const canRedo = computed(() => historyIndex.value < history.value.length - 1)
const deepCopyState = (
nodes: Node[],
edges: Edge[],
): { nodes: Node[]; edges: Edge[] } =>
JSON.parse(JSON.stringify({ nodes, edges })) as {
nodes: Node[]
edges: Edge[]
}
// 保存状态到历史记录
const saveState = (nodes: Node[], edges: Edge[]) => {
const currentState = { nodes: [...nodes], edges: [...edges] }
const currentState = deepCopyState(nodes, edges)
// 如果当前不在历史记录的末尾,删除后面的记录
if (historyIndex.value < history.value.length - 1) {
history.value = history.value.slice(0, historyIndex.value + 1)
}
history.value.push(currentState)
history.value = [...history.value, currentState]
historyIndex.value = history.value.length - 1
// 限制历史记录数量
if (history.value.length > 20) {
history.value.shift()
history.value = history.value.slice(1)
historyIndex.value--
}
}
@@ -38,7 +47,7 @@ export function useHistory() {
if (canUndo.value) {
historyIndex.value--
const state = history.value[historyIndex.value]
return state
return deepCopyState(state.nodes, state.edges)
}
return null
}
@@ -48,7 +57,7 @@ export function useHistory() {
if (canRedo.value) {
historyIndex.value++
const state = history.value[historyIndex.value]
return state
return deepCopyState(state.nodes, state.edges)
}
return null
}