重新实现拖动功能,其他组件适配

This commit is contained in:
2025-07-17 16:37:29 +08:00
parent f083f8065b
commit 5ede390132
6 changed files with 100 additions and 94 deletions

View File

@@ -1,8 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { ref } from 'vue';
import useDragAndDrop from '@/ts/useDnD'; import { getLogicFlowInstance } from '@/ts/useLogicFlow';
const { onDragStart } = useDragAndDrop();
// 使用嵌套结构定义组件分组 // 使用嵌套结构定义组件分组
const componentGroups = [ const componentGroups = [
@@ -107,25 +105,28 @@ const componentGroups = [
} }
} }
] ]
} },
// 可以轻松添加新的游戏组件组 // 可以轻松添加新的游戏组件组
// { {
// id: 'other-game', id: 'other-game',
// title: '其他游戏', title: '其他游戏',
// components: [] components: []
// } }
]; ];
// 处理组件点击 - 直接使用 onDragStart 的数据格式 // 处理组件点击 - 可选:可直接创建节点
const handleComponentClick = (component) => { const handleComponentClick = (component) => {
const nodeData = { // 可选:实现点击直接添加节点到画布
};
const handleMouseDown = (e, component) => {
e.preventDefault(); // 阻止文字选中
const lf = getLogicFlowInstance();
if (!lf) return;
lf.dnd.startDrag({
type: component.type, type: component.type,
label: component.name, properties: component.data
position: { x: 100, y: 100 }, });
data: component.data
};
onDragStart({ dataTransfer: { setData: () => {} } } as DragEvent, nodeData);
}; };
</script> </script>
@@ -140,17 +141,12 @@ const handleComponentClick = (component) => {
> >
<div class="group-title">{{ group.title }}</div> <div class="group-title">{{ group.title }}</div>
<div class="components-list"> <div class="components-list">
<div <div
v-for="component in group.components" v-for="component in group.components"
:key="component.id" :key="component.id"
class="component-item" class="component-item"
@click="handleComponentClick(component)" @click="handleComponentClick(component)"
draggable="true" @mousedown="(e) => handleMouseDown(e, component)"
@dragstart="(e) => onDragStart(e, {
type: component.type,
label: component.name,
data: component.data
})"
> >
<div class="component-icon"> <div class="component-icon">
<i class="el-icon-plus"></i> <i class="el-icon-plus"></i>

View File

