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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -149,7 +164,10 @@ watch(
|
|||||||
:name="file.name.toString()"
|
:name="file.name.toString()"
|
||||||
/>
|
/>
|
||||||
</el-tabs>
|
</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
|
<FlowEditor
|
||||||
ref="flowEditorRef"
|
ref="flowEditorRef"
|
||||||
:height="contentHeight"
|
:height="contentHeight"
|
||||||
|
@@ -16,11 +16,8 @@ const filesStore = useFilesStore();
|
|||||||
:currentShikigami="dialogs.shikigami.data"
|
:currentShikigami="dialogs.shikigami.data"
|
||||||
@closeSelectShikigami="closeDialog('shikigami')"
|
@closeSelectShikigami="closeDialog('shikigami')"
|
||||||
@updateShikigami="data => {
|
@updateShikigami="data => {
|
||||||
if (dialogs.shikigami.node?.id) {
|
dialogs.shikigami.callback?.(data);
|
||||||
filesStore.updateNode(dialogs.shikigami.node.id, { data: { ...dialogs.shikigami.node.data, shikigami: data } })
|
closeDialog('shikigami');
|
||||||
}
|
|
||||||
dialogs.shikigami.callback?.(data, dialogs.shikigami.node)
|
|
||||||
closeDialog('shikigami')
|
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
<YuhunSelect
|
<YuhunSelect
|
||||||
@@ -29,11 +26,8 @@ const filesStore = useFilesStore();
|
|||||||
:currentYuhun="dialogs.yuhun.data"
|
:currentYuhun="dialogs.yuhun.data"
|
||||||
@closeSelectYuhun="closeDialog('yuhun')"
|
@closeSelectYuhun="closeDialog('yuhun')"
|
||||||
@updateYuhun="data => {
|
@updateYuhun="data => {
|
||||||
if (dialogs.yuhun.node?.id) {
|
dialogs.yuhun.callback?.(data);
|
||||||
filesStore.updateNode(dialogs.yuhun.node.id, { data: { ...dialogs.yuhun.node.data, yuhun: data } })
|
closeDialog('yuhun');
|
||||||
}
|
|
||||||
dialogs.yuhun.callback?.(data, dialogs.yuhun.node);
|
|
||||||
closeDialog('yuhun')
|
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
<PropertySelect
|
<PropertySelect
|
||||||
@@ -42,11 +36,8 @@ const filesStore = useFilesStore();
|
|||||||
:currentProperty="dialogs.property.data"
|
:currentProperty="dialogs.property.data"
|
||||||
@closePropertySelect="closeDialog('property')"
|
@closePropertySelect="closeDialog('property')"
|
||||||
@updateProperty="data => {
|
@updateProperty="data => {
|
||||||
if (dialogs.property.node?.id) {
|
dialogs.property.callback?.(data);
|
||||||
filesStore.updateNode(dialogs.property.node.id, { data: { ...dialogs.property.node.data, property: data } })
|
closeDialog('property');
|
||||||
}
|
|
||||||
dialogs.property.callback?.(data, dialogs.property.node);
|
|
||||||
closeDialog('property')
|
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
@@ -1,76 +1,125 @@
|
|||||||
<template>
|
<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>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch, onMounted, onBeforeUnmount, defineExpose } from 'vue';
|
import { ref, watch, onMounted, onBeforeUnmount, defineExpose } from 'vue';
|
||||||
import type { PropType } from 'vue';
|
import LogicFlow, { EventType } from '@logicflow/core';
|
||||||
import LogicFlow from '@logicflow/core';
|
|
||||||
import '@logicflow/core/lib/style/index.css';
|
import '@logicflow/core/lib/style/index.css';
|
||||||
|
import { register } from '@logicflow/vue-node-registry';
|
||||||
// 类型定义放在 import 之后,避免顶层 await 错误
|
import ShikigamiSelectNode from './nodes/yys/ShikigamiSelectNode.vue';
|
||||||
|
import YuhunSelectNode from './nodes/yys/YuhunSelectNode.vue';
|
||||||
type NodeData = {
|
import PropertySelectNode from './nodes/yys/PropertySelectNode.vue';
|
||||||
id: string;
|
// import ImageNode from './nodes/common/ImageNode.vue';
|
||||||
type: string;
|
// import TextNode from './nodes/common/TextNode.vue';
|
||||||
x: number;
|
import PropertyPanel from './PropertyPanel.vue';
|
||||||
y: number;
|
import {useFilesStore} from "@/ts/useStore";
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
nodes: NodeData[];
|
nodes: any[];
|
||||||
edges: EdgeData[];
|
edges: any[];
|
||||||
viewport?: Viewport;
|
viewport?: { x: number; y: number; zoom: number };
|
||||||
height?: string;
|
height?: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const filesStore = useFilesStore();
|
||||||
const containerRef = ref<HTMLElement | null>(null);
|
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
|
// 初始化 LogicFlow
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
lf = new LogicFlow({
|
lf.value = new LogicFlow({
|
||||||
container: containerRef.value as HTMLElement,
|
container: containerRef.value as HTMLElement,
|
||||||
grid: true,
|
grid: true,
|
||||||
});
|
});
|
||||||
// lf.zoom(2);
|
registerNodes(lf.value);
|
||||||
// zoom(2);
|
|
||||||
renderFlow();
|
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
|
// 销毁 LogicFlow
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
lf?.destroy();
|
lf.value?.destroy();
|
||||||
lf = null;
|
lf.value = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 响应式更新 nodes/edges
|
// 响应式更新 nodes/edges
|
||||||
watch(
|
// watch(
|
||||||
() => [props.nodes, props.edges],
|
// () => [props.nodes, props.edges],
|
||||||
() => {
|
// () => {
|
||||||
renderFlow();
|
// renderFlow();
|
||||||
},
|
// },
|
||||||
{ deep: true }
|
// { deep: true }
|
||||||
);
|
// );
|
||||||
|
|
||||||
// 响应式更新 viewport
|
// 响应式更新 viewport
|
||||||
watch(
|
watch(
|
||||||
@@ -81,22 +130,22 @@ watch(
|
|||||||
);
|
);
|
||||||
|
|
||||||
function renderFlow() {
|
function renderFlow() {
|
||||||
if (!lf) return;
|
if (!lf.value) return;
|
||||||
lf.render({
|
lf.value.render({
|
||||||
nodes: props.nodes,
|
nodes: props.nodes,
|
||||||
edges: props.edges,
|
edges: props.edges,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function setViewport(viewport: Viewport) {
|
function setViewport(viewport?: { x: number; y: number; zoom: number }) {
|
||||||
if (!lf || !viewport) return;
|
if (!lf.value || !viewport) return;
|
||||||
lf.zoom(viewport.zoom);
|
lf.value.zoom(viewport.zoom);
|
||||||
// lf.focusOn({ x: viewport.x, y: viewport.y });
|
// lf.value.focusOn({ x: viewport.x, y: viewport.y });
|
||||||
}
|
}
|
||||||
|
|
||||||
function getViewport(): Viewport {
|
function getViewport() {
|
||||||
if (!lf) return { x: 0, y: 0, zoom: 1 };
|
if (!lf.value) return { x: 0, y: 0, zoom: 1 };
|
||||||
const t = lf.getTransform();
|
const t = lf.value.getTransform();
|
||||||
return {
|
return {
|
||||||
x: t.TRANSLATE_X,
|
x: t.TRANSLATE_X,
|
||||||
y: t.TRANSLATE_Y,
|
y: t.TRANSLATE_Y,
|
||||||
@@ -104,34 +153,82 @@ function getViewport(): Viewport {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleAddNode(node: NodeData) {
|
// 右键菜单相关
|
||||||
if (!lf) return;
|
function handleNodeContextMenu({ data, e }: { data: any; e: MouseEvent }) {
|
||||||
lf.addNode(node);
|
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() {
|
function getGraphRawData() {
|
||||||
if (!lf) return null;
|
if (!lf) return null;
|
||||||
return lf.getGraphRawData();
|
return lf.value.getGraphRawData();
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderRawData(data: any) {
|
function renderRawData(data: any) {
|
||||||
if (!lf) return;
|
if (!lf) return;
|
||||||
lf.renderRawData(data);
|
lf.value.renderRawData(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
getViewport,
|
getViewport,
|
||||||
setViewport, // 新增暴露
|
setViewport,
|
||||||
handleAddNode,
|
renderFlow,
|
||||||
getGraphRawData,
|
getGraphRawData,
|
||||||
renderRawData,
|
renderRawData,
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.editor-layout {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.flow-container {
|
||||||
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
.container {
|
.container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 300px;
|
min-height: 300px;
|
||||||
background: #fff;
|
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>
|
</style>
|
@@ -1,83 +1,31 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onUnmounted, watch } from 'vue';
|
import { ref, watch, onMounted, inject } from 'vue';
|
||||||
import { Handle, Position, useVueFlow } from '@vue-flow/core';
|
import { EventType } from '@logicflow/core';
|
||||||
import { NodeResizer } from '@vue-flow/node-resizer'
|
|
||||||
import '@vue-flow/node-resizer/dist/style.css';
|
|
||||||
|
|
||||||
const props = defineProps({
|
const currentProperty = ref({ type: '未选择', priority: '可选' });
|
||||||
data: Object,
|
|
||||||
id: String,
|
|
||||||
selected: Boolean
|
|
||||||
});
|
|
||||||
|
|
||||||
// 获取Vue Flow的实例和节点更新方法
|
const getNode = inject('getNode') as (() => any) | undefined;
|
||||||
const { findNode, updateNode } = useVueFlow();
|
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(() => {
|
onMounted(() => {
|
||||||
console.log('PropertySelectNode mounted:', props.id);
|
const node = getNode?.();
|
||||||
// 添加全局事件监听
|
const graph = getGraph?.();
|
||||||
window.addEventListener('update-property', handlePropertyUpdate);
|
|
||||||
|
|
||||||
// 初始化时检查是否有数据
|
if (node?.properties?.property) {
|
||||||
if (props.data && props.data.property) {
|
currentProperty.value = node.properties.property;
|
||||||
currentProperty.value = props.data.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 getPropertyTypeName = () => {
|
||||||
const typeMap = {
|
const typeMap: Record<string, string> = {
|
||||||
'attack': '攻击',
|
'attack': '攻击',
|
||||||
'health': '生命',
|
'health': '生命',
|
||||||
'defense': '防御',
|
'defense': '防御',
|
||||||
@@ -88,165 +36,38 @@ const getPropertyTypeName = () => {
|
|||||||
'effectResist': '效果抵抗',
|
'effectResist': '效果抵抗',
|
||||||
'未选择': '未选择'
|
'未选择': '未选择'
|
||||||
};
|
};
|
||||||
|
|
||||||
return typeMap[currentProperty.value.type] || currentProperty.value.type;
|
return typeMap[currentProperty.value.type] || currentProperty.value.type;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取优先级显示名称
|
|
||||||
const getPriorityName = () => {
|
const getPriorityName = () => {
|
||||||
const priorityMap = {
|
const priorityMap: Record<string, string> = {
|
||||||
'required': '必须',
|
'required': '必须',
|
||||||
'recommended': '推荐',
|
'recommended': '推荐',
|
||||||
'optional': '可选'
|
'optional': '可选'
|
||||||
};
|
};
|
||||||
|
|
||||||
return priorityMap[currentProperty.value.priority] || currentProperty.value.priority;
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<NodeResizer
|
<div class="property-node" :class="[currentProperty.priority ? `priority-${currentProperty.priority}` : '']">
|
||||||
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="node-content">
|
<div class="node-content">
|
||||||
<div class="node-header">
|
<div class="node-header">
|
||||||
<div class="node-title">属性要求</div>
|
<div class="node-title">属性要求</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="node-body">
|
<div class="node-body">
|
||||||
<div class="property-main">
|
<div class="property-main">
|
||||||
<div class="property-type">{{ getPropertyTypeName() }}</div>
|
<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 v-else class="property-placeholder">点击设置属性</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="property-details" v-if="currentProperty.type !== '未选择'">
|
<div class="property-details" v-if="currentProperty.type !== '未选择'">
|
||||||
<div class="property-priority">优先级: {{ getPriorityName() }}</div>
|
<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">
|
<div class="property-description" v-if="currentProperty.description">
|
||||||
{{ currentProperty.description }}
|
{{ currentProperty.description }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 输出连接点 -->
|
|
||||||
<Handle type="source" position="right" :id="`${id}-source`" />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -258,7 +79,6 @@ defineExpose({
|
|||||||
min-width: 180px;
|
min-width: 180px;
|
||||||
min-height: 180px;
|
min-height: 180px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.node-content {
|
.node-content {
|
||||||
position: relative;
|
position: relative;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
@@ -266,89 +86,28 @@ defineExpose({
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
min-width: 180px;
|
min-width: 180px;
|
||||||
min-height: 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 {
|
.node-header {
|
||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
background-color: #f0f7ff;
|
background-color: #f0f7ff;
|
||||||
border-bottom: 1px solid #dcdfe6;
|
border-bottom: 1px solid #dcdfe6;
|
||||||
border-radius: 4px 4px 0 0;
|
border-radius: 4px 4px 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.node-title {
|
.node-title {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.node-body {
|
.node-body {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.property-main {
|
.property-main {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -356,20 +115,17 @@ defineExpose({
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.property-type {
|
.property-type {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.property-value {
|
.property-value {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
color: #409eff;
|
color: #409eff;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.property-placeholder {
|
.property-placeholder {
|
||||||
width: 120px;
|
width: 120px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
@@ -383,29 +139,16 @@ defineExpose({
|
|||||||
margin: 8px 0;
|
margin: 8px 0;
|
||||||
transition: width 0.2s, height 0.2s;
|
transition: width 0.2s, height 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.property-details {
|
.property-details {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-top: 1px dashed #ebeef5;
|
border-top: 1px dashed #ebeef5;
|
||||||
padding-top: 8px;
|
padding-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.property-priority {
|
.property-priority {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #606266;
|
color: #606266;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.property-extra-info {
|
|
||||||
font-size: 11px;
|
|
||||||
color: #909399;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.property-extra-info > div {
|
|
||||||
margin-bottom: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.property-description {
|
.property-description {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: #606266;
|
color: #606266;
|
||||||
|
@@ -1,78 +1,41 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onUnmounted, watch } from 'vue';
|
import {ref, onMounted, inject, watch} from 'vue';
|
||||||
import { Handle, Position, useVueFlow } from '@vue-flow/core';
|
import { EventType } from '@logicflow/core';
|
||||||
import { NodeResizer } from '@vue-flow/node-resizer';
|
|
||||||
import '@vue-flow/node-resizer/dist/style.css';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
data: Object,
|
|
||||||
id: String,
|
|
||||||
selected: Boolean
|
|
||||||
});
|
|
||||||
|
|
||||||
// 获取Vue Flow的实例和节点更新方法
|
|
||||||
const { findNode, updateNode } = useVueFlow();
|
|
||||||
|
|
||||||
// 式神信息保存在节点数据中
|
|
||||||
const currentShikigami = ref({ name: '未选择式神', avatar: '', rarity: '' });
|
const currentShikigami = ref({ name: '未选择式神', avatar: '', rarity: '' });
|
||||||
|
|
||||||
// 节点尺寸
|
const getNode = inject('getNode') as (() => any) | undefined;
|
||||||
const nodeWidth = ref(180);
|
const getGraph = inject('getGraph') as (() => any) | undefined;
|
||||||
const nodeHeight = ref(180);
|
|
||||||
|
|
||||||
// 监听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(() => {
|
onMounted(() => {
|
||||||
console.log('ShikigamiSelectNode mounted:', props.id);
|
const node = getNode?.();
|
||||||
// 添加全局事件监听
|
const graph = getGraph?.();
|
||||||
window.addEventListener('update-shikigami', handleShikigamiUpdate);
|
|
||||||
|
|
||||||
// 初始化时检查是否有数据
|
// 初始化
|
||||||
if (props.data && props.data.shikigami) {
|
if (node?.properties?.shikigami) {
|
||||||
currentShikigami.value = props.data.shikigami;
|
currentShikigami.value = node.properties.shikigami;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
// 监听属性变化
|
||||||
// 移除全局事件监听器
|
graph?.eventCenter.on(EventType.NODE_PROPERTIES_CHANGE, (eventData: any) => {
|
||||||
window.removeEventListener('update-shikigami', handleShikigamiUpdate);
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<NodeResizer
|
<div
|
||||||
v-if="selected"
|
class="node-content"
|
||||||
/>
|
:style="{
|
||||||
|
boxSizing: 'border-box',
|
||||||
<Handle type="target" :position="position.Left" :id="`${id}-target`" />
|
background: '#fff',
|
||||||
|
borderRadius: '8px'
|
||||||
<div class="node-content">
|
}"
|
||||||
|
>
|
||||||
<img
|
<img
|
||||||
v-if="currentShikigami.avatar"
|
v-if="currentShikigami.avatar"
|
||||||
:src="currentShikigami.avatar"
|
:src="currentShikigami.avatar"
|
||||||
@@ -82,8 +45,6 @@ const position = Position;
|
|||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Handle type="source" :position="position.Right" :id="`${id}-source`" />
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -93,38 +54,18 @@ const position = Position;
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shikigami-image {
|
.shikigami-image {
|
||||||
width: 85%;
|
width: 85%;
|
||||||
height: 85%;
|
height: 85%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
.placeholder-text {
|
.placeholder-text {
|
||||||
color: #909399;
|
color: #909399;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.name-text {
|
.name-text {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-top: 8px;
|
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;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
@@ -1,99 +1,31 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {ref, onMounted, onUnmounted, watch} from 'vue';
|
import { ref, watch, onMounted, inject } from 'vue';
|
||||||
import {Handle, Position, useVueFlow} from '@vue-flow/core';
|
import { EventType } from '@logicflow/core';
|
||||||
import {NodeResizer} from '@vue-flow/node-resizer'
|
|
||||||
import '@vue-flow/node-resizer/dist/style.css';
|
|
||||||
|
|
||||||
const props = defineProps({
|
const currentYuhun = ref({ name: '未选择御魂', avatar: '', type: '' });
|
||||||
data: Object,
|
|
||||||
id: String,
|
|
||||||
selected: Boolean
|
|
||||||
});
|
|
||||||
|
|
||||||
// 获取Vue Flow的实例和节点更新方法
|
const getNode = inject('getNode') as (() => any) | undefined;
|
||||||
const {findNode, updateNode} = useVueFlow();
|
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(() => {
|
onMounted(() => {
|
||||||
console.log('YuhunSelectNode mounted:', props.id);
|
const node = getNode?.();
|
||||||
// 添加全局事件监听
|
const graph = getGraph?.();
|
||||||
window.addEventListener('update-yuhun', handleYuhunUpdate);
|
|
||||||
|
|
||||||
// 初始化时检查是否有数据
|
if (node?.properties?.yuhun) {
|
||||||
if (props.data && props.data.yuhun) {
|
currentYuhun.value = node.properties.yuhun;
|
||||||
currentYuhun.value = props.data.yuhun;
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
graph?.eventCenter.on(EventType.NODE_PROPERTIES_CHANGE, (eventData: any) => {
|
||||||
// 移除全局事件监听器
|
if (eventData.id === node.id && eventData.properties?.yuhun) {
|
||||||
window.removeEventListener('update-yuhun', handleYuhunUpdate);
|
currentYuhun.value = eventData.properties.yuhun;
|
||||||
});
|
}
|
||||||
|
});
|
||||||
// 导出方法,使父组件可以调用
|
|
||||||
defineExpose({
|
|
||||||
updateNodeYuhun
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<NodeResizer
|
|
||||||
v-if="selected"
|
|
||||||
:min-width="150"
|
|
||||||
:min-height="150"
|
|
||||||
:max-width="300"
|
|
||||||
:max-height="300"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Handle type="target" :position="Position.Left" :id="`${id}-target`" />
|
|
||||||
|
|
||||||
<div class="node-content">
|
<div class="node-content">
|
||||||
<img
|
<img
|
||||||
v-if="currentYuhun.avatar"
|
v-if="currentYuhun.avatar"
|
||||||
@@ -105,59 +37,32 @@ defineExpose({
|
|||||||
<div class="name-text">{{ currentYuhun.name }}</div>
|
<div class="name-text">{{ currentYuhun.name }}</div>
|
||||||
<div v-if="currentYuhun.type" class="type-text">{{ currentYuhun.type }}</div>
|
<div v-if="currentYuhun.type" class="type-text">{{ currentYuhun.type }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Handle type="source" :position="Position.Right" :id="`${id}-source`" />
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.node-content {
|
.node-content {
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
min-width: 180px;
|
|
||||||
min-height: 180px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.yuhun-image {
|
.yuhun-image {
|
||||||
width: 85%;
|
width: 85%;
|
||||||
height: 85%;
|
height: 85%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
.placeholder-text {
|
.placeholder-text {
|
||||||
color: #909399;
|
color: #909399;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.name-text {
|
.name-text {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.type-text {
|
.type-text {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #909399;
|
color: #909399;
|
||||||
margin-top: 4px;
|
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;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
@@ -6,6 +6,12 @@ import { useGlobalMessage } from "./useGlobalMessage";
|
|||||||
|
|
||||||
const { showMessage } = useGlobalMessage();
|
const { showMessage } = useGlobalMessage();
|
||||||
|
|
||||||
|
// LogicFlow 实例全局引用
|
||||||
|
let logicFlowInstance: any = null;
|
||||||
|
function setLogicFlowInstance(lf: any) {
|
||||||
|
logicFlowInstance = lf;
|
||||||
|
}
|
||||||
|
|
||||||
function getDefaultState() {
|
function getDefaultState() {
|
||||||
return {
|
return {
|
||||||
fileList: [
|
fileList: [
|
||||||
@@ -36,6 +42,69 @@ function getDefaultState() {
|
|||||||
x: 350,
|
x: 350,
|
||||||
y: 120,
|
y: 120,
|
||||||
text: "File1-圆形节点"
|
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: [
|
edges: [
|
||||||
@@ -130,13 +199,26 @@ interface FileGroup {
|
|||||||
details: string;
|
details: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface LogicFlowNode {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
text?: string | object;
|
||||||
|
properties?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
interface FlowFile {
|
interface FlowFile {
|
||||||
label: string;
|
label: string;
|
||||||
name: string;
|
name: string;
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
type: string;
|
type: string;
|
||||||
groups: FileGroup[];
|
groups: FileGroup[];
|
||||||
flowData?: any;
|
flowData?: {
|
||||||
|
nodes: LogicFlowNode[];
|
||||||
|
edges: any[];
|
||||||
|
viewport: any;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useFilesStore = defineStore('files', () => {
|
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);
|
const file = fileList.value.find(f => f.name === activeFile.value);
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
if (!file.flowData) file.flowData = { nodes: [], edges: [], viewport: { x: 0, y: 0, zoom: 1 } };
|
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);
|
const file = fileList.value.find(f => f.name === activeFile.value);
|
||||||
if (!file || !file.flowData || !file.flowData.nodes) return;
|
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;
|
if (nodeIndex === -1) return;
|
||||||
file.flowData.nodes[nodeIndex] = {
|
|
||||||
...file.flowData.nodes[nodeIndex],
|
const oldNode = file.flowData.nodes[nodeIndex];
|
||||||
...updateData,
|
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 updateNodePosition = (nodeId: string, position: { x: number; y: number }) => {
|
||||||
const file = fileList.value.find(f => f.name === activeFile.value);
|
const file = fileList.value.find(f => f.name === activeFile.value);
|
||||||
if (!file || !file.flowData || !file.flowData.nodes) return;
|
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) {
|
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);
|
const file = fileList.value.find(f => f.name === activeFile.value);
|
||||||
if (!file || !file.flowData) return;
|
if (!file || !file.flowData) return;
|
||||||
file.flowData.nodes = nodes;
|
file.flowData.nodes = nodes;
|
||||||
@@ -422,5 +516,6 @@ export const useFilesStore = defineStore('files', () => {
|
|||||||
setupAutoSave,
|
setupAutoSave,
|
||||||
exportData,
|
exportData,
|
||||||
importData,
|
importData,
|
||||||
|
setLogicFlowInstance,
|
||||||
};
|
};
|
||||||
});
|
});
|
Reference in New Issue
Block a user