fix flowchart
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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,7 +281,6 @@ 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="流程图渲染失败">
|
||||
@@ -296,7 +289,6 @@ onUnmounted(() => {
|
||||
<div class="flowchart" v-else ref="mermaidContainer"></div>
|
||||
</n-spin>
|
||||
</div>
|
||||
</n-card>
|
||||
<!-- 加载到编辑器按钮 -->
|
||||
<n-flex style="margin-top: 16px" justify="center">
|
||||
<n-button @click="loadToEditor" type="primary">
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}`"
|
||||
>
|
||||
|
||||
@@ -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(
|
||||
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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user