update
Some checks failed
Deploy / deploy (push) Has been cancelled

This commit is contained in:
2025-10-13 15:18:14 +08:00
parent 854b1f0769
commit 6f1720acd5
17 changed files with 248 additions and 184 deletions

View File

@@ -0,0 +1,7 @@
<script lang="ts" setup>
import FlowchartEditor from "shared/components/FlowchartEditor/index.vue"
</script>
<template>
<FlowchartEditor />
</template>
<style scoped></style>

View File

@@ -72,19 +72,17 @@ const menu = computed<DropdownOption[]>(() => [
{ label: "重置代码", key: "reset" }, { label: "重置代码", key: "reset" },
]) ])
const languageOptions: DropdownOption[] = languages.value.map( const languageOptions: DropdownOption[] = languages.value.map((it) => ({
(it) => ({ label: () =>
label: () => h(NFlex, { align: "center" }, () => [
h(NFlex, { align: "center" }, () => [ h(Icon, {
h(Icon, { icon: ICON_SET[it],
icon: ICON_SET[it], width: 16,
width: 16, }),
}), LANGUAGE_SHOW_VALUE[it],
LANGUAGE_SHOW_VALUE[it], ]),
]), value: it,
value: it, }))
}),
)
const copy = async () => { const copy = async () => {
const success = await copyToClipboard(codeStore.code.value) const success = await copyToClipboard(codeStore.code.value)

View File

@@ -81,7 +81,7 @@ const handleSyncStatusChange = (status: {
} }
// 提供FlowchartEditor的ref给子组件 // 提供FlowchartEditor的ref给子组件
provide('flowchartEditorRef', flowchartEditorRef) provide("flowchartEditorRef", flowchartEditorRef)
</script> </script>
<template> <template>
@@ -93,7 +93,10 @@ provide('flowchartEditorRef', flowchartEditorRef)
@change-language="changeLanguage" @change-language="changeLanguage"
@toggle-sync="toggleSync" @toggle-sync="toggleSync"
/> />
<FlowchartEditor v-if="codeStore.code.language === 'Flowchart'" ref="flowchartEditorRef" /> <FlowchartEditor
v-if="codeStore.code.language === 'Flowchart'"
ref="flowchartEditorRef"
/>
<SyncCodeEditor <SyncCodeEditor
v-else v-else
v-model:value="codeStore.code.value" v-model:value="codeStore.code.value"

View File

@@ -1,9 +1,9 @@
import { ref, watch } from 'vue' import { ref, watch } from "vue"
import { useMessage } from 'naive-ui' import { useMessage } from "naive-ui"
import { import {
useFlowchartWebSocket, useFlowchartWebSocket,
type FlowchartEvaluationUpdate, type FlowchartEvaluationUpdate,
} from 'shared/composables/websocket' } from "shared/composables/websocket"
export interface EvaluationResult { export interface EvaluationResult {
score?: number score?: number

View File

@@ -1,10 +1,10 @@
import { ref } from 'vue' import { ref } from "vue"
import { useMessage } from 'naive-ui' import { useMessage } from "naive-ui"
import { submitFlowchart, getCurrentProblemFlowchartSubmission } from 'oj/api' import { submitFlowchart, getCurrentProblemFlowchartSubmission } from "oj/api"
import { useProblemStore } from 'oj/store/problem' import { useProblemStore } from "oj/store/problem"
import { atou, utoa } from 'utils/functions' import { atou, utoa } from "utils/functions"
import { useMermaidConverter } from './useMermaidConverter' import { useMermaidConverter } from "./useMermaidConverter"
import { useFlowchartSubmission } from './useFlowchartSubmission' import { useFlowchartSubmission } from "./useFlowchartSubmission"
export function useFlowchartSubmit() { export function useFlowchartSubmit() {
const message = useMessage() const message = useMessage()
@@ -34,7 +34,9 @@ export function useFlowchartSubmit() {
const checkCurrentSubmissionStatus = async () => { const checkCurrentSubmissionStatus = async () => {
if (!problem.value?.id) return if (!problem.value?.id) return
const { data } = await getCurrentProblemFlowchartSubmission(problem.value.id) const { data } = await getCurrentProblemFlowchartSubmission(
problem.value.id,
)
const submission = data.submission const submission = data.submission
submissionCount.value = data.count submissionCount.value = data.count
if (submission && submission.status === 2) { if (submission && submission.status === 2) {

View File

@@ -65,7 +65,8 @@ export function useMermaidConverter() {
mermaid += "\n" mermaid += "\n"
mermaid += mermaid +=
" classDef startEnd fill:#e1f5fe,stroke:#01579b,stroke-width:2px\n" " classDef startEnd fill:#e1f5fe,stroke:#01579b,stroke-width:2px\n"
mermaid += " classDef input fill:#e3f2fd,stroke:#1976d2,stroke-width:2px\n" mermaid +=
" classDef input fill:#e3f2fd,stroke:#1976d2,stroke-width:2px\n"
mermaid += mermaid +=
" classDef output fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px\n" " classDef output fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px\n"
mermaid += mermaid +=

View File

@@ -96,6 +96,10 @@ export const ojs: RouteRecordRaw = {
component: () => import("oj/ai/analysis.vue"), component: () => import("oj/ai/analysis.vue"),
meta: { requiresAuth: true }, meta: { requiresAuth: true },
}, },
{
path: "flowchart",
component: () => import("oj/flowchart/index.vue"),
},
], ],
} }

View File

@@ -11,10 +11,7 @@
@mousedown="handleMouseDown" @mousedown="handleMouseDown"
> >
<!-- 连线点 - 根据节点类型动态显示 --> <!-- 连线点 - 根据节点类型动态显示 -->
<NodeHandles <NodeHandles :node-type="nodeType" :node-config="nodeConfig" />
:node-type="nodeType"
:node-config="nodeConfig"
/>
<!-- 节点内容 --> <!-- 节点内容 -->
<div class="node-content"> <div class="node-content">
@@ -35,7 +32,9 @@
/> />
<!-- 隐藏的文字用于保持尺寸 --> <!-- 隐藏的文字用于保持尺寸 -->
<span v-if="isEditing" class="node-label-hidden" aria-hidden="true">{{ displayLabel }}</span> <span v-if="isEditing" class="node-label-hidden" aria-hidden="true">{{
displayLabel
}}</span>
</div> </div>
<!-- 悬停时显示的操作按钮 --> <!-- 悬停时显示的操作按钮 -->
@@ -49,10 +48,10 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, onUnmounted, nextTick, computed, watch } from 'vue' import { ref, onUnmounted, nextTick, computed, watch } from "vue"
import { getNodeTypeConfig } from './useNodeStyles' import { getNodeTypeConfig } from "./useNodeStyles"
import NodeHandles from './NodeHandles.vue' import NodeHandles from "./NodeHandles.vue"
import NodeActions from './NodeActions.vue' import NodeActions from "./NodeActions.vue"
// 类型定义 // 类型定义
interface Props { interface Props {
@@ -73,7 +72,7 @@ const emit = defineEmits<Emits>()
// 响应式状态 // 响应式状态
const isHovered = ref(false) const isHovered = ref(false)
const isEditing = ref(false) const isEditing = ref(false)
const editText = ref('') const editText = ref("")
const editInput = ref<HTMLInputElement>() const editInput = ref<HTMLInputElement>()
// 定时器和事件处理器 // 定时器和事件处理器
@@ -83,16 +82,17 @@ let globalClickHandler: ((event: MouseEvent) => void) | null = null
// 计算属性 // 计算属性
const nodeType = computed(() => props.data.originalType || props.type) const nodeType = computed(() => props.data.originalType || props.type)
const nodeConfig = computed(() => getNodeTypeConfig(nodeType.value)) const nodeConfig = computed(() => getNodeTypeConfig(nodeType.value))
const displayLabel = computed(() => props.data.customLabel || nodeConfig.value.label) const displayLabel = computed(
() => props.data.customLabel || nodeConfig.value.label,
)
// 事件处理器 // 事件处理器
const handleDelete = () => emit('delete', props.id) const handleDelete = () => emit("delete", props.id)
const handleMouseDown = (event: MouseEvent) => { const handleMouseDown = (event: MouseEvent) => {
// 检查是否点击在连线点区域 // 检查是否点击在连线点区域
const target = event.target as HTMLElement const target = event.target as HTMLElement
if (target.closest('.vue-flow__handle')) { if (target.closest(".vue-flow__handle")) {
// 如果在连线点区域,禁用节点拖拽 // 如果在连线点区域,禁用节点拖拽
event.preventDefault() event.preventDefault()
return false return false
@@ -106,13 +106,13 @@ const handleDragStart = (event: DragEvent) => {
// 检查是否在连线点区域开始拖拽 // 检查是否在连线点区域开始拖拽
const target = event.target as HTMLElement const target = event.target as HTMLElement
if (target.closest('.vue-flow__handle')) { if (target.closest(".vue-flow__handle")) {
event.preventDefault() event.preventDefault()
return false return false
} }
if (event.dataTransfer) { if (event.dataTransfer) {
event.dataTransfer.effectAllowed = 'move' event.dataTransfer.effectAllowed = "move"
} }
} }
@@ -133,7 +133,7 @@ const handleSaveEdit = () => {
if (isEditing.value) { if (isEditing.value) {
// 保存编辑的文本 // 保存编辑的文本
if (editText.value.trim()) { if (editText.value.trim()) {
emit('update', props.id, editText.value.trim()) emit("update", props.id, editText.value.trim())
} }
isEditing.value = false isEditing.value = false
removeGlobalClickHandler() removeGlobalClickHandler()
@@ -142,7 +142,7 @@ const handleSaveEdit = () => {
const handleCancelEdit = () => { const handleCancelEdit = () => {
isEditing.value = false isEditing.value = false
editText.value = '' editText.value = ""
removeGlobalClickHandler() removeGlobalClickHandler()
} }
@@ -167,21 +167,23 @@ const addGlobalClickHandler = () => {
if (globalClickHandler) return if (globalClickHandler) return
globalClickHandler = (event: MouseEvent) => { globalClickHandler = (event: MouseEvent) => {
if (isEditing.value && !(event.target as Element)?.closest('.custom-node')) { if (
isEditing.value &&
!(event.target as Element)?.closest(".custom-node")
) {
handleSaveEdit() handleSaveEdit()
} }
} }
document.addEventListener('click', globalClickHandler, { capture: true }) document.addEventListener("click", globalClickHandler, { capture: true })
} }
const removeGlobalClickHandler = () => { const removeGlobalClickHandler = () => {
if (globalClickHandler) { if (globalClickHandler) {
document.removeEventListener('click', globalClickHandler, { capture: true }) document.removeEventListener("click", globalClickHandler, { capture: true })
globalClickHandler = null globalClickHandler = null
} }
} }
// 清理函数 // 清理函数
onUnmounted(() => { onUnmounted(() => {
if (hideTimeout) { if (hideTimeout) {

View File

@@ -10,7 +10,9 @@
title="删除节点" title="删除节点"
> >
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/> <path
d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"
/>
</svg> </svg>
</button> </button>
</div> </div>

View File

@@ -39,7 +39,7 @@
zIndex: 10, zIndex: 10,
left: '-10px', left: '-10px',
top: '50%', top: '50%',
transform: 'translateY(-50%)' transform: 'translateY(-50%)',
}" }"
/> />
<Handle <Handle
@@ -54,7 +54,7 @@
zIndex: 10, zIndex: 10,
right: '-10px', right: '-10px',
top: '50%', top: '50%',
transform: 'translateY(-50%)' transform: 'translateY(-50%)',
}" }"
/> />

View File

@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue' import { computed } from "vue"
import { getNodeTypeConfig } from './useNodeStyles' import { getNodeTypeConfig } from "./useNodeStyles"
// 拖拽开始处理 // 拖拽开始处理
const onDragStart = (event: DragEvent, type: string) => { const onDragStart = (event: DragEvent, type: string) => {
@@ -30,36 +30,30 @@ const emit = defineEmits<{
// 工具栏状态 // 工具栏状态
// 节点类型定义 - 优化性能 // 节点类型定义 - 优化性能
const nodeTypes = computed(() => [ const nodeTypes = computed(() =>
'start', ["start", "input", "default", "decision", "loop", "output", "end"].map(
'input', (type) => {
'default', const config = getNodeTypeConfig(type)
'decision', return {
'loop', type,
'output', ...config,
'end' }
].map(type => { },
const config = getNodeTypeConfig(type) ),
return { )
type,
...config
}
}))
// 获取保存状态标题 // 获取保存状态标题
const getSaveStatusTitle = () => { const getSaveStatusTitle = () => {
if (props.isSaving) { if (props.isSaving) {
return '正在保存...' return "正在保存..."
} else if (props.hasUnsavedChanges) { } else if (props.hasUnsavedChanges) {
return '有未保存的更改' return "有未保存的更改"
} else if (props.lastSaved) { } else if (props.lastSaved) {
return `已保存 - ${new Date(props.lastSaved).toLocaleTimeString()}` return `已保存 - ${new Date(props.lastSaved).toLocaleTimeString()}`
} else { } else {
return '已保存' return "已保存"
} }
} }
</script> </script>
<template> <template>
<div class="toolbar"> <div class="toolbar">
@@ -67,11 +61,15 @@ const getSaveStatusTitle = () => {
<div class="toolbar-header"> <div class="toolbar-header">
<div class="header-content"> <div class="header-content">
<h3>节点库</h3> <h3>节点库</h3>
<div class="save-status-indicator" :class="{ <div
'saving': props.isSaving, class="save-status-indicator"
'unsaved': props.hasUnsavedChanges && !props.isSaving, :class="{
'saved': !props.hasUnsavedChanges && !props.isSaving saving: props.isSaving,
}" :title="getSaveStatusTitle()"> unsaved: props.hasUnsavedChanges && !props.isSaving,
saved: !props.hasUnsavedChanges && !props.isSaving,
}"
:title="getSaveStatusTitle()"
>
<span v-if="props.isSaving" class="spinner"></span> <span v-if="props.isSaving" class="spinner"></span>
<span v-else-if="props.hasUnsavedChanges"></span> <span v-else-if="props.hasUnsavedChanges"></span>
<span v-else></span> <span v-else></span>
@@ -80,7 +78,6 @@ const getSaveStatusTitle = () => {
<p class="description">拖拽节点到画布中</p> <p class="description">拖拽节点到画布中</p>
</div> </div>
<!-- 节点列表 --> <!-- 节点列表 -->
<div class="nodes"> <div class="nodes">
<div <div
@@ -124,12 +121,15 @@ const getSaveStatusTitle = () => {
<span class="btn-text">重做</span> <span class="btn-text">重做</span>
</button> </button>
</div> </div>
<button class="action-btn clear-btn" @click="$emit('clear')" title="清空画布"> <button
class="action-btn clear-btn"
@click="$emit('clear')"
title="清空画布"
>
<span class="btn-icon">🗑</span> <span class="btn-icon">🗑</span>
<span class="btn-text">清空画布</span> <span class="btn-text">清空画布</span>
</button> </button>
</div> </div>
</div> </div>
</template> </template>
@@ -150,7 +150,6 @@ const getSaveStatusTitle = () => {
transition: all 0.3s ease; transition: all 0.3s ease;
} }
.toolbar-header { .toolbar-header {
margin-bottom: 16px; margin-bottom: 16px;
border-bottom: 1px solid #e5e7eb; border-bottom: 1px solid #e5e7eb;
@@ -207,23 +206,30 @@ const getSaveStatusTitle = () => {
} }
@keyframes spin { @keyframes spin {
from { transform: rotate(0deg); } from {
to { transform: rotate(360deg); } transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
} }
@keyframes pulse { @keyframes pulse {
0%, 100% { opacity: 1; } 0%,
50% { opacity: 0.5; } 100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
} }
.description { .description {
margin: 0; margin: 0;
font-size: 12px; font-size: 12px;
color: #6b7280; color: #6b7280;
} }
/* 节点列表样式 */ /* 节点列表样式 */
.nodes { .nodes {
display: flex; display: flex;
@@ -285,7 +291,6 @@ const getSaveStatusTitle = () => {
line-height: 1.3; line-height: 1.3;
} }
/* 工具栏操作按钮样式 */ /* 工具栏操作按钮样式 */
.toolbar-actions { .toolbar-actions {
display: flex; display: flex;
@@ -366,7 +371,6 @@ const getSaveStatusTitle = () => {
font-size: 12px; font-size: 12px;
} }
/* 滚动条样式 */ /* 滚动条样式 */
.toolbar::-webkit-scrollbar { .toolbar::-webkit-scrollbar {
width: 6px; width: 6px;
@@ -386,7 +390,6 @@ const getSaveStatusTitle = () => {
background: #94a3b8; background: #94a3b8;
} }
/* 响应式设计 */ /* 响应式设计 */
@media (max-width: 768px) { @media (max-width: 768px) {
.toolbar { .toolbar {

View File

@@ -1,10 +1,16 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref, onMounted, onUnmounted } from 'vue' import { ref, onMounted, onUnmounted } from "vue"
import Toolbar from "./Toolbar.vue" import Toolbar from "./Toolbar.vue"
import "@vue-flow/core/dist/style.css" import "@vue-flow/core/dist/style.css"
import "@vue-flow/core/dist/theme-default.css" import "@vue-flow/core/dist/theme-default.css"
import "@vue-flow/controls/dist/style.css" import "@vue-flow/controls/dist/style.css"
import { useVueFlow, VueFlow, type Node, type Edge, MarkerType } from "@vue-flow/core" import {
useVueFlow,
VueFlow,
type Node,
type Edge,
MarkerType,
} from "@vue-flow/core"
import { Controls } from "@vue-flow/controls" import { Controls } from "@vue-flow/controls"
import { Background } from "@vue-flow/background" import { Background } from "@vue-flow/background"
@@ -13,6 +19,15 @@ import { useHistory } from "./useHistory"
import { useFlowOperations } from "./useFlowOperations" import { useFlowOperations } from "./useFlowOperations"
import { useCache } from "./useCache" import { useCache } from "./useCache"
import CustomNode from "./CustomNode.vue" import CustomNode from "./CustomNode.vue"
import { useProblemStore } from "oj/store/problem"
interface Props {
readonly?: boolean
}
const props = withDefaults(defineProps<Props>(), {
readonly: false,
})
// Vue Flow 实例 // Vue Flow 实例
const { addNodes, addEdges, removeNodes, removeEdges } = useVueFlow() const { addNodes, addEdges, removeNodes, removeEdges } = useVueFlow()
@@ -24,15 +39,20 @@ const edges = ref<Edge[]>([])
// 历史记录管理 // 历史记录管理
const { canUndo, canRedo, saveState, undo, redo } = useHistory() const { canUndo, canRedo, saveState, undo, redo } = useHistory()
const problemStore = useProblemStore()
const { problem } = storeToRefs(problemStore)
// 缓存管理 // 缓存管理
const { isSaving, lastSaved, hasUnsavedChanges, saveToCache, loadFromCache, clearCache } = useCache( const { isSaving, lastSaved, hasUnsavedChanges, loadFromCache, clearCache } =
nodes, useCache(
edges, nodes,
'flowchart-editor-data' edges,
) problem.value?._id
? `flowchart-editor-data-problem-${problem.value!._id}`
: "flowchart-editor-data",
)
// 拖拽处理 // 拖拽处理
const { isDragOver, onDragOver, onDragLeave, onDrop } = useDnD() const { onDragOver, onDragLeave, onDrop } = useDnD()
// 流程操作 // 流程操作
const { const {
@@ -41,7 +61,7 @@ const {
handleNodeDelete, handleNodeDelete,
handleNodeUpdate, handleNodeUpdate,
clearCanvas, clearCanvas,
deleteSelected deleteSelected,
} = useFlowOperations( } = useFlowOperations(
nodes, nodes,
edges, edges,
@@ -49,7 +69,7 @@ const {
addEdges, addEdges,
removeNodes, removeNodes,
removeEdges, removeEdges,
saveState saveState,
) )
// 拖拽处理包装 // 拖拽处理包装
@@ -96,15 +116,15 @@ const handleClear = () => {
const handleKeyDown = (event: KeyboardEvent) => { const handleKeyDown = (event: KeyboardEvent) => {
if (event.target instanceof HTMLInputElement) return if (event.target instanceof HTMLInputElement) return
if (event.key === 'Delete' || event.key === 'Backspace') { if (event.key === "Delete" || event.key === "Backspace") {
deleteSelected() deleteSelected()
} }
if (event.ctrlKey || event.metaKey) { if (event.ctrlKey || event.metaKey) {
if (event.key === 'z' && !event.shiftKey) { if (event.key === "z" && !event.shiftKey) {
event.preventDefault() event.preventDefault()
handleUndo() handleUndo()
} else if (event.key === 'z' && event.shiftKey) { } else if (event.key === "z" && event.shiftKey) {
event.preventDefault() event.preventDefault()
handleRedo() handleRedo()
} }
@@ -112,30 +132,30 @@ const handleKeyDown = (event: KeyboardEvent) => {
} }
onMounted(() => { onMounted(() => {
document.addEventListener('keydown', handleKeyDown) document.addEventListener("keydown", handleKeyDown)
// 从缓存恢复数据 // 从缓存恢复数据
loadFromCache() loadFromCache()
}) })
onUnmounted(() => { onUnmounted(() => {
document.removeEventListener('keydown', handleKeyDown) document.removeEventListener("keydown", handleKeyDown)
}) })
// 加载外部数据到编辑器 // 加载外部数据到编辑器
const setFlowchartData = (data: { nodes: Node[], edges: Edge[] }) => { const setFlowchartData = (data: { nodes: Node[]; edges: Edge[] }) => {
if (data && data.nodes && data.edges) { if (data && data.nodes && data.edges) {
// 确保节点数据包含必要的位置信息 // 确保节点数据包含必要的位置信息
const processedNodes = data.nodes.map(node => ({ const processedNodes = data.nodes.map((node) => ({
...node, ...node,
position: node.position || { x: 0, y: 0 } position: node.position || { x: 0, y: 0 },
})) }))
// 确保边数据包含必要的 handle 信息 // 确保边数据包含必要的 handle 信息
const processedEdges = data.edges.map(edge => ({ const processedEdges = data.edges.map((edge) => ({
...edge, ...edge,
sourceHandle: edge.sourceHandle || null, sourceHandle: edge.sourceHandle || null,
targetHandle: edge.targetHandle || null targetHandle: edge.targetHandle || null,
})) }))
nodes.value = processedNodes nodes.value = processedNodes
@@ -150,9 +170,9 @@ defineExpose({
edges, edges,
getFlowchartData: () => ({ getFlowchartData: () => ({
nodes: nodes.value, nodes: nodes.value,
edges: edges.value edges: edges.value,
}), }),
setFlowchartData setFlowchartData,
}) })
</script> </script>
@@ -166,13 +186,14 @@ defineExpose({
@drop="handleDrop" @drop="handleDrop"
@connect="handleConnect" @connect="handleConnect"
@edge-click="handleEdgeClick" @edge-click="handleEdgeClick"
:readonly="readonly"
:default-edge-options="{ :default-edge-options="{
type: 'step', type: 'step',
style: { style: {
stroke: '#6366f1', stroke: '#6366f1',
strokeWidth: 2.5, strokeWidth: 2.5,
cursor: 'pointer', cursor: 'pointer',
filter: 'drop-shadow(0 2px 4px rgba(0,0,0,0.1))' filter: 'drop-shadow(0 2px 4px rgba(0,0,0,0.1))',
}, },
markerEnd: { markerEnd: {
type: MarkerType.ArrowClosed, type: MarkerType.ArrowClosed,
@@ -204,7 +225,12 @@ defineExpose({
orient="auto" orient="auto"
markerUnits="strokeWidth" markerUnits="strokeWidth"
> >
<path d="M0,0 L0,6 L10,3 z" fill="#6366f1" stroke="#6366f1" strokeWidth="0.5" /> <path
d="M0,0 L0,6 L10,3 z"
fill="#6366f1"
stroke="#6366f1"
strokeWidth="0.5"
/>
</marker> </marker>
</defs> </defs>
<template #node-custom="{ data, id, type }"> <template #node-custom="{ data, id, type }">
@@ -220,6 +246,8 @@ defineExpose({
<Background variant="lines" :gap="20" :size="1" /> <Background variant="lines" :gap="20" :size="1" />
<Controls /> <Controls />
<Toolbar <Toolbar
v-if="!readonly"
r
:can-undo="canUndo" :can-undo="canUndo"
:can-redo="canRedo" :can-redo="canRedo"
:is-saving="isSaving" :is-saving="isSaving"
@@ -241,4 +269,3 @@ defineExpose({
position: relative; position: relative;
} }
</style> </style>

View File

@@ -1,6 +1,6 @@
import { ref, watch } from 'vue' import { ref, watch } from "vue"
import { useStorage, useDebounceFn } from '@vueuse/core' import { useStorage, useDebounceFn } from "@vueuse/core"
import type { Node, Edge } from '@vue-flow/core' import type { Node, Edge } from "@vue-flow/core"
/** /**
* 缓存管理 - 使用 @vueuse 的 useStorage * 缓存管理 - 使用 @vueuse 的 useStorage
@@ -8,7 +8,7 @@ import type { Node, Edge } from '@vue-flow/core'
export function useCache( export function useCache(
nodes: any, nodes: any,
edges: any, edges: any,
storageKey: string = 'flowchart-editor-data' storageKey: string = "flowchart-editor-data",
) { ) {
const isSaving = ref(false) const isSaving = ref(false)
const lastSaved = ref<Date | null>(null) const lastSaved = ref<Date | null>(null)
@@ -22,7 +22,7 @@ export function useCache(
}>(storageKey, { }>(storageKey, {
nodes: [], nodes: [],
edges: [], edges: [],
timestamp: '' timestamp: "",
}) })
// 防抖保存 // 防抖保存
@@ -52,7 +52,9 @@ export function useCache(
if (storedData.value.nodes?.length || storedData.value.edges?.length) { if (storedData.value.nodes?.length || storedData.value.edges?.length) {
nodes.value = storedData.value.nodes nodes.value = storedData.value.nodes
edges.value = storedData.value.edges edges.value = storedData.value.edges
lastSaved.value = storedData.value.timestamp ? new Date(storedData.value.timestamp) : null lastSaved.value = storedData.value.timestamp
? new Date(storedData.value.timestamp)
: null
hasUnsavedChanges.value = false hasUnsavedChanges.value = false
return true return true
} }
@@ -61,16 +63,20 @@ export function useCache(
// 清除缓存数据 // 清除缓存数据
const clearCache = () => { const clearCache = () => {
storedData.value = { nodes: [], edges: [], timestamp: '' } storedData.value = { nodes: [], edges: [], timestamp: "" }
lastSaved.value = null lastSaved.value = null
hasUnsavedChanges.value = false hasUnsavedChanges.value = false
} }
// 监听节点和边的变化 // 监听节点和边的变化
watch([nodes, edges], () => { watch(
hasUnsavedChanges.value = true [nodes, edges],
debouncedSave() () => {
}, { deep: true }) hasUnsavedChanges.value = true
debouncedSave()
},
{ deep: true },
)
return { return {
isSaving, isSaving,
@@ -78,6 +84,6 @@ export function useCache(
hasUnsavedChanges, hasUnsavedChanges,
saveToCache, saveToCache,
loadFromCache, loadFromCache,
clearCache clearCache,
} }
} }

View File

@@ -1,7 +1,11 @@
import { ref } from 'vue' import { ref } from "vue"
import { useVueFlow } from "@vue-flow/core" import { useVueFlow } from "@vue-flow/core"
import { nanoid } from "nanoid" import { nanoid } from "nanoid"
import { getNodeTypeConfig, createNodeStyle, getNodeDimensions } from "./useNodeStyles" import {
getNodeTypeConfig,
createNodeStyle,
getNodeDimensions,
} from "./useNodeStyles"
/** /**
* 简化的拖拽处理 * 简化的拖拽处理
@@ -41,21 +45,21 @@ export function useDnD() {
// 调整位置,使节点中心点对齐到鼠标位置 // 调整位置,使节点中心点对齐到鼠标位置
const adjustedPosition = { const adjustedPosition = {
x: position.x - dimensions.width / 2, x: position.x - dimensions.width / 2,
y: position.y - dimensions.height / 2 y: position.y - dimensions.height / 2,
} }
const nodeId = `node-${nanoid()}` const nodeId = `node-${nanoid()}`
const config = getNodeTypeConfig(type) const config = getNodeTypeConfig(type)
const newNode = { const newNode = {
id: nodeId, id: nodeId,
type: 'custom', type: "custom",
position: adjustedPosition, position: adjustedPosition,
data: { data: {
label: config.label, label: config.label,
color: config.color, color: config.color,
originalType: type originalType: type,
}, },
style: createNodeStyle(type) style: createNodeStyle(type),
} }
addNodes([newNode]) addNodes([newNode])
@@ -66,6 +70,6 @@ export function useDnD() {
isDragOver, isDragOver,
onDragOver, onDragOver,
onDragLeave, onDragLeave,
onDrop onDrop,
} }
} }

View File

@@ -1,11 +1,11 @@
import { ref, computed } from 'vue' import { ref, computed } from "vue"
import type { Node, Edge } from '@vue-flow/core' import type { Node, Edge } from "@vue-flow/core"
/** /**
* 简化的历史记录管理 * 简化的历史记录管理
*/ */
export function useHistory() { export function useHistory() {
const history = ref<{ nodes: Node[], edges: Edge[] }[]>([]) const history = ref<{ nodes: Node[]; edges: Edge[] }[]>([])
const historyIndex = ref(-1) const historyIndex = ref(-1)
// 是否可以撤销 // 是否可以撤销
@@ -58,6 +58,6 @@ export function useHistory() {
canRedo, canRedo,
saveState, saveState,
undo, undo,
redo redo,
} }
} }

View File

@@ -414,7 +414,10 @@ export function createWebSocketComposable<T extends WebSocketMessage>(
* 流程图评分更新消息类型 * 流程图评分更新消息类型
*/ */
export interface FlowchartEvaluationUpdate extends WebSocketMessage { export interface FlowchartEvaluationUpdate extends WebSocketMessage {
type: "flowchart_evaluation_completed" | "flowchart_evaluation_failed" | "flowchart_evaluation_update" type:
| "flowchart_evaluation_completed"
| "flowchart_evaluation_failed"
| "flowchart_evaluation_update"
submission_id: string submission_id: string
score?: number score?: number
grade?: string grade?: string
@@ -474,8 +477,10 @@ export function useFlowchartWebSocket(
scheduleDisconnect: (delay?: number) => ws.scheduleDisconnect(delay), scheduleDisconnect: (delay?: number) => ws.scheduleDisconnect(delay),
cancelScheduledDisconnect: () => ws.cancelScheduledDisconnect(), cancelScheduledDisconnect: () => ws.cancelScheduledDisconnect(),
status: ws.status, status: ws.status,
addHandler: (h: MessageHandler<FlowchartEvaluationUpdate>) => ws.addHandler(h), addHandler: (h: MessageHandler<FlowchartEvaluationUpdate>) =>
removeHandler: (h: MessageHandler<FlowchartEvaluationUpdate>) => ws.removeHandler(h), ws.addHandler(h),
removeHandler: (h: MessageHandler<FlowchartEvaluationUpdate>) =>
ws.removeHandler(h),
} }
} }