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

@@ -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
}