@@ -32,7 +32,8 @@ import PropertySelectNode from './nodes/yys/PropertySelectNode.vue';
// import ImageNode from './nodes/common/ImageNode.vue'; // import ImageNode from './nodes/common/ImageNode.vue';
// import TextNode from './nodes/common/TextNode.vue'; // import TextNode from './nodes/common/TextNode.vue';
import PropertyPanel from './PropertyPanel.vue'; import PropertyPanel from './PropertyPanel.vue';
import {useFilesStore} from "@/ts/useStore"; import { useFilesStore } from "@/ts/useStore";
import { setLogicFlowInstance, destroyLogicFlowInstance } from '@/ts/useLogicFlow';
const props = defineProps<{ const props = defineProps<{
nodes: any[]; nodes: any[];
@@ -74,7 +75,7 @@ onMounted(() => {
}); });
registerNodes(lf.value); registerNodes(lf.value);
renderFlow(); renderFlow();
filesStore.setLogicFlowInstance(lf.value); setLogicFlowInstance(lf.value);
// 监听节点点击事件,更新 selectedNode // 监听节点点击事件,更新 selectedNode
lf.value.on(EventType.NODE_CLICK, ({ data }) => { lf.value.on(EventType.NODE_CLICK, ({ data }) => {
@@ -88,18 +89,16 @@ onMounted(() => {
// 节点属性改变,如果当前节点是选中节点,则同步更新 selectedNode // 节点属性改变,如果当前节点是选中节点,则同步更新 selectedNode
lf.value.on(EventType.NODE_PROPERTIES_CHANGE, (data) => { lf.value.on(EventType.NODE_PROPERTIES_CHANGE, (data) => {
const nodeId = data.id || (data.value && data.value.id); const nodeId = data.id;
if (selectedNode.value && nodeId === selectedNode.value.id) { if (selectedNode.value && nodeId === selectedNode.value.id) {
if (data.value) { if (data.properties) {
selectedNode.value = data.value; selectedNode.value = {
} else if (data.properties) { ...selectedNode.value,
selectedNode.value = { properties: data.properties
...selectedNode.value, };
properties: data.properties }
};
} }
} });
});
// 右键事件 // 右键事件
lf.value.on('node:contextmenu', handleNodeContextMenu); lf.value.on('node:contextmenu', handleNodeContextMenu);
@@ -110,6 +109,7 @@ onMounted(() => {
onBeforeUnmount(() => { onBeforeUnmount(() => {
lf.value?.destroy(); lf.value?.destroy();
lf.value = null; lf.value = null;
destroyLogicFlowInstance();
}); });
// 响应式更新 nodes/edges // 响应式更新 nodes/edges

View File

@@ -1,38 +1,31 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, watch } from 'vue'; import { computed } from 'vue';
import { useVueFlow } from '@vue-flow/core'; import type LogicFlow from '@logicflow/core';
// import { useVueFlow } from '@vue-flow/core';
import { QuillEditor } from '@vueup/vue-quill'; import { QuillEditor } from '@vueup/vue-quill';
import '@vueup/vue-quill/dist/vue-quill.snow.css'; import '@vueup/vue-quill/dist/vue-quill.snow.css';
import { useDialogs } from '../../ts/useDialogs'; import { useDialogs } from '../../ts/useDialogs';
import { useFilesStore } from '@/ts/useStore';
import { getLogicFlowInstance } from '@/ts/useLogicFlow';
const props = defineProps({ const props = defineProps({
height: { height: {
type: String, type: String,
default: '100%' default: '100%'
},
node: {
type: Object,
default: null
} }
}); });
// 使用VueFlow的store获取当前选中的节点 const filesStore = useFilesStore();
const { findNode, getNodes, updateNode } = useVueFlow();
const { openDialog } = useDialogs(); const { openDialog } = useDialogs();
// getNodes是一个ref对象而不是函数 const selectedNode = computed(() => props.node);
const nodes = getNodes;
// 当前选中的节点
const selectedNode = ref(null);
// 监听节点变化
watch(nodes, (newNodes) => {
// 查找选中的节点
const selected = newNodes.find(node => node.selected);
selectedNode.value = selected || null;
}, { deep: true });
// 计算属性:节点是否选中
const hasNodeSelected = computed(() => !!selectedNode.value); const hasNodeSelected = computed(() => !!selectedNode.value);
// 计算属性:节点类型
const nodeType = computed(() => { const nodeType = computed(() => {
if (!selectedNode.value) return ''; if (!selectedNode.value) return '';
return selectedNode.value.type || 'default'; return selectedNode.value.type || 'default';
@@ -40,16 +33,21 @@ const nodeType = computed(() => {
// 通用的弹窗处理方法 // 通用的弹窗处理方法
const handleOpenDialog = (type: 'shikigami' | 'yuhun' | 'property') => { const handleOpenDialog = (type: 'shikigami' | 'yuhun' | 'property') => {
if (selectedNode.value) { const lf = getLogicFlowInstance();
if (selectedNode.value && lf) {
const node = selectedNode.value; const node = selectedNode.value;
const currentData = node.data && node.data[type] ? node.data[type] : undefined; // 取 properties 下的 type 字段
const currentData = node.properties && node.properties[type] ? node.properties[type] : undefined;
openDialog( openDialog(
type, type,
currentData, currentData,
node, node,
(updatedData, nodeToUpdate) => { (updatedData) => {
updateNode(nodeToUpdate.id, { data: { ...nodeToUpdate.data, [type]: updatedData } }); lf.setProperties(node.id, {
...node.properties,
[type]: updatedData
});
} }
); );
} }
@@ -60,18 +58,11 @@ const handleImageUpload = (e) => {
if (!file) return; if (!file) return;
const reader = new FileReader(); const reader = new FileReader();
reader.onload = (evt) => { reader.onload = (evt) => {
updateNodeData('url', evt.target.result); // updateNodeData('url', evt.target.result);
}; };
reader.readAsDataURL(file); reader.readAsDataURL(file);
}; };
const updateNodeData = (key, value) => {
if (!selectedNode.value) return;
const node = findNode(selectedNode.value.id);
if (node) {
node.data = { ...node.data, [key]: value };
}
};
const quillToolbar = [ const quillToolbar = [
[{ header: 1 }, { header: 2 }], [{ header: 1 }, { header: 2 }],
@@ -88,11 +79,11 @@ const quillToolbar = [
<div class="panel-header"> <div class="panel-header">
<h3>属性编辑</h3> <h3>属性编辑</h3>
</div> </div>
<div v-if="!hasNodeSelected" class="no-selection"> <div v-if="!hasNodeSelected" class="no-selection">
<p>请选择一个节点以编辑其属性</p> <p>请选择一个节点以编辑其属性</p>
</div> </div>
<div v-else class="property-content"> <div v-else class="property-content">
<div class="property-section"> <div class="property-section">
<div class="section-header">基本信息</div> <div class="section-header">基本信息</div>
@@ -105,42 +96,43 @@ const quillToolbar = [
<div class="property-value">{{ nodeType }}</div> <div class="property-value">{{ nodeType }}</div>
</div> </div>
</div> </div>
<!-- 式神选择节点的特定属性 --> <!-- 式神选择节点的特定属性 -->
<div v-if="nodeType === 'shikigamiSelect'" class="property-section"> <div v-if="nodeType === 'shikigamiSelect'" class="property-section">
<div class="section-header">式神属性</div> <div class="section-header">式神属性</div>
<div class="property-item"> <div class="property-item">
<el-button <span>当前选择式神{{ selectedNode.properties?.shikigami?.name || '未选择' }}</span>
type="primary" <el-button
@click="handleOpenDialog('shikigami')" type="primary"
@click="handleOpenDialog('shikigami')"
style="width: 100%" style="width: 100%"
> >
选择式神 选择式神
</el-button> </el-button>
</div> </div>
</div> </div>
<!-- 御魂选择节点的特定属性 --> <!-- 御魂选择节点的特定属性 -->
<div v-if="nodeType === 'yuhunSelect'" class="property-section"> <div v-if="nodeType === 'yuhunSelect'" class="property-section">
<div class="section-header">御魂属性</div> <div class="section-header">御魂属性</div>
<div class="property-item"> <div class="property-item">
<el-button <el-button
type="primary" type="primary"
@click="handleOpenDialog('yuhun')" @click="handleOpenDialog('yuhun')"
style="width: 100%" style="width: 100%"
> >
选择御魂 选择御魂
</el-button> </el-button>
</div> </div>
</div> </div>
<!-- 属性选择节点的特定属性 --> <!-- 属性选择节点的特定属性 -->
<div v-if="nodeType === 'propertySelect'" class="property-section"> <div v-if="nodeType === 'propertySelect'" class="property-section">
<div class="section-header">属性设置</div> <div class="section-header">属性设置</div>
<div class="property-item"> <div class="property-item">
<el-button <el-button
type="primary" type="primary"
@click="handleOpenDialog('property')" @click="handleOpenDialog('property')"
style="width: 100%" style="width: 100%"
> >
设置属性 设置属性
@@ -153,8 +145,8 @@ const quillToolbar = [
<div class="section-header">图片设置</div> <div class="section-header">图片设置</div>
<div class="property-item"> <div class="property-item">
<input type="file" accept="image/*" @change="handleImageUpload" /> <input type="file" accept="image/*" @change="handleImageUpload" />
<div v-if="selectedNode.data && selectedNode.data.url" style="margin-top:8px;"> <div v-if="selectedNode.value.properties && selectedNode.value.properties.url" style="margin-top:8px;">
<img :src="selectedNode.data.url" alt="预览" style="max-width:100%;max-height:100px;" /> <img :src="selectedNode.value.properties.url" alt="预览" style="max-width:100%;max-height:100px;" />
</div> </div>
</div> </div>
</div> </div>
@@ -163,14 +155,14 @@ const quillToolbar = [
<div v-if="nodeType === 'textNode'" class="property-section"> <div v-if="nodeType === 'textNode'" class="property-section">
<div class="section-header">文本编辑</div> <div class="section-header">文本编辑</div>
<div class="property-item"> <div class="property-item">
<QuillEditor <!-- <QuillEditor-->
v-model:content="selectedNode.data.html" <!-- v-model:content="selectedNode.value.properties.html"-->
contentType="html" <!-- contentType="html"-->
:toolbar="quillToolbar" <!-- :toolbar="quillToolbar"-->
theme="snow" <!-- theme="snow"-->
style="height:120px;" <!-- style="height:120px;"-->
@update:content="val => updateNodeData('html', val)" <!-- @update:content="val => updateNodeData('html', val)"-->
/> <!-- />-->
</div> </div>
</div> </div>
</div> </div>
@@ -248,4 +240,4 @@ const quillToolbar = [
font-size: 14px; font-size: 14px;
word-break: break-all; word-break: break-all;
} }
</style> </style>

View File

@@ -41,6 +41,7 @@ onMounted(() => {
:src="currentShikigami.avatar" :src="currentShikigami.avatar"
:alt="currentShikigami.name" :alt="currentShikigami.name"
class="shikigami-image" class="shikigami-image"
draggable="false"
/> />
<div v-else class="placeholder-text">点击选择式神</div> <div v-else class="placeholder-text">点击选择式神</div>
<div class="name-text">{{ currentShikigami.name }}</div> <div class="name-text">{{ currentShikigami.name }}</div>

View File

@@ -32,6 +32,7 @@ onMounted(() => {
:src="currentYuhun.avatar" :src="currentYuhun.avatar"
:alt="currentYuhun.name" :alt="currentYuhun.name"
class="yuhun-image" class="yuhun-image"
draggable="false"
/> />
<div v-else class="placeholder-text">点击选择御魂</div> <div v-else class="placeholder-text">点击选择御魂</div>
<div class="name-text">{{ currentYuhun.name }}</div> <div class="name-text">{{ currentYuhun.name }}</div>

16
src/ts/useLogicFlow.ts Normal file
View File

@@ -0,0 +1,16 @@
import type LogicFlow from '@logicflow/core';
let logicFlowInstance: LogicFlow | null = null;
export function setLogicFlowInstance(lf: LogicFlow) {
logicFlowInstance = lf;
}
export function getLogicFlowInstance(): LogicFlow | null {
return logicFlowInstance;
}
export function destroyLogicFlowInstance() {
logicFlowInstance?.destroy();
logicFlowInstance = null;
}