7
src/oj/flowchart/index.vue
Normal file
7
src/oj/flowchart/index.vue
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import FlowchartEditor from "shared/components/FlowchartEditor/index.vue"
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<FlowchartEditor />
|
||||||
|
</template>
|
||||||
|
<style scoped></style>
|
||||||
@@ -72,8 +72,7 @@ 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, {
|
||||||
@@ -83,8 +82,7 @@ const languageOptions: DropdownOption[] = languages.value.map(
|
|||||||
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)
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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 +=
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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%)',
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -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',
|
|
||||||
'decision',
|
|
||||||
'loop',
|
|
||||||
'output',
|
|
||||||
'end'
|
|
||||||
].map(type => {
|
|
||||||
const config = getNodeTypeConfig(type)
|
const config = getNodeTypeConfig(type)
|
||||||
return {
|
return {
|
||||||
type,
|
type,
|
||||||
...config
|
...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,15 +206,23 @@ 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;
|
||||||
@@ -223,7 +230,6 @@ const getSaveStatusTitle = () => {
|
|||||||
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 {
|
||||||
|
|||||||
@@ -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 } =
|
||||||
|
useCache(
|
||||||
nodes,
|
nodes,
|
||||||
edges,
|
edges,
|
||||||
'flowchart-editor-data'
|
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>
|
||||||
|
|
||||||
|
|||||||
@@ -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(
|
||||||
|
[nodes, edges],
|
||||||
|
() => {
|
||||||
hasUnsavedChanges.value = true
|
hasUnsavedChanges.value = true
|
||||||
debouncedSave()
|
debouncedSave()
|
||||||
}, { deep: true })
|
},
|
||||||
|
{ deep: true },
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isSaving,
|
isSaving,
|
||||||
@@ -78,6 +84,6 @@ export function useCache(
|
|||||||
hasUnsavedChanges,
|
hasUnsavedChanges,
|
||||||
saveToCache,
|
saveToCache,
|
||||||
loadFromCache,
|
loadFromCache,
|
||||||
clearCache
|
clearCache,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user