onDragStart(e, {
+ type: component.type,
+ label: component.name,
+ data: component.data
+ })"
>
@@ -142,6 +171,9 @@ const handleComponentClick = (component) => {
background-color: #f5f7fa;
border-bottom: 1px solid #e4e7ed;
margin-bottom: 10px;
+ width: 200px;
+ display: flex;
+ flex-direction: column;
}
.components-list {
@@ -194,6 +226,7 @@ const handleComponentClick = (component) => {
.components-group {
margin-bottom: 18px;
}
+
.group-title {
font-weight: bold;
font-size: 15px;
diff --git a/src/components/flow/FlowEditor.vue b/src/components/flow/FlowEditor.vue
index 22eda86..d6e81f4 100644
--- a/src/components/flow/FlowEditor.vue
+++ b/src/components/flow/FlowEditor.vue
@@ -6,13 +6,13 @@ import { Controls } from '@vue-flow/controls';
import '@vue-flow/core/dist/style.css';
import '@vue-flow/core/dist/theme-default.css';
import '@vue-flow/controls/dist/style.css';
-import ComponentsPanel from './ComponentsPanel.vue';
import PropertyPanel from './PropertyPanel.vue';
import ShikigamiSelectNode from './nodes/yys/ShikigamiSelectNode.vue';
import YuhunSelectNode from './nodes/yys/YuhunSelectNode.vue';
import PropertySelectNode from './nodes/yys/PropertySelectNode.vue';
import ImageNode from './nodes/common/ImageNode.vue';
import TextNode from './nodes/common/TextNode.vue';
+import useDragAndDrop from '@/ts/useDnD';
const props = defineProps({
height: {
@@ -21,8 +21,6 @@ const props = defineProps({
}
});
-const emit = defineEmits(['open-shikigami-select', 'open-yuhun-select', 'open-property-select']);
-
// 设置节点类型
const nodeTypes = shallowRef({
shikigamiSelect: markRaw(ShikigamiSelectNode),
@@ -47,6 +45,9 @@ const { nodes, edges, onNodesChange, onEdgesChange, onConnect, addNodes, setTran
nodeTypes
});
+// 使用拖拽功能
+const { onDragOver, onDrop } = useDragAndDrop();
+
// 右键菜单相关
const contextMenu = ref({
show: false,
@@ -55,138 +56,6 @@ const contextMenu = ref({
nodeId: null
});
-// 处理拖拽放置
-const handleDrop = (event) => {
- event.preventDefault();
-
- try {
- // 获取拖拽数据
- const nodeData = JSON.parse(event.dataTransfer.getData('application/json'));
-
- // 获取画布元素
- const flowContainer = event.currentTarget;
- const bounds = flowContainer.getBoundingClientRect();
-
- // 获取画布的缩放和偏移信息
- const { x: viewportX, y: viewportY, zoom } = getViewport();
-
- // 计算相对于画布的位置,并考虑缩放和偏移
- const position = {
- x: (event.clientX - bounds.left - viewportX) / zoom,
- y: (event.clientY - bounds.top - viewportY) / zoom
- };
-
- // 创建新节点
- const newNode = {
- ...nodeData,
- position,
- selected: true // 设置节点为选中状态
- };
-
- // 添加节点
- handleAddNode(newNode);
- } catch (error) {
- console.error('拖拽放置处理失败:', error);
- }
-};
-
-// 处理拖拽悬停
-const handleDragOver = (event) => {
- event.preventDefault();
- event.dataTransfer.dropEffect = 'copy';
-};
-
-// 处理添加节点
-const handleAddNode = (newNode) => {
- // 取消所有现有节点的选中状态
- nodes.value.forEach(node => {
- if (node.selected) {
- updateNode(node.id, { selected: false });
- }
- });
-
- // 根据节点类型设置初始数据
- let initialData = {};
- switch (newNode.type) {
- case 'shikigamiSelect':
- initialData = {
- shikigami: { name: '未选择式神', avatar: '', rarity: '' }
- };
- break;
- case 'yuhunSelect':
- initialData = {
- yuhun: { name: '未选择御魂', avatar: '', type: '' }
- };
- break;
- case 'propertySelect':
- initialData = {
- property: {
- type: '未选择',
- priority: 'optional',
- description: '',
- value: 0,
- valueType: '',
- levelRequired: "40",
- skillRequiredMode: "all",
- skillRequired: ["5", "5", "5"],
- yuhun: {
- yuhunSetEffect: [],
- target: "1",
- property2: ["Attack"],
- property4: ["Attack"],
- property6: ["Crit", "CritDamage"],
- },
- expectedDamage: 0,
- survivalRate: 50,
- damageType: "balanced"
- }
- };
- break;
- case 'imageNode':
- initialData = {
- url: '',
- width: 180,
- height: 120
- };
- break;
- case 'textNode':
- initialData = {
- html: '
双击右侧可编辑文字
',
- width: 200,
- height: 120
- };
- break;
- }
-
- // 添加新节点,并设置为选中状态
- addNodes([{
- ...newNode,
- selected: true,
- data: {
- ...newNode.data,
- ...initialData
- }
- }]);
-
- // 重新设置视图,使新节点可见
- setTransform({ x: 0, y: 0, zoom: 1 });
-};
-
-// 处理从属性面板打开式神选择
-const handleOpenShikigamiSelect = (node) => {
- emit('open-shikigami-select', node);
-};
-
-// 处理从属性面板打开御魂选择
-const handleOpenYuhunSelect = (node) => {
- emit('open-yuhun-select', node);
-};
-
-// 处理从属性面板打开属性选择
-const handleOpenPropertySelect = (node) => {
- emit('open-property-select', node);
-};
-
// 处理节点右键点击
const handleNodeContextMenu = (event) => {
const { event: mouseEvent, node } = event;
@@ -256,26 +125,24 @@ const handleLayerOrder = (action) => {
contextMenu.value.show = false;
};
+defineExpose({
+ handleAddNode: addNodes,
+ getViewport
+});
+
onMounted(() => {
console.log('FlowEditor 组件已挂载');
- // 添加全局点击事件监听
- document.addEventListener('click', handleClickOutside);
});
onUnmounted(() => {
// 移除事件监听
- document.removeEventListener('click', handleClickOutside);
+ // document.removeEventListener('click', handleClickOutside);
});
-
-
-
{
@edges-change="onEdgesChange"
@connect="onConnect"
fit-view-on-init
- @drop="handleDrop"
- @dragover="handleDragOver"
+ @drop="onDrop"
+ @dragover="onDragOver"
@node-context-menu="handleNodeContextMenu"
@pane-context-menu="handlePaneContextMenu"
>
@@ -314,9 +181,6 @@ onUnmounted(() => {
@@ -334,39 +198,12 @@ onUnmounted(() => {
height: 100%;
}
-.components-sidebar {
- width: 230px;
- background-color: #f5f7fa;
- border-right: 1px solid #e4e7ed;
- overflow-y: auto;
-}
-
.flow-container {
flex: 1;
position: relative;
overflow: hidden;
}
-:deep(.vue-flow__node) {
- padding: 10px;
- border-radius: 5px;
- background-color: white;
- border: 1px solid #1a192b;
- color: #333;
- text-align: center;
-}
-
-:deep(.vue-flow__node-input) {
- background-color: #f6fafd;
- border: 1px solid #66B1FF;
-}
-
-:deep(.flow-panel) {
- background-color: white;
- padding: 10px;
- border-radius: 5px;
- box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
-}
.context-menu {
position: fixed;
diff --git a/src/ts/useDnD.ts b/src/ts/useDnD.ts
new file mode 100644
index 0000000..3e0a14f
--- /dev/null
+++ b/src/ts/useDnD.ts
@@ -0,0 +1,100 @@
+import { useVueFlow } from '@vue-flow/core'
+import { ref, watch } from 'vue'
+
+let id = 0
+
+function getId() {
+ return `dndnode_${id++}`
+}
+
+const state = {
+ draggedType: ref
(null),
+ isDragOver: ref(false),
+ isDragging: ref(false),
+}
+
+export default function useDragAndDrop() {
+ const { draggedType, isDragOver, isDragging } = state
+ const { addNodes, screenToFlowCoordinate, onNodesInitialized, updateNode } = useVueFlow()
+
+ watch(isDragging, (dragging) => {
+ document.body.style.userSelect = dragging ? 'none' : ''
+ })
+
+ function onDragStart(event: DragEvent, nodeData: any) {
+ if (event.dataTransfer) {
+ event.dataTransfer.setData('application/json', JSON.stringify(nodeData))
+ event.dataTransfer.effectAllowed = 'move'
+ }
+
+ draggedType.value = nodeData.type
+ isDragging.value = true
+
+ document.addEventListener('drop', onDragEnd)
+ }
+
+ function onDragOver(event: DragEvent) {
+ event.preventDefault()
+
+ if (draggedType.value) {
+ isDragOver.value = true
+
+ if (event.dataTransfer) {
+ event.dataTransfer.dropEffect = 'move'
+ }
+ }
+ }
+
+ function onDragLeave() {
+ isDragOver.value = false
+ }
+
+ function onDragEnd() {
+ isDragging.value = false
+ isDragOver.value = false
+ draggedType.value = null
+ document.removeEventListener('drop', onDragEnd)
+ }
+
+ function onDrop(event: DragEvent) {
+ const position = screenToFlowCoordinate({
+ x: event.clientX,
+ y: event.clientY,
+ })
+
+ try {
+ const nodeData = JSON.parse(event.dataTransfer?.getData('application/json') || '{}')
+ const nodeId = getId()
+
+ const newNode = {
+ id: nodeId,
+ ...nodeData,
+ position,
+ }
+
+ const { off } = onNodesInitialized(() => {
+ updateNode(nodeId, (node) => ({
+ position: {
+ x: node.position.x - node.dimensions.width / 2,
+ y: node.position.y - node.dimensions.height / 2
+ },
+ }))
+ off()
+ })
+
+ addNodes(newNode)
+ } catch (error) {
+ console.error('拖拽放置处理失败:', error)
+ }
+ }
+
+ return {
+ draggedType,
+ isDragOver,
+ isDragging,
+ onDragStart,
+ onDragLeave,
+ onDragOver,
+ onDrop,
+ }
+}
\ No newline at end of file