mirror of
https://github.com/Powerful-517/yys-editor.git
synced 2025-08-23 08:04:50 +00:00
自定义节点注册,属性编辑对话框交互,持久化配置
This commit is contained in:
20
src/App.vue
20
src/App.vue
@@ -123,6 +123,21 @@ watch(
|
||||
}
|
||||
);
|
||||
|
||||
const handleDropOnCanvas = (event: DragEvent) => {
|
||||
event.preventDefault();
|
||||
const nodeData = JSON.parse(event.dataTransfer?.getData('application/json') || '{}');
|
||||
// 计算画布坐标(这里简单用鼠标坐标,后续可结合 LogicFlow 视口变换优化)
|
||||
const rect = (event.target as HTMLElement).getBoundingClientRect();
|
||||
const x = event.clientX - rect.left;
|
||||
const y = event.clientY - rect.top;
|
||||
const id = `node-${Date.now()}-${Math.floor(Math.random() * 10000)}`;
|
||||
filesStore.addNode({ id, type: nodeData.type, x, y, ...nodeData.data });
|
||||
};
|
||||
|
||||
const handleDragOverOnCanvas = (event: DragEvent) => {
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -149,7 +164,10 @@ watch(
|
||||
:name="file.name.toString()"
|
||||
/>
|
||||
</el-tabs>
|
||||
<div id="main-container" :style="{ height: contentHeight, overflow: 'auto' }">
|
||||
<div id="main-container" :style="{ height: contentHeight, overflow: 'auto' }"
|
||||
@dragover="handleDragOverOnCanvas"
|
||||
@drop="handleDropOnCanvas"
|
||||
>
|
||||
<FlowEditor
|
||||
ref="flowEditorRef"
|
||||
:height="contentHeight"
|
||||
|
@@ -16,11 +16,8 @@ const filesStore = useFilesStore();
|
||||
:currentShikigami="dialogs.shikigami.data"
|
||||
@closeSelectShikigami="closeDialog('shikigami')"
|
||||
@updateShikigami="data => {
|
||||
if (dialogs.shikigami.node?.id) {
|
||||
filesStore.updateNode(dialogs.shikigami.node.id, { data: { ...dialogs.shikigami.node.data, shikigami: data } })
|
||||
}
|
||||
dialogs.shikigami.callback?.(data, dialogs.shikigami.node)
|
||||
closeDialog('shikigami')
|
||||
dialogs.shikigami.callback?.(data);
|
||||
closeDialog('shikigami');
|
||||
}"
|
||||
/>
|
||||
<YuhunSelect
|
||||
@@ -28,12 +25,9 @@ const filesStore = useFilesStore();
|
||||
:showSelectYuhun="dialogs.yuhun.show"
|
||||
:currentYuhun="dialogs.yuhun.data"
|
||||
@closeSelectYuhun="closeDialog('yuhun')"
|
||||
@updateYuhun="data => {
|
||||
if (dialogs.yuhun.node?.id) {
|
||||
filesStore.updateNode(dialogs.yuhun.node.id, { data: { ...dialogs.yuhun.node.data, yuhun: data } })
|
||||
}
|
||||
dialogs.yuhun.callback?.(data, dialogs.yuhun.node);
|
||||
closeDialog('yuhun')
|
||||
@updateYuhun="data => {
|
||||
dialogs.yuhun.callback?.(data);
|
||||
closeDialog('yuhun');
|
||||
}"
|
||||
/>
|
||||
<PropertySelect
|
||||
@@ -41,12 +35,9 @@ const filesStore = useFilesStore();
|
||||
:showPropertySelect="dialogs.property.show"
|
||||
:currentProperty="dialogs.property.data"
|
||||
@closePropertySelect="closeDialog('property')"
|
||||
@updateProperty="data => {
|
||||
if (dialogs.property.node?.id) {
|
||||
filesStore.updateNode(dialogs.property.node.id, { data: { ...dialogs.property.node.data, property: data } })
|
||||
}
|
||||
dialogs.property.callback?.(data, dialogs.property.node);
|
||||
closeDialog('property')
|
||||
@updateProperty="data => {
|
||||
dialogs.property.callback?.(data);
|
||||
closeDialog('property');
|
||||
}"
|
||||
/>
|
||||
</template>
|
@@ -1,102 +1,151 @@
|
||||
<template>
|
||||
<div class="container" ref="containerRef" :style="{ height }"></div>
|
||||
<div class="editor-layout" :style="{ height }">
|
||||
<!-- 中间流程图区域 -->
|
||||
<div class="flow-container">
|
||||
<div class="container" ref="containerRef" :style="{ height: '100%' }"></div>
|
||||
<!-- 右键菜单 -->
|
||||
<Teleport to="body">
|
||||
<div v-if="contextMenu.show"
|
||||
class="context-menu"
|
||||
:style="{ left: contextMenu.x + 'px', top: contextMenu.y + 'px' }"
|
||||
@click.stop>
|
||||
<div class="menu-item" @click="handleLayerOrder('bringToFront')">移至最前</div>
|
||||
<div class="menu-item" @click="handleLayerOrder('sendToBack')">移至最后</div>
|
||||
<div class="menu-item" @click="handleLayerOrder('bringForward')">上移一层</div>
|
||||
<div class="menu-item" @click="handleLayerOrder('sendBackward')">下移一层</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</div>
|
||||
<!-- 右侧属性面板 -->
|
||||
<PropertyPanel :height="height" :node="selectedNode" :lf="lf" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onMounted, onBeforeUnmount, defineExpose } from 'vue';
|
||||
import type { PropType } from 'vue';
|
||||
import LogicFlow from '@logicflow/core';
|
||||
import LogicFlow, { EventType } from '@logicflow/core';
|
||||
import '@logicflow/core/lib/style/index.css';
|
||||
|
||||
// 类型定义放在 import 之后,避免顶层 await 错误
|
||||
|
||||
type NodeData = {
|
||||
id: string;
|
||||
type: string;
|
||||
x: number;
|
||||
y: number;
|
||||
text?: string;
|
||||
properties?: Record<string, any>;
|
||||
};
|
||||
|
||||
type EdgeData = {
|
||||
id: string;
|
||||
type: string;
|
||||
sourceNodeId: string;
|
||||
targetNodeId: string;
|
||||
properties?: Record<string, any>;
|
||||
};
|
||||
|
||||
type Viewport = {
|
||||
x: number;
|
||||
y: number;
|
||||
zoom: number;
|
||||
};
|
||||
import { register } from '@logicflow/vue-node-registry';
|
||||
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 PropertyPanel from './PropertyPanel.vue';
|
||||
import {useFilesStore} from "@/ts/useStore";
|
||||
|
||||
const props = defineProps<{
|
||||
nodes: NodeData[];
|
||||
edges: EdgeData[];
|
||||
viewport?: Viewport;
|
||||
nodes: any[];
|
||||
edges: any[];
|
||||
viewport?: { x: number; y: number; zoom: number };
|
||||
height?: string;
|
||||
}>();
|
||||
|
||||
const filesStore = useFilesStore();
|
||||
const containerRef = ref<HTMLElement | null>(null);
|
||||
let lf: LogicFlow | null = null;
|
||||
const lf = ref<LogicFlow | null>(null);
|
||||
|
||||
// 右键菜单相关
|
||||
const contextMenu = ref({
|
||||
show: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
nodeId: null
|
||||
});
|
||||
|
||||
// 当前选中节点
|
||||
const selectedNode = ref<any>(null);
|
||||
|
||||
// 注册自定义节点
|
||||
function registerNodes(lfInstance: LogicFlow) {
|
||||
register({ type: 'shikigamiSelect', component: ShikigamiSelectNode }, lfInstance);
|
||||
register({ type: 'yuhunSelect', component: YuhunSelectNode }, lfInstance);
|
||||
register({ type: 'propertySelect', component: PropertySelectNode }, lfInstance);
|
||||
|
||||
// register({ type: 'imageNode', component: ImageNode }, lfInstance);
|
||||
// register({ type: 'textNode', component: TextNode }, lfInstance);
|
||||
}
|
||||
|
||||
// 初始化 LogicFlow
|
||||
onMounted(() => {
|
||||
lf = new LogicFlow({
|
||||
lf.value = new LogicFlow({
|
||||
container: containerRef.value as HTMLElement,
|
||||
grid: true,
|
||||
});
|
||||
// lf.zoom(2);
|
||||
// zoom(2);
|
||||
registerNodes(lf.value);
|
||||
renderFlow();
|
||||
filesStore.setLogicFlowInstance(lf.value);
|
||||
|
||||
// 监听节点点击事件,更新 selectedNode
|
||||
lf.value.on(EventType.NODE_CLICK, ({ data }) => {
|
||||
selectedNode.value = data;
|
||||
});
|
||||
|
||||
// if (props.viewport) setViewport(props.viewport);
|
||||
// 监听空白点击事件,取消选中
|
||||
lf.value.on(EventType.BLANK_CLICK, () => {
|
||||
selectedNode.value = null;
|
||||
});
|
||||
|
||||
// 节点属性改变,如果当前节点是选中节点,则同步更新 selectedNode
|
||||
lf.value.on(EventType.NODE_PROPERTIES_CHANGE, (data) => {
|
||||
const nodeId = data.id || (data.value && data.value.id);
|
||||
if (selectedNode.value && nodeId === selectedNode.value.id) {
|
||||
if (data.value) {
|
||||
selectedNode.value = data.value;
|
||||
} else if (data.properties) {
|
||||
selectedNode.value = {
|
||||
...selectedNode.value,
|
||||
properties: data.properties
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 右键事件
|
||||
lf.value.on('node:contextmenu', handleNodeContextMenu);
|
||||
lf.value.on('blank:contextmenu', handlePaneContextMenu);
|
||||
});
|
||||
|
||||
// 销毁 LogicFlow
|
||||
onBeforeUnmount(() => {
|
||||
lf?.destroy();
|
||||
lf = null;
|
||||
lf.value?.destroy();
|
||||
lf.value = null;
|
||||
});
|
||||
|
||||
// 响应式更新 nodes/edges
|
||||
watch(
|
||||
() => [props.nodes, props.edges],
|
||||
() => {
|
||||
renderFlow();
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
// watch(
|
||||
// () => [props.nodes, props.edges],
|
||||
// () => {
|
||||
// renderFlow();
|
||||
// },
|
||||
// { deep: true }
|
||||
// );
|
||||
|
||||
// 响应式更新 viewport
|
||||
watch(
|
||||
() => props.viewport,
|
||||
(val) => {
|
||||
if (val) setViewport(val);
|
||||
}
|
||||
() => props.viewport,
|
||||
(val) => {
|
||||
if (val) setViewport(val);
|
||||
}
|
||||
);
|
||||
|
||||
function renderFlow() {
|
||||
if (!lf) return;
|
||||
lf.render({
|
||||
if (!lf.value) return;
|
||||
lf.value.render({
|
||||
nodes: props.nodes,
|
||||
edges: props.edges,
|
||||
});
|
||||
}
|
||||
|
||||
function setViewport(viewport: Viewport) {
|
||||
if (!lf || !viewport) return;
|
||||
lf.zoom(viewport.zoom);
|
||||
// lf.focusOn({ x: viewport.x, y: viewport.y });
|
||||
function setViewport(viewport?: { x: number; y: number; zoom: number }) {
|
||||
if (!lf.value || !viewport) return;
|
||||
lf.value.zoom(viewport.zoom);
|
||||
// lf.value.focusOn({ x: viewport.x, y: viewport.y });
|
||||
}
|
||||
|
||||
function getViewport(): Viewport {
|
||||
if (!lf) return { x: 0, y: 0, zoom: 1 };
|
||||
const t = lf.getTransform();
|
||||
function getViewport() {
|
||||
if (!lf.value) return { x: 0, y: 0, zoom: 1 };
|
||||
const t = lf.value.getTransform();
|
||||
return {
|
||||
x: t.TRANSLATE_X,
|
||||
y: t.TRANSLATE_Y,
|
||||
@@ -104,34 +153,82 @@ function getViewport(): Viewport {
|
||||
};
|
||||
}
|
||||
|
||||
function handleAddNode(node: NodeData) {
|
||||
if (!lf) return;
|
||||
lf.addNode(node);
|
||||
// 右键菜单相关
|
||||
function handleNodeContextMenu({ data, e }: { data: any; e: MouseEvent }) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
contextMenu.value = {
|
||||
show: true,
|
||||
x: e.clientX,
|
||||
y: e.clientY,
|
||||
nodeId: data.id
|
||||
};
|
||||
}
|
||||
function handlePaneContextMenu({ e }: { e: MouseEvent }) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
contextMenu.value.show = false;
|
||||
}
|
||||
function handleLayerOrder(action: string) {
|
||||
// 这里需要结合你的 store 或数据结构实现节点顺序调整
|
||||
contextMenu.value.show = false;
|
||||
}
|
||||
|
||||
function getGraphRawData() {
|
||||
if (!lf) return null;
|
||||
return lf.getGraphRawData();
|
||||
return lf.value.getGraphRawData();
|
||||
}
|
||||
|
||||
function renderRawData(data: any) {
|
||||
if (!lf) return;
|
||||
lf.renderRawData(data);
|
||||
lf.value.renderRawData(data);
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
getViewport,
|
||||
setViewport, // 新增暴露
|
||||
handleAddNode,
|
||||
setViewport,
|
||||
renderFlow,
|
||||
getGraphRawData,
|
||||
renderRawData,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.editor-layout {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
.flow-container {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.container {
|
||||
width: 100%;
|
||||
min-height: 300px;
|
||||
background: #fff;
|
||||
height: 100%;
|
||||
}
|
||||
.context-menu {
|
||||
position: fixed;
|
||||
background: white;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
padding: 5px 0;
|
||||
z-index: 9999;
|
||||
min-width: 120px;
|
||||
user-select: none;
|
||||
}
|
||||
.menu-item {
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.menu-item:hover {
|
||||
background-color: #f5f7fa;
|
||||
color: #409eff;
|
||||
}
|
||||
</style>
|
@@ -1,83 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, watch } from 'vue';
|
||||
import { Handle, Position, useVueFlow } from '@vue-flow/core';
|
||||
import { NodeResizer } from '@vue-flow/node-resizer'
|
||||
import '@vue-flow/node-resizer/dist/style.css';
|
||||
import { ref, watch, onMounted, inject } from 'vue';
|
||||
import { EventType } from '@logicflow/core';
|
||||
|
||||
const props = defineProps({
|
||||
data: Object,
|
||||
id: String,
|
||||
selected: Boolean
|
||||
});
|
||||
const currentProperty = ref({ type: '未选择', priority: '可选' });
|
||||
|
||||
// 获取Vue Flow的实例和节点更新方法
|
||||
const { findNode, updateNode } = useVueFlow();
|
||||
const getNode = inject('getNode') as (() => any) | undefined;
|
||||
const getGraph = inject('getGraph') as (() => any) | undefined;
|
||||
|
||||
// 属性信息保存在节点数据中
|
||||
const currentProperty = ref({
|
||||
type: '未选择',
|
||||
value: 0,
|
||||
valueType: '',
|
||||
priority: '可选',
|
||||
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",
|
||||
description: '',
|
||||
});
|
||||
|
||||
// 节点尺寸
|
||||
const nodeWidth = ref(180);
|
||||
const nodeHeight = ref(180);
|
||||
|
||||
// 监听props.data的变化
|
||||
watch(() => props.data, (newData) => {
|
||||
if (newData && newData.property) {
|
||||
currentProperty.value = newData.property;
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
// 更新属性信息的方法(将由App.vue调用)
|
||||
const updateNodeProperty = (property) => {
|
||||
currentProperty.value = property;
|
||||
};
|
||||
|
||||
// 备用方案:通过全局事件总线监听更新
|
||||
const handlePropertyUpdate = (event) => {
|
||||
const { nodeId, property } = event.detail;
|
||||
if (nodeId === props.id) {
|
||||
updateNodeProperty(property);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
console.log('PropertySelectNode mounted:', props.id);
|
||||
// 添加全局事件监听
|
||||
window.addEventListener('update-property', handlePropertyUpdate);
|
||||
|
||||
// 初始化时检查是否有数据
|
||||
if (props.data && props.data.property) {
|
||||
currentProperty.value = props.data.property;
|
||||
const node = getNode?.();
|
||||
const graph = getGraph?.();
|
||||
|
||||
if (node?.properties?.property) {
|
||||
currentProperty.value = node.properties.property;
|
||||
}
|
||||
|
||||
graph?.eventCenter.on(EventType.NODE_PROPERTIES_CHANGE, (eventData: any) => {
|
||||
if (eventData.id === node.id && eventData.properties?.property) {
|
||||
currentProperty.value = eventData.properties.property;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
// 移除全局事件监听器
|
||||
window.removeEventListener('update-property', handlePropertyUpdate);
|
||||
});
|
||||
|
||||
// 获取属性类型显示名称
|
||||
// 辅助函数
|
||||
const getPropertyTypeName = () => {
|
||||
const typeMap = {
|
||||
const typeMap: Record<string, string> = {
|
||||
'attack': '攻击',
|
||||
'health': '生命',
|
||||
'defense': '防御',
|
||||
@@ -88,165 +36,38 @@ const getPropertyTypeName = () => {
|
||||
'effectResist': '效果抵抗',
|
||||
'未选择': '未选择'
|
||||
};
|
||||
|
||||
return typeMap[currentProperty.value.type] || currentProperty.value.type;
|
||||
};
|
||||
|
||||
// 获取优先级显示名称
|
||||
const getPriorityName = () => {
|
||||
const priorityMap = {
|
||||
const priorityMap: Record<string, string> = {
|
||||
'required': '必须',
|
||||
'recommended': '推荐',
|
||||
'optional': '可选'
|
||||
};
|
||||
|
||||
return priorityMap[currentProperty.value.priority] || currentProperty.value.priority;
|
||||
};
|
||||
|
||||
// 获取格式化的属性值显示
|
||||
const getFormattedValue = () => {
|
||||
const type = currentProperty.value.type;
|
||||
|
||||
if (type === '未选择') return '';
|
||||
|
||||
// 根据属性类型获取相应的值
|
||||
let value = 0;
|
||||
let isPercentage = false;
|
||||
|
||||
switch (type) {
|
||||
case 'attack':
|
||||
value = currentProperty.value.attackValue || 0;
|
||||
isPercentage = currentProperty.value.attackType === 'percentage';
|
||||
break;
|
||||
case 'health':
|
||||
value = currentProperty.value.healthValue || 0;
|
||||
isPercentage = currentProperty.value.healthType === 'percentage';
|
||||
break;
|
||||
case 'defense':
|
||||
value = currentProperty.value.defenseValue || 0;
|
||||
isPercentage = currentProperty.value.defenseType === 'percentage';
|
||||
break;
|
||||
case 'speed':
|
||||
value = currentProperty.value.speedValue || 0;
|
||||
break;
|
||||
case 'crit':
|
||||
value = currentProperty.value.critRate || 0;
|
||||
isPercentage = true;
|
||||
break;
|
||||
case 'critDmg':
|
||||
value = currentProperty.value.critDmg || 0;
|
||||
isPercentage = true;
|
||||
break;
|
||||
case 'effectHit':
|
||||
value = currentProperty.value.effectHitValue || 0;
|
||||
isPercentage = true;
|
||||
break;
|
||||
case 'effectResist':
|
||||
value = currentProperty.value.effectResistValue || 0;
|
||||
isPercentage = true;
|
||||
break;
|
||||
}
|
||||
|
||||
return isPercentage ? `${value}%` : value.toString();
|
||||
};
|
||||
|
||||
// 获取式神技能要求显示
|
||||
const getSkillRequirementText = () => {
|
||||
const mode = currentProperty.value.skillRequiredMode;
|
||||
if (mode === 'all') {
|
||||
return '技能: 全满';
|
||||
} else if (mode === '111') {
|
||||
return '技能: 111';
|
||||
} else if (mode === 'custom') {
|
||||
return `技能: ${currentProperty.value.skillRequired.join('/')}`;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
// 获取御魂套装信息
|
||||
const getYuhunSetInfo = () => {
|
||||
const sets = currentProperty.value.yuhun?.yuhunSetEffect;
|
||||
if (!sets || sets.length === 0) return '';
|
||||
|
||||
return `御魂: ${sets.length}套`;
|
||||
};
|
||||
|
||||
// 获取显示的等级信息
|
||||
const getLevelText = () => {
|
||||
const level = currentProperty.value.levelRequired;
|
||||
return level === '0' ? '等级: 献祭' : `等级: ${level}`;
|
||||
};
|
||||
|
||||
// 处理调整大小
|
||||
const handleResize = (event, { width, height }) => {
|
||||
// 更新本地状态
|
||||
nodeWidth.value = width;
|
||||
nodeHeight.value = height;
|
||||
|
||||
// 更新Vue Flow中的节点
|
||||
const node = findNode(props.id);
|
||||
if (node) {
|
||||
const updatedNode = {
|
||||
...node,
|
||||
style: {
|
||||
...node.style,
|
||||
width: `${width}px`,
|
||||
height: `${height}px`
|
||||
}
|
||||
};
|
||||
updateNode(props.id, updatedNode);
|
||||
}
|
||||
};
|
||||
|
||||
// 导出方法,使父组件可以调用
|
||||
defineExpose({
|
||||
updateNodeProperty
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NodeResizer
|
||||
v-if="selected"
|
||||
:min-width="150"
|
||||
:min-height="150"
|
||||
:max-width="300"
|
||||
:max-height="300"
|
||||
/>
|
||||
<div class="property-node" :class="[currentProperty.priority ? `priority-${currentProperty.priority}` : '']" >
|
||||
<!-- 输入连接点 -->
|
||||
<Handle type="target" position="left" :id="`${id}-target`" />
|
||||
|
||||
<div class="property-node" :class="[currentProperty.priority ? `priority-${currentProperty.priority}` : '']">
|
||||
<div class="node-content">
|
||||
<div class="node-header">
|
||||
<div class="node-title">属性要求</div>
|
||||
</div>
|
||||
|
||||
<div class="node-body">
|
||||
<div class="property-main">
|
||||
<div class="property-type">{{ getPropertyTypeName() }}</div>
|
||||
<div v-if="currentProperty.type !== '未选择'" class="property-value">{{ getFormattedValue() }}</div>
|
||||
<div v-if="currentProperty.type !== '未选择'" class="property-value">{{ currentProperty.value }}</div>
|
||||
<div v-else class="property-placeholder">点击设置属性</div>
|
||||
</div>
|
||||
|
||||
<div class="property-details" v-if="currentProperty.type !== '未选择'">
|
||||
<div class="property-priority">优先级: {{ getPriorityName() }}</div>
|
||||
|
||||
<!-- 额外信息展示 -->
|
||||
<div class="property-extra-info" v-if="currentProperty.levelRequired">
|
||||
<div>{{ getLevelText() }}</div>
|
||||
<div>{{ getSkillRequirementText() }}</div>
|
||||
<div>{{ getYuhunSetInfo() }}</div>
|
||||
</div>
|
||||
|
||||
<div class="property-description" v-if="currentProperty.description">
|
||||
{{ currentProperty.description }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 输出连接点 -->
|
||||
<Handle type="source" position="right" :id="`${id}-source`" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -258,7 +79,6 @@ defineExpose({
|
||||
min-width: 180px;
|
||||
min-height: 180px;
|
||||
}
|
||||
|
||||
.node-content {
|
||||
position: relative;
|
||||
background-color: white;
|
||||
@@ -266,89 +86,28 @@ defineExpose({
|
||||
border-radius: 4px;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
min-width: 180px;
|
||||
min-height: 180px;
|
||||
}
|
||||
|
||||
:deep(.vue-flow__node-resizer) {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
:deep(.vue-flow__node-resizer-handle) {
|
||||
position: absolute;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background-color: #409EFF;
|
||||
border: 1px solid white;
|
||||
border-radius: 50%;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
:deep(.vue-flow__node-resizer-handle.top-left) {
|
||||
top: -4px;
|
||||
left: -4px;
|
||||
cursor: nwse-resize;
|
||||
}
|
||||
|
||||
:deep(.vue-flow__node-resizer-handle.top-right) {
|
||||
top: -4px;
|
||||
right: -4px;
|
||||
cursor: nesw-resize;
|
||||
}
|
||||
|
||||
:deep(.vue-flow__node-resizer-handle.bottom-left) {
|
||||
bottom: -4px;
|
||||
left: -4px;
|
||||
cursor: nesw-resize;
|
||||
}
|
||||
|
||||
:deep(.vue-flow__node-resizer-handle.bottom-right) {
|
||||
bottom: -4px;
|
||||
right: -4px;
|
||||
cursor: nwse-resize;
|
||||
}
|
||||
|
||||
.priority-required {
|
||||
border: 2px solid #f56c6c;
|
||||
}
|
||||
|
||||
.priority-recommended {
|
||||
border: 2px solid #67c23a;
|
||||
}
|
||||
|
||||
.priority-optional {
|
||||
border: 1px solid #dcdfe6;
|
||||
}
|
||||
|
||||
.node-header {
|
||||
padding: 8px 10px;
|
||||
background-color: #f0f7ff;
|
||||
border-bottom: 1px solid #dcdfe6;
|
||||
border-radius: 4px 4px 0 0;
|
||||
}
|
||||
|
||||
.node-title {
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.node-body {
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.property-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -356,20 +115,17 @@ defineExpose({
|
||||
width: 100%;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.property-type {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.property-value {
|
||||
font-size: 16px;
|
||||
color: #409eff;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.property-placeholder {
|
||||
width: 120px;
|
||||
height: 40px;
|
||||
@@ -383,29 +139,16 @@ defineExpose({
|
||||
margin: 8px 0;
|
||||
transition: width 0.2s, height 0.2s;
|
||||
}
|
||||
|
||||
.property-details {
|
||||
width: 100%;
|
||||
border-top: 1px dashed #ebeef5;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.property-priority {
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.property-extra-info {
|
||||
font-size: 11px;
|
||||
color: #909399;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.property-extra-info > div {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.property-description {
|
||||
font-size: 11px;
|
||||
color: #606266;
|
||||
@@ -414,4 +157,4 @@ defineExpose({
|
||||
padding-top: 5px;
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
</style>
|
@@ -1,130 +1,71 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, watch } from 'vue';
|
||||
import { Handle, Position, useVueFlow } from '@vue-flow/core';
|
||||
import { NodeResizer } from '@vue-flow/node-resizer';
|
||||
import '@vue-flow/node-resizer/dist/style.css';
|
||||
import {ref, onMounted, inject, watch} from 'vue';
|
||||
import { EventType } from '@logicflow/core';
|
||||
|
||||
const props = defineProps({
|
||||
data: Object,
|
||||
id: String,
|
||||
selected: Boolean
|
||||
});
|
||||
|
||||
// 获取Vue Flow的实例和节点更新方法
|
||||
const { findNode, updateNode } = useVueFlow();
|
||||
|
||||
// 式神信息保存在节点数据中
|
||||
const currentShikigami = ref({ name: '未选择式神', avatar: '', rarity: '' });
|
||||
|
||||
// 节点尺寸
|
||||
const nodeWidth = ref(180);
|
||||
const nodeHeight = ref(180);
|
||||
const getNode = inject('getNode') as (() => any) | undefined;
|
||||
const getGraph = inject('getGraph') as (() => any) | undefined;
|
||||
|
||||
// 监听props.data的变化
|
||||
watch(() => props.data, (newData) => {
|
||||
if (newData && newData.shikigami) {
|
||||
currentShikigami.value = newData.shikigami;
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
// 更新式神信息的方法(将由App.vue调用)
|
||||
const updateNodeShikigami = (shikigami) => {
|
||||
currentShikigami.value = shikigami;
|
||||
};
|
||||
|
||||
// 备用方案:通过全局事件总线监听更新
|
||||
const handleShikigamiUpdate = (event) => {
|
||||
const { nodeId, shikigami } = event.detail;
|
||||
if (nodeId === props.id) {
|
||||
updateNodeShikigami(shikigami);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
console.log('ShikigamiSelectNode mounted:', props.id);
|
||||
// 添加全局事件监听
|
||||
window.addEventListener('update-shikigami', handleShikigamiUpdate);
|
||||
const node = getNode?.();
|
||||
const graph = getGraph?.();
|
||||
|
||||
// 初始化时检查是否有数据
|
||||
if (props.data && props.data.shikigami) {
|
||||
currentShikigami.value = props.data.shikigami;
|
||||
// 初始化
|
||||
if (node?.properties?.shikigami) {
|
||||
currentShikigami.value = node.properties.shikigami;
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
// 移除全局事件监听器
|
||||
window.removeEventListener('update-shikigami', handleShikigamiUpdate);
|
||||
// 监听属性变化
|
||||
graph?.eventCenter.on(EventType.NODE_PROPERTIES_CHANGE, (eventData: any) => {
|
||||
if (eventData.id === node.id && eventData.properties?.shikigami) {
|
||||
currentShikigami.value = eventData.properties.shikigami;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 导出方法,使父组件可以调用
|
||||
defineExpose({
|
||||
updateNodeShikigami
|
||||
});
|
||||
|
||||
// Add Position enum usage to fix type error
|
||||
const position = Position;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NodeResizer
|
||||
v-if="selected"
|
||||
<div
|
||||
class="node-content"
|
||||
:style="{
|
||||
boxSizing: 'border-box',
|
||||
background: '#fff',
|
||||
borderRadius: '8px'
|
||||
}"
|
||||
>
|
||||
<img
|
||||
v-if="currentShikigami.avatar"
|
||||
:src="currentShikigami.avatar"
|
||||
:alt="currentShikigami.name"
|
||||
class="shikigami-image"
|
||||
/>
|
||||
|
||||
<Handle type="target" :position="position.Left" :id="`${id}-target`" />
|
||||
|
||||
<div class="node-content">
|
||||
<img
|
||||
v-if="currentShikigami.avatar"
|
||||
:src="currentShikigami.avatar"
|
||||
:alt="currentShikigami.name"
|
||||
class="shikigami-image"
|
||||
/>
|
||||
<div v-else class="placeholder-text">点击选择式神</div>
|
||||
<div class="name-text">{{ currentShikigami.name }}</div>
|
||||
</div>
|
||||
|
||||
<Handle type="source" :position="position.Right" :id="`${id}-source`" />
|
||||
<div v-else class="placeholder-text">点击选择式神</div>
|
||||
<div class="name-text">{{ currentShikigami.name }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.node-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.shikigami-image {
|
||||
width: 85%;
|
||||
height: 85%;
|
||||
object-fit: cover;
|
||||
width: 85%;
|
||||
height: 85%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.placeholder-text {
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.name-text {
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
:deep(.vue-flow__node-resizer) {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
:deep(.vue-flow__node-resizer-handle) {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background-color: #409EFF;
|
||||
border-radius: 50%;
|
||||
pointer-events: all;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
margin-top: 8px;
|
||||
}
|
||||
</style>
|
@@ -1,163 +1,68 @@
|
||||
<script setup lang="ts">
|
||||
import {ref, onMounted, onUnmounted, watch} from 'vue';
|
||||
import {Handle, Position, useVueFlow} from '@vue-flow/core';
|
||||
import {NodeResizer} from '@vue-flow/node-resizer'
|
||||
import '@vue-flow/node-resizer/dist/style.css';
|
||||
import { ref, watch, onMounted, inject } from 'vue';
|
||||
import { EventType } from '@logicflow/core';
|
||||
|
||||
const props = defineProps({
|
||||
data: Object,
|
||||
id: String,
|
||||
selected: Boolean
|
||||
});
|
||||
const currentYuhun = ref({ name: '未选择御魂', avatar: '', type: '' });
|
||||
|
||||
// 获取Vue Flow的实例和节点更新方法
|
||||
const {findNode, updateNode} = useVueFlow();
|
||||
const getNode = inject('getNode') as (() => any) | undefined;
|
||||
const getGraph = inject('getGraph') as (() => any) | undefined;
|
||||
|
||||
// 御魂信息保存在节点数据中
|
||||
const currentYuhun = ref({name: '未选择御魂', avatar: '', type: ''});
|
||||
|
||||
// 节点尺寸
|
||||
const nodeWidth = ref(180);
|
||||
const nodeHeight = ref(180);
|
||||
|
||||
// 监听props.data的变化
|
||||
watch(() => props.data, (newData) => {
|
||||
if (newData && newData.yuhun) {
|
||||
currentYuhun.value = newData.yuhun;
|
||||
}
|
||||
}, {immediate: true});
|
||||
|
||||
// 更新御魂信息的方法(将由App.vue调用)
|
||||
const updateNodeYuhun = (yuhun) => {
|
||||
currentYuhun.value = yuhun;
|
||||
};
|
||||
|
||||
// 备用方案:通过全局事件总线监听更新
|
||||
const handleYuhunUpdate = (event) => {
|
||||
const {nodeId, yuhun} = event.detail;
|
||||
if (nodeId === props.id) {
|
||||
updateNodeYuhun(yuhun);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理调整大小
|
||||
const handleResize = (event, {width, height}) => {
|
||||
// 更新本地状态
|
||||
nodeWidth.value = width;
|
||||
nodeHeight.value = height;
|
||||
|
||||
// 更新Vue Flow中的节点
|
||||
const node = findNode(props.id);
|
||||
if (node) {
|
||||
const updatedNode = {
|
||||
...node,
|
||||
style: {
|
||||
...node.style,
|
||||
width: `${width}px`,
|
||||
height: `${height}px`
|
||||
}
|
||||
};
|
||||
updateNode(props.id, updatedNode);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
console.log('YuhunSelectNode mounted:', props.id);
|
||||
// 添加全局事件监听
|
||||
window.addEventListener('update-yuhun', handleYuhunUpdate);
|
||||
const node = getNode?.();
|
||||
const graph = getGraph?.();
|
||||
|
||||
// 初始化时检查是否有数据
|
||||
if (props.data && props.data.yuhun) {
|
||||
currentYuhun.value = props.data.yuhun;
|
||||
if (node?.properties?.yuhun) {
|
||||
currentYuhun.value = node.properties.yuhun;
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
// 移除全局事件监听器
|
||||
window.removeEventListener('update-yuhun', handleYuhunUpdate);
|
||||
});
|
||||
|
||||
// 导出方法,使父组件可以调用
|
||||
defineExpose({
|
||||
updateNodeYuhun
|
||||
graph?.eventCenter.on(EventType.NODE_PROPERTIES_CHANGE, (eventData: any) => {
|
||||
if (eventData.id === node.id && eventData.properties?.yuhun) {
|
||||
currentYuhun.value = eventData.properties.yuhun;
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NodeResizer
|
||||
v-if="selected"
|
||||
:min-width="150"
|
||||
:min-height="150"
|
||||
:max-width="300"
|
||||
:max-height="300"
|
||||
<div class="node-content">
|
||||
<img
|
||||
v-if="currentYuhun.avatar"
|
||||
:src="currentYuhun.avatar"
|
||||
:alt="currentYuhun.name"
|
||||
class="yuhun-image"
|
||||
/>
|
||||
|
||||
<Handle type="target" :position="Position.Left" :id="`${id}-target`" />
|
||||
|
||||
<div class="node-content">
|
||||
<img
|
||||
v-if="currentYuhun.avatar"
|
||||
:src="currentYuhun.avatar"
|
||||
:alt="currentYuhun.name"
|
||||
class="yuhun-image"
|
||||
/>
|
||||
<div v-else class="placeholder-text">点击选择御魂</div>
|
||||
<div class="name-text">{{ currentYuhun.name }}</div>
|
||||
<div v-if="currentYuhun.type" class="type-text">{{ currentYuhun.type }}</div>
|
||||
</div>
|
||||
|
||||
<Handle type="source" :position="Position.Right" :id="`${id}-source`" />
|
||||
<div v-else class="placeholder-text">点击选择御魂</div>
|
||||
<div class="name-text">{{ currentYuhun.name }}</div>
|
||||
<div v-if="currentYuhun.type" class="type-text">{{ currentYuhun.type }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.node-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 180px;
|
||||
min-height: 180px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.yuhun-image {
|
||||
width: 85%;
|
||||
height: 85%;
|
||||
object-fit: cover;
|
||||
width: 85%;
|
||||
height: 85%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.placeholder-text {
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.name-text {
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
margin-top: 8px;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.type-text {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
:deep(.vue-flow__node-resizer) {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
:deep(.vue-flow__node-resizer-handle) {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background-color: #409EFF;
|
||||
border-radius: 50%;
|
||||
pointer-events: all;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-top: 4px;
|
||||
}
|
||||
</style>
|
||||
|
@@ -6,6 +6,12 @@ import { useGlobalMessage } from "./useGlobalMessage";
|
||||
|
||||
const { showMessage } = useGlobalMessage();
|
||||
|
||||
// LogicFlow 实例全局引用
|
||||
let logicFlowInstance: any = null;
|
||||
function setLogicFlowInstance(lf: any) {
|
||||
logicFlowInstance = lf;
|
||||
}
|
||||
|
||||
function getDefaultState() {
|
||||
return {
|
||||
fileList: [
|
||||
@@ -36,6 +42,69 @@ function getDefaultState() {
|
||||
x: 350,
|
||||
y: 120,
|
||||
text: "File1-圆形节点"
|
||||
},
|
||||
{
|
||||
id: "node-3",
|
||||
type: "shikigamiSelect",
|
||||
x: 200,
|
||||
y: 300,
|
||||
properties: {
|
||||
shikigami: {
|
||||
name: "时曜泷夜叉姬",
|
||||
avatar: "/assets/Shikigami/sp/584.png",
|
||||
rarity: "SP"
|
||||
},
|
||||
width: 100,
|
||||
height: 80
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "node-yuhun-1",
|
||||
type: "yuhunSelect",
|
||||
x: 300,
|
||||
y: 200,
|
||||
properties: {
|
||||
yuhun: {
|
||||
name: "针女",
|
||||
avatar: "/assets/Yuhun/针女.png",
|
||||
type: "攻击类"
|
||||
},
|
||||
width: 100,
|
||||
height: 80
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "node-property-1",
|
||||
type: "propertySelect",
|
||||
x: 500,
|
||||
y: 300,
|
||||
properties: {
|
||||
property: {
|
||||
type: "attack",
|
||||
priority: "required",
|
||||
attackType: "fixed",
|
||||
attackValue: 3000,
|
||||
description: "主输出式神,需高攻击",
|
||||
levelRequired: "40",
|
||||
skillRequiredMode: "all",
|
||||
skillRequired: ["5", "5", "5"],
|
||||
yuhun: {
|
||||
yuhunSetEffect: [
|
||||
{ name: "破势", avatar: "/assets/Yuhun/破势.png" },
|
||||
{ name: "荒骷髅", avatar: "/assets/Yuhun/荒骷髅.png" }
|
||||
],
|
||||
target: "1",
|
||||
property2: ["Attack"],
|
||||
property4: ["Attack"],
|
||||
property6: ["Crit", "CritDamage"]
|
||||
},
|
||||
expectedDamage: 10000,
|
||||
survivalRate: 50,
|
||||
damageType: "balanced"
|
||||
},
|
||||
width: 120,
|
||||
height: 100
|
||||
}
|
||||
}
|
||||
],
|
||||
edges: [
|
||||
@@ -130,13 +199,26 @@ interface FileGroup {
|
||||
details: string;
|
||||
}
|
||||
|
||||
interface LogicFlowNode {
|
||||
id: string;
|
||||
type: string;
|
||||
x: number;
|
||||
y: number;
|
||||
text?: string | object;
|
||||
properties?: Record<string, any>;
|
||||
}
|
||||
|
||||
interface FlowFile {
|
||||
label: string;
|
||||
name: string;
|
||||
visible: boolean;
|
||||
type: string;
|
||||
groups: FileGroup[];
|
||||
flowData?: any;
|
||||
flowData?: {
|
||||
nodes: LogicFlowNode[];
|
||||
edges: any[];
|
||||
viewport: any;
|
||||
};
|
||||
}
|
||||
|
||||
export const useFilesStore = defineStore('files', () => {
|
||||
@@ -190,7 +272,7 @@ export const useFilesStore = defineStore('files', () => {
|
||||
};
|
||||
|
||||
// 添加节点
|
||||
const addNode = (node: Node) => {
|
||||
const addNode = (node: LogicFlowNode) => {
|
||||
const file = fileList.value.find(f => f.name === activeFile.value);
|
||||
if (!file) return;
|
||||
if (!file.flowData) file.flowData = { nodes: [], edges: [], viewport: { x: 0, y: 0, zoom: 1 } };
|
||||
@@ -198,15 +280,26 @@ export const useFilesStore = defineStore('files', () => {
|
||||
};
|
||||
|
||||
// 更新节点
|
||||
const updateNode = (nodeId: string, updateData: Partial<Node>) => {
|
||||
const updateNode = (nodeId: string, updateData: Partial<LogicFlowNode>) => {
|
||||
const file = fileList.value.find(f => f.name === activeFile.value);
|
||||
if (!file || !file.flowData || !file.flowData.nodes) return;
|
||||
const nodeIndex = file.flowData.nodes.findIndex(n => n.id === nodeId);
|
||||
const nodeIndex = file.flowData.nodes.findIndex((n: LogicFlowNode) => n.id === nodeId);
|
||||
if (nodeIndex === -1) return;
|
||||
file.flowData.nodes[nodeIndex] = {
|
||||
...file.flowData.nodes[nodeIndex],
|
||||
...updateData,
|
||||
};
|
||||
|
||||
const oldNode = file.flowData.nodes[nodeIndex];
|
||||
const mergedNode = { ...oldNode, ...updateData };
|
||||
|
||||
// Deep merge properties
|
||||
if (updateData.properties && oldNode.properties) {
|
||||
mergedNode.properties = { ...oldNode.properties, ...updateData.properties };
|
||||
}
|
||||
file.flowData.nodes[nodeIndex] = mergedNode;
|
||||
|
||||
// 同步 LogicFlow 画布
|
||||
if (logicFlowInstance && mergedNode.properties) {
|
||||
// setProperties overwrites, so we pass the fully merged properties object
|
||||
logicFlowInstance.setProperties(nodeId, mergedNode.properties);
|
||||
}
|
||||
};
|
||||
|
||||
// 删除节点
|
||||
@@ -239,14 +332,15 @@ export const useFilesStore = defineStore('files', () => {
|
||||
const updateNodePosition = (nodeId: string, position: { x: number; y: number }) => {
|
||||
const file = fileList.value.find(f => f.name === activeFile.value);
|
||||
if (!file || !file.flowData || !file.flowData.nodes) return;
|
||||
const node = file.flowData.nodes.find(n => n.id === nodeId);
|
||||
const node = file.flowData.nodes.find((n: LogicFlowNode) => n.id === nodeId);
|
||||
if (node) {
|
||||
node.position = position;
|
||||
node.x = position.x;
|
||||
node.y = position.y;
|
||||
}
|
||||
};
|
||||
|
||||
// 更新节点顺序
|
||||
const updateNodesOrder = (nodes: Node[]) => {
|
||||
const updateNodesOrder = (nodes: LogicFlowNode[]) => {
|
||||
const file = fileList.value.find(f => f.name === activeFile.value);
|
||||
if (!file || !file.flowData) return;
|
||||
file.flowData.nodes = nodes;
|
||||
@@ -422,5 +516,6 @@ export const useFilesStore = defineStore('files', () => {
|
||||
setupAutoSave,
|
||||
exportData,
|
||||
importData,
|
||||
setLogicFlowInstance,
|
||||
};
|
||||
});
|
Reference in New Issue
Block a user