init commit

This commit is contained in:
rookie4show 2025-05-14 13:43:44 +08:00
parent 13db9c4e7b
commit 721acb9033
20 changed files with 3218 additions and 228 deletions

View File

@ -12,11 +12,14 @@
}, },
"dependencies": { "dependencies": {
"@element-plus/icons-vue": "^2.3.1", "@element-plus/icons-vue": "^2.3.1",
"@vue-flow/background": "^1.3.2",
"@vue-flow/controls": "^1.1.2",
"@vue-flow/core": "^1.42.5",
"@vue-flow/node-resizer": "^1.4.0",
"@vueup/vue-quill": "^1.2.0", "@vueup/vue-quill": "^1.2.0",
"element-plus": "^2.9.1", "element-plus": "^2.9.1",
"html2canvas": "^1.4.1", "html2canvas": "^1.4.1",
"pinia": "^3.0.1", "pinia": "^3.0.1",
"simple-mind-map": "^0.13.1-fix.2",
"vue": "^3.3.10", "vue": "^3.3.10",
"vue-i18n": "^11.1.1", "vue-i18n": "^11.1.1",
"vue3-draggable-resizable": "^1.6.5", "vue3-draggable-resizable": "^1.6.5",

View File

@ -1,34 +1,68 @@
<script setup lang="ts"> <script setup lang="ts">
import Yys from './components/Yys.vue';
import Toolbar from './components/Toolbar.vue'; import Toolbar from './components/Toolbar.vue';
import ProjectExplorer from './components/ProjectExplorer.vue'; import ProjectExplorer from './components/ProjectExplorer.vue';
import {computed, ref, onMounted, onUnmounted} from "vue"; import { computed, ref, onMounted, onUnmounted } from "vue";
import {useFilesStore} from "@/ts/files"; import { useFilesStore } from "@/ts/files";
import Vue3DraggableResizable from 'vue3-draggable-resizable'; import Vue3DraggableResizable from 'vue3-draggable-resizable';
import {TabPaneName, TabsPaneContext} from "element-plus"; import { TabPaneName, TabsPaneContext } from "element-plus";
import YysRank from "@/components/YysRank.vue"; import FlowEditor from './components/flow/FlowEditor.vue';
import ShikigamiSelect from './components/flow/nodes/yys/ShikigamiSelect.vue';
import YuhunSelect from './components/flow/nodes/yys/YuhunSelect.vue';
import PropertySelect from './components/flow/nodes/yys/PropertySelect.vue';
import { useVueFlow } from '@vue-flow/core';
const filesStore = useFilesStore(); const filesStore = useFilesStore();
const { updateNode } = useVueFlow();
const yysRef = ref(null);
const width = ref('100%'); const width = ref('100%');
const height = ref('100vh'); const height = ref('100vh');
const toolbarHeight = 48; // const toolbarHeight = 48; //
const windowHeight = ref(window.innerHeight); const windowHeight = ref(window.innerHeight);
const contentHeight = computed(() => `${windowHeight.value - toolbarHeight}px`); const contentHeight = computed(() => `${windowHeight.value - toolbarHeight}px`);
const onResizing = (x, y, width, height) => { // Dialogs and Selected Node Management
width.value = width; const showShikigamiDialog = ref(false);
height.value = height; const showYuhunDialog = ref(false);
const showPropertyDialog = ref(false);
const currentShikigami = ref({ name: '未选择式神', avatar: '', rarity: '' });
const currentYuhun = ref({ name: '未选择御魂', avatar: '', type: '' });
const currentProperty = ref({ type: '未选择属性', priority: 'optional', description: '' });
const selectedNode = ref(null);
const flowEditorRef = ref(null);
const openDialogForType = (type: string, node: any) => {
selectedNode.value = node;
switch (type) {
case 'shikigami': showShikigamiDialog.value = true; break;
case 'yuhun': showYuhunDialog.value = true; break;
case 'property': showPropertyDialog.value = true; break;
}
}; };
const element = ref({ // Handle Dialogs Close
x: 400, const closeDialogForType = (type: string) => {
y: 20, switch (type) {
width: 1080, case 'shikigami': showShikigamiDialog.value = false; break;
height: windowHeight.value - toolbarHeight, case 'yuhun': showYuhunDialog.value = false; break;
isActive: false, case 'property': showPropertyDialog.value = false; break;
}); }
};
//
const updateNodeData = (type: string, data: any) => {
if (selectedNode.value) {
updateNode(selectedNode.value.id, {
data: {
...selectedNode.value.data,
[type]: data
}
});
console.log(`${type.charAt(0).toUpperCase() + type.slice(1)}信息已更新:`, data.name || data.type);
closeDialogForType(type);
}
};
const handleTabsEdit = ( const handleTabsEdit = (
targetName: String | undefined, targetName: String | undefined,
@ -43,7 +77,7 @@ const handleTabsEdit = (
label: newFileName, label: newFileName,
name: newFileName, name: newFileName,
visible: true, visible: true,
type: 'PVE', type: 'FLOW',
groups: [ groups: [
{ {
shortDescription: " ", shortDescription: " ",
@ -85,10 +119,7 @@ const activeFileGroups = computed(() => {
<!-- 侧边栏和工作区 --> <!-- 侧边栏和工作区 -->
<div class="main-content"> <div class="main-content">
<!-- 侧边栏 --> <!-- 侧边栏 -->
<aside class="sidebar"> <ProjectExplorer />
<ProjectExplorer :allFiles="filesStore.fileList"/>
</aside>
<!-- 工作区 --> <!-- 工作区 -->
<div class="workspace"> <div class="workspace">
<el-tabs <el-tabs
@ -104,14 +135,44 @@ const activeFileGroups = computed(() => {
:label="file.label" :label="file.label"
:name="file.name.toString()" :name="file.name.toString()"
> >
<main id="main-container" :style="{ height: contentHeight, overflow: 'auto' }"> <div id="main-container" :style="{ height: contentHeight, overflow: 'auto' }">
<Yys class="yys" :groups="activeFileGroups" v-if="file.type === 'PVE' "/> <!-- 流程图编辑器 -->
<YysRank :groups="activeFileGroups" v-else-if="file.type === 'PVP' "/> <FlowEditor
</main> ref="flowEditorRef"
:height="contentHeight"
@open-shikigami-select="node => openDialogForType('shikigami', node)"
@open-yuhun-select="node => openDialogForType('yuhun', node)"
@open-property-select="node => openDialogForType('property', node)"
/>
</div>
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
</div> </div>
</div> </div>
<!-- 全局式神选择对话框 -->
<ShikigamiSelect
:showSelectShikigami="showShikigamiDialog"
:currentShikigami="currentShikigami"
@closeSelectShikigami="closeDialogForType('shikigami')"
@updateShikigami="data => updateNodeData('shikigami', data)"
/>
<!-- 全局御魂选择对话框 -->
<YuhunSelect
:showSelectYuhun="showYuhunDialog"
:currentYuhun="currentYuhun"
@closeSelectYuhun="closeDialogForType('yuhun')"
@updateYuhun="data => updateNodeData('yuhun', data)"
/>
<!-- 全局属性选择对话框 -->
<PropertySelect
:showPropertySelect="showPropertyDialog"
:currentProperty="currentProperty"
@closePropertySelect="closeDialogForType('property')"
@updateProperty="data => updateNodeData('property', data)"
/>
</div> </div>
</template> </template>
@ -153,8 +214,8 @@ const activeFileGroups = computed(() => {
height: 100%; /* 确保内容区域占满父容器 */ height: 100%; /* 确保内容区域占满父容器 */
overflow-y: auto; /* 允许内容滚动 */ overflow-y: auto; /* 允许内容滚动 */
min-height: 100vh; /* 允许容器扩展 */ min-height: 100vh; /* 允许容器扩展 */
//display: inline-block;
max-width: 100%;
} }
</style> </style>

View File

@ -1,111 +0,0 @@
<template>
<el-dialog v-model="show" :title="t('yuhunSelect')" @close="cancel">
<div style="display: flex; justify-content: space-between; align-items: center;">
<span>当前选择{{ current.name }}</span>
<el-button type="danger" icon="Delete" round @click="remove()"></el-button>
</div>
<div style="display: flex; align-items: center;">
<el-input
placeholder="请输入内容"
v-model="searchText"
style="width: 200px; margin-right: 10px;"
/>
</div>
<el-tabs v-model="activeName" type="card" class="demo-tabs" @tab-click="handleTabClick">
<el-tab-pane v-for="type in yuhunTypes" :key="type.name" :label="type.label" :name="type.name">
<div style="max-height: 500px; overflow-y: auto;">
<el-space wrap size="large" style="">
<div v-for="yuhun in filterYuhunByTypeAndSearch(activeName,searchText)" :key="yuhun.name">
<el-button style="width: 100px; height: 100px;" @click="confirm(yuhun)">
<img :src="yuhun.avatar" style="width: 99px; height: 99px;">
</el-button>
<span style="text-align: center; display: block;">{{yuhun.name}}</span>
</div>
</el-space>
</div>
</el-tab-pane>
</el-tabs>
</el-dialog>
</template>
<script setup lang="ts">
import {ref, watch, computed} from 'vue';
import shikigamiData from '../data/Shikigami.json';
import yuhunData from '../data/Yuhun.json';
import {useI18n} from 'vue-i18n'
// i18n
const {t} = useI18n()
const props = defineProps({
currentShikigami: {
type: Object,
default: () => ({})
},
showYuhunSelect: Boolean
});
const emit = defineEmits(['updateYuhunSelect', 'closeYuhunSelect']);
const searchText = ref('') //
const show = ref(false);
const activeName = ref('ALL');
const current = ref(props.currentShikigami);
const yuhunTypes = [
{label: '全部', name: 'ALL'},
{label: '攻击加成', name: 'Attack'},
{label: '暴击', name: 'Crit'},
{label: '生命加成', name: 'Health'},
{label: '防御加成', name: 'Defense'},
{label: '效果命中', name: 'ControlHit'},
{label: '效果抵抗', name: 'ControlMiss'},
{label: '暴击伤害', name: 'CritDamage'},
{label: '首领御魂', name: 'PVE'}
];
watch(() => props.showYuhunSelect, (newVal) => {
show.value = newVal;
});
watch(() => props.currentShikigami, (newVal) => {
current.value = newVal;
});
const handleTabClick = (tab) => {
console.log(tab.paneName);
};
const filterYuhunByTypeAndSearch = (type: string, search: string) => {
let filteredList = yuhunData;
//
if (type.toLowerCase() !== 'all') {
filteredList = filteredList.filter(item =>
item.type.toLowerCase() === type.toLowerCase()
);
}
//
if (search.trim() !== '') {
filteredList = filteredList.filter(item =>
item.name.toLowerCase().includes(search.toLowerCase())
);
}
return filteredList;
};
const cancel = () => {
emit('closeYuhunSelect');
};
const confirm = (item) => {
emit('updateYuhunSelect', JSON.parse(JSON.stringify(item)), 'Update');
searchText.value=''
activeName.value = 'ALL'
};
const remove = () => {
emit('updateYuhunSelect',undefined,'Remove')
}
</script>

View File

@ -1,9 +1,109 @@
<template> <template>
<ShikigamiSelect
:showSelectShikigami="state.showSelectShikigami"
:currentShikigami="state.currentShikigami"
@closeSelectShikigami="closeSelectShikigami"
@updateShikigami="updateShikigami"
/>
<ShikigamiProperty
:showProperty="state.showProperty"
:currentShikigami="state.currentShikigami"
@closeProperty="closeProperty"
@updateProperty="updateProperty"
/>
<draggable :list="groups" item-key="group" style="display: flex; flex-direction: column; width: 100%;" <draggable :list="groups" item-key="group" style="display: flex; flex-direction: column; width: 100%;"
handle=".drag-handle"> handle=".drag-handle">
<template class="group" #item="{ element: group, index: groupIndex }"> <template class="group" #item="{ element: group, index: groupIndex }">
<el-row :span="24"> <el-row :span="24">
<ShikigamiGroup :groups="groups" :group="group" :group-index="groupIndex"/> <div class="group-item">
<div class="group-header">
<div class="group-opt" data-html2canvas-ignore="true">
<div class="opt-left">
<el-button type="primary" icon="CopyDocument" @click="copy(group.shortDescription)">{{ t('Copy') }}
</el-button>
<el-button type="primary" icon="Document" @click="paste(groupIndex,'shortDescription')">{{
t('Paste')
}}
</el-button>
</div>
<div class="opt-right">
<el-button class="drag-handle" type="primary" icon="Rank" circle></el-button>
<el-button type="primary" @click="addGroup">{{t('AddGroup')}}</el-button>
<el-button type="primary" @click="addGroupElement(groupIndex)">{{t('AddShikigami')}}</el-button>
<el-button type="danger" icon="Delete" circle @click="removeGroup(groupIndex)"></el-button>
</div>
</div>
<QuillEditor ref="shortDescriptionEditor" v-model:content="group.shortDescription" contentType="html" theme="snow" :toolbar="toolbarOptions"/>
</div>
<div class="group-body">
<draggable :list="group.groupInfo" item-key="name" class="body-content">
<template #item="{element : position, index:positionIndex}">
<div>
<el-col>
<el-card class="group-card" shadow="never">
<div class="opt-btn" data-html2canvas-ignore="true">
<!-- Add delete button here -->
<el-button type="danger" icon="Delete" circle @click="removeGroupElement(groupIndex, positionIndex)"/>
<!-- <el-button type="primary" icon="Plus" circle @click="addGroupElement(groupIndex)"/> -->
</div>
<div class="avatar-container">
<!-- 头像图片 -->
<img :src="position.avatar || '/assets/Shikigami/default.png'"
style="cursor: pointer; vertical-align: bottom;"
class="avatar-image"
@click="editShikigami(groupIndex, positionIndex)"/>
<!-- 文字图层 -->
<span v-if="position.properties">{{ position.properties.levelRequired }} {{ position.properties.skillRequired.join('') }}</span>
</div>
<div class="property-wrap">
<div style="display: flex; justify-content: center;" data-html2canvas-ignore="true">
<span>{{ position.name || "" }}</span>
</div>
<div style="display: flex; justify-content: center;" class="bottom" data-html2canvas-ignore="true">
<el-button @click="editProperty(groupIndex,positionIndex)">{{ t('editProperties') }}
</el-button>
</div>
<div v-if="position.properties">
<div style="display: flex; justify-content: center;">
<span
style="width: 100px;height: 50px;background-color: #666;
border-radius: 5px; margin-right: 5px; color: white;
text-align: center; white-space: pre-wrap; display: flex; align-items: center; justify-content: center; flex-direction: column ">
{{getYuhunNames(position.properties.yuhun.yuhunSetEffect)}}<br/>{{ t('yuhun_target.shortName.' + position.properties.yuhun.target) }}·{{ getYuhunPropertyNames(position.properties.yuhun) }}
</span>
</div>
<div>
<span
style="display: inline-block; width: 100px; height: 30px; border-radius: 5px; margin-right: 5px; color: red; text-align: center; white-space: pre-wrap; display: flex; align-items: center; justify-content: center; flex-direction: column ">
{{ position.properties.desc }}
</span>
</div>
</div>
</div>
</el-card>
</el-col>
</div>
</template>
</draggable>
</div>
<div class="group-footer">
<div class="group-opt" data-html2canvas-ignore="true">
<div class="opt-left">
<el-button type="primary" icon="CopyDocument" @click="copy(group.details)">{{ t('Copy') }}</el-button>
<el-button type="primary" icon="Document" @click="paste(groupIndex,'details')">{{
t('Paste')
}}
</el-button>
</div>
</div>
<QuillEditor ref="detailsEditor" v-model:content="group.details" contentType="html" theme="snow" :toolbar="toolbarOptions" />
</div>
</div>
<div class="divider-horizontal"></div> <div class="divider-horizontal"></div>
</el-row> </el-row>
</template> </template>
@ -11,16 +111,344 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {ref, reactive, toRefs, nextTick} from 'vue';
import draggable from 'vuedraggable'; import draggable from 'vuedraggable';
import ShikigamiGroup from "@/components/ShikigamiGroup.vue"; // import ShikigamiSelect from './flow/nodes/yys/ShikigamiSelect.vue';
import ShikigamiProperty from './flow/nodes/yys/ShikigamiProperty.vue';
import html2canvas from 'html2canvas';
import {useI18n} from 'vue-i18n'
import { Quill, QuillEditor } from '@vueup/vue-quill'
import '@vueup/vue-quill/dist/vue-quill.bubble.css'
import '@vueup/vue-quill/dist/vue-quill.snow.css' //
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import shikigamiData from '../data/Shikigami.json';
import _ from 'lodash';
import {Action, ElMessage, ElMessageBox} from "element-plus";
import { useGlobalMessage } from '../ts/useGlobalMessage'; //
const props = defineProps<{ const props = defineProps<{
groups: any[]; groups: any[];
}>(); }>();
const dialogTableVisible = ref(false)
//
const state = reactive({
showSelectShikigami: false,
showProperty: false,
groupIndex: 0,
positionIndex: 0,
currentShikigami: {},
previewImage: null, // URL
previewVisible: false, //
});
const clipboard = ref('');
const { showMessage } = useGlobalMessage();
// i18n
const {t} = useI18n()
const copy = (str) => {
clipboard.value = str
}
const paste = (groupIndex, type) => {
console.log("paste", groupIndex, type, clipboard.value)
if ('shortDescription' == type)
props.groups[groupIndex].shortDescription = clipboard.value
else if ('details' == type)
props.groups[groupIndex].details = clipboard.value
}
//
const registerFonts = () => {
const Font = Quill.import('attributors/style/font')
Font.whitelist = ['SimSun', 'SimHei', 'KaiTi', 'FangSong', 'Microsoft YaHei', 'PingFang SC']
Quill.register(Font, true)
}
//
const registerSizes = () => {
const Size = Quill.import('attributors/style/size')
Size.whitelist = ['12px', '14px', '16px', '18px', '21px', '29px', '32px', '34px']
Quill.register(Size, true)
}
//
registerFonts()
registerSizes()
//
const toolbarOptions = ref([
[{ font: ['SimSun', 'SimHei', 'KaiTi', 'FangSong', 'Microsoft YaHei', 'PingFang SC'] }],
[{ header: 1 }, { header: 2 }],
[{ size: ['12px', '14px', '16px', '18px', '21px', '29px', '32px', '34px'] }],
['bold', 'italic', 'underline', 'strike'],
[{ color: [] }, { background: [] }],
// ['blockquote', 'code-block'],
[ { list: 'bullet' }, { list: 'ordered' }, {'list': 'check'}],
[{ indent: '-1' }, { indent: '+1' }],
[{ align: [] }],
[{ direction: 'rtl' }],
// [{ header: [1, 2, 3, 4, 5, 6, false] }],
// ['link', 'image', 'video'],
// ['clean']
] as const)
//
const closeSelectShikigami = () => {
console.log("close select ====");
state.showSelectShikigami = false;
state.currentShikigami = {};
};
const editShikigami = (groupIndex, positionIndex) => {
console.log("==== 选择式神 ===", groupIndex, positionIndex);
state.showSelectShikigami = true;
state.groupIndex = groupIndex;
state.positionIndex = positionIndex;
state.currentShikigami = props.groups[groupIndex].groupInfo[positionIndex];
};
const updateShikigami = (shikigami) => {
console.log("parent====> ", shikigami, state);
state.showSelectShikigami = false;
const oldProperties = props.groups[state.groupIndex].groupInfo[state.positionIndex].properties;
props.groups[state.groupIndex].groupInfo[state.positionIndex] = _.cloneDeep(shikigami);
props.groups[state.groupIndex].groupInfo[state.positionIndex].properties = oldProperties;
};
const editProperty = (groupIndex, positionIndex) => {
state.showProperty = true;
state.groupIndex = groupIndex;
state.positionIndex = positionIndex;
state.currentShikigami = props.groups[groupIndex].groupInfo[positionIndex];
};
const closeProperty = () => {
state.showProperty = false;
state.currentShikigami = {};
};
const updateProperty = (property) => {
state.showProperty = false;
state.currentShikigami = {};
props.groups[state.groupIndex].groupInfo[state.positionIndex].properties = _.cloneDeep(property);
};
const removeGroupElement = async (groupIndex: number, positionIndex: number) => {
const group = props.groups[groupIndex];
if (group.groupInfo.length === 1) {
showMessage('warning', '无法删除');
return;
}
try {
await ElMessageBox.confirm('确定要删除此元素吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
group.groupInfo.splice(positionIndex, 1);
showMessage('success', '删除成功!');
} catch (error) {
showMessage('info', '已取消删除');
}
};
const removeGroup = async (groupIndex: number) => {
if (props.groups.length === 1) {
showMessage('warning', '无法删除最后一个队伍');
return;
}
try {
await ElMessageBox.confirm('确定要删除此组吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
props.groups.splice(groupIndex, 1);
showMessage('success', '删除成功!');
} catch (error) {
showMessage('info', '已取消删除');
}
};
const addGroup = () => {
props.groups.push({
shortDescription: '',
groupInfo: [{}, {}, {}, {}, {}],
details: ''
});
const container = document.getElementById('main-container');
nextTick(() => {
container.scrollTo({
top: container.scrollHeight,
behavior: 'smooth' //
});
})
};
const addGroupElement = (groupIndex) => {
props.groups[groupIndex].groupInfo.push({});
editShikigami(groupIndex, props.groups[groupIndex].groupInfo.length - 1);
};
const exportGroups = () => {
const dataStr = JSON.stringify(props.groups, null, 2);
const blob = new Blob([dataStr], {type: 'application/json'});
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `yys-export-${Date.now()}.json`;
link.click();
URL.revokeObjectURL(url);
};
const getYuhunNames =(yuhunSetEffect) =>{
const names = yuhunSetEffect.map(item => item.name).join('');
if (names.length <= 6) {
return names;
} else {
return yuhunSetEffect.map(item => item.shortName || item.name).join('');
}
}
const getYuhunPropertyNames = (yuhun) =>{
// yuhun.property2
let property2Value,property4Value,property6Value;
if (yuhun.property2.length >= 4) {
property2Value = 'X';
} else {
property2Value = t('yuhun_property.shortName.' + yuhun.property2[0]);
}
if (yuhun.property4.length >= 5) {
property4Value = 'X';
} else {
property4Value = t('yuhun_property.shortName.' + yuhun.property4[0]);
}
if (yuhun.property6.length >= 5) {
property6Value = 'X';
} else {
property6Value = t('yuhun_property.shortName.' + yuhun.property6[0]);
}
// propertyNames
const propertyNames =
property2Value + property4Value + property6Value
return propertyNames;
}
const importGroups = (file) => {
const reader = new FileReader();
reader.onload = (e) => {
try {
const importedData = JSON.parse(e.target.result);
props.groups = importedData;
ElMessage.success('导入成功');
} catch (error) {
ElMessage.error('文件格式错误');
}
};
reader.readAsText(file);
};
// QuillEditor ref
const shortDescriptionEditor = ref<InstanceType<typeof QuillEditor>>()
const detailsEditor = ref<InstanceType<typeof QuillEditor>>()
//
const saveQuillDesc = async (): Promise<string> => {
if (!shortDescriptionEditor.value) {
throw new Error('Quill editor instance not found')
}
return shortDescriptionEditor.value.getHTML()
}
//
const saveQuillDetail = async (): Promise<string> => {
if (!detailsEditor.value) {
throw new Error('Quill detailsEditor instance not found')
}
return detailsEditor.value.getHTML()
}
//
defineExpose({
saveQuillDesc,
saveQuillDetail,
exportGroups,
importGroups
});
</script> </script>
<style scoped> <style scoped>
.drag-handle {
cursor: move;
}
.position-drag-handle {
cursor: move;
}
.ql-toolbar {
display: none;
}
/* 正方形容器 */
.avatar-wrapper {
width: 100px; /* 正方形边长 */
height: 100px; /* 与宽度相同 */
position: relative;
overflow: hidden; /* 隐藏超出部分 */
border-radius: 50%; /* 圆形裁剪 */
//border: 2px solid #fff; /* */ //box-shadow: 0 2px 8px rgba(0,0,0,0.1); /* */
}
/* 图片样式 */
.avatar-image {
width: 100%;
height: 100%;
object-fit: cover; /* 关键属性:保持比例填充容器 */
object-position: center; /* 居中显示 */
display: block;
cursor: pointer;
}
.el-card {
border: 0;
}
.group-header {
margin: 10px;
padding: 10px;
}
.group-opt {
padding: 10px;
display: flex;
justify-content: space-between;
}
.group-item {
width: 100%;
}
.group-body {
padding: 20px;
width: 80%;
}
/* 水平分割线 */ /* 水平分割线 */
.divider-horizontal { .divider-horizontal {
@ -31,6 +459,190 @@ const props = defineProps<{
width: 100%; width: 100%;
} }
.body-content {
display: flex;
flex-direction: row;
width: 20%;
}
.group-card {
position: relative;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.avatar-container {
position: relative;
min-width: 100px;
}
.avatar-container span {
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%) translateY(50%);
font-size: 24px;
color: white;
text-shadow: -1px -1px 0 black, 1px -1px 0 black, -1px 1px 0 black, 1px 1px 0 black;
white-space: nowrap;
padding: 0 8px;
margin: 0;
display: flex;
align-items: center;
justify-content: center;
}
.opt-btn {
position: absolute;
top: 0px;
right: 0px;
z-index: 10;
opacity: 0;
}
}
.property-wrap {
margin: 10px 0;
}
/* 当鼠标悬停在容器上时显示按钮 */
.group-card:hover .opt-btn {
opacity: 1;
}
.group-footer {
margin: 10px;
padding: 10px;
}
</style> </style>
<style>
.ql-container {
border-bottom-left-radius: 5px;
border-bottom-right-radius: 5px;
}
.ql-toolbar {
border-top-left-radius: 5px;
border-top-right-radius: 5px;
.ql-tooltip[data-mode="link"]::before {
content: "链接地址:";
}
.ql-tooltip[data-mode="video"]::before {
content: "视频地址:";
}
.ql-tooltip.ql-editing {
a.ql-action::after {
content: "保存";
border-right: 0px;
padding-right: 0px;
}
}
.ql-picker.ql-font {
.ql-picker-label[data-value=SimSun]::before,
.ql-picker-item[data-value=SimSun]::before {
content: "宋体";
font-family: SimSun;
font-size: 15px;
}
.ql-picker-label[data-value=SimHei]::before,
.ql-picker-item[data-value=SimHei]::before {
content: "黑体";
font-family: SimHei;
font-size: 15px;
}
.ql-picker-label[data-value=KaiTi]::before,
.ql-picker-item[data-value=KaiTi]::before {
content: "楷体";
font-family: KaiTi;
font-size: 15px;
}
.ql-picker-label[data-value=FangSong]::before,
.ql-picker-item[data-value=FangSong]::before {
content: "仿宋";
font-family: FangSong;
font-size: 15px;
}
.ql-picker-label[data-value="Microsoft YaHei"]::before,
.ql-picker-item[data-value="Microsoft YaHei"]::before {
content: "微软雅黑";
font-family: 'Microsoft YaHei';
font-size: 15px;
}
.ql-picker-label[data-value="PingFang SC"]::before,
.ql-picker-item[data-value="PingFang SC"]::before {
content: "苹方";
font-family: 'PingFang SC';
font-size: 15px;
}
}
.ql-picker.ql-size {
.ql-picker-label::before,
.ql-picker-item::before {
font-size: 14px !important;
content: "五号" !important;
}
.ql-picker-label[data-value="12px"]::before {
content: "小五" !important;
}
.ql-picker-item[data-value="12px"]::before {
font-size: 12px;
content: "小五" !important;
}
.ql-picker-label[data-value="16px"]::before {
content: "小四" !important;
}
.ql-picker-item[data-value="16px"]::before {
font-size: 16px;
content: "小四" !important;
}
.ql-picker-label[data-value="18px"]::before {
content: "四号" !important;
}
.ql-picker-item[data-value="18px"]::before {
font-size: 18px;
content: "四号" !important;
}
.ql-picker-label[data-value="21px"]::before {
content: "三号" !important;
}
.ql-picker-item[data-value="21px"]::before {
font-size: 21px;
content: "三号" !important;
}
.ql-picker-label[data-value="24px"]::before {
content: "小二" !important;
}
.ql-picker-item[data-value="24px"]::before {
font-size: 24px;
content: "小二" !important;
}
.ql-picker-label[data-value="29px"]::before {
content: "二号" !important;
}
.ql-picker-item[data-value="29px"]::before {
font-size: 29px;
content: "二号" !important;
}
.ql-picker-label[data-value="32px"]::before {
content: "小一" !important;
}
.ql-picker-item[data-value="32px"]::before {
font-size: 32px;
content: "小一" !important;
}
.ql-picker-label[data-value="34px"]::before {
content: "一号" !important;
}
.ql-picker-item[data-value="34px"]::before {
font-size: 34px;
content: "一号" !important;
}
}
}
</style>

View File

@ -16,7 +16,7 @@
<div class="yys-rank"> <div class="yys-rank">
<div class="pvp-mindmap"> <div class="pvp-mindmap">
<div id="mindMapContainer"></div>
</div> </div>
<div class="pvp-shikigami-editor"> <div class="pvp-shikigami-editor">
<el-button type="primary" @click="addGroupElement()">{{ t('AddShikigami') }}</el-button> <el-button type="primary" @click="addGroupElement()">{{ t('AddShikigami') }}</el-button>
@ -26,9 +26,6 @@
<draggable :list="props.groups[0].groupInfo" item-key="name" class="body-content"> <draggable :list="props.groups[0].groupInfo" item-key="name" class="body-content">
<template #item="{element : position, index:positionIndex}"> <template #item="{element : position, index:positionIndex}">
<div class="group-card"> <div class="group-card">
<div style="width: 20px;padding-left: 10px">
{{positionIndex + 1}}
</div>
<div class="opt-btn" data-html2canvas-ignore="true"> <div class="opt-btn" data-html2canvas-ignore="true">
<!-- Add delete button here --> <!-- Add delete button here -->
<el-button type="danger" icon="Delete" circle @click="removeGroupElement(positionIndex)"/> <el-button type="danger" icon="Delete" circle @click="removeGroupElement(positionIndex)"/>
@ -40,45 +37,36 @@
style="cursor: pointer; vertical-align: bottom;" style="cursor: pointer; vertical-align: bottom;"
class="avatar-image" class="avatar-image"
@click="editShikigami(positionIndex)"/> @click="editShikigami(positionIndex)"/>
<!-- 文字图层 -->
<!-- <span v-if="position.properties">{{ position.properties.levelRequired }} {{ position.properties.skillRequired.join('') }}</span>-->
</div> </div>
<div class="property-wrap"> <div class="opt-foot">
<div class="shikigami-name"> <div class="property-wrap">
<div style="display: flex; justify-content: center;"> <div style="display: flex; justify-content: center;" data-html2canvas-ignore="true">
<span>{{ position.name || "" }}</span> <span>{{ position.name || "" }}</span>
</div> </div>
<div style="display: flex; justify-content: center;" class="bottom" data-html2canvas-ignore="true"> <div style="display: flex; justify-content: center;" class="bottom" data-html2canvas-ignore="true">
<el-button @click="editProperty(positionIndex)">{{ t('editProperties') }} <el-button @click="editProperty(positionIndex)">{{ t('editProperties') }}
</el-button> </el-button>
</div> </div>
</div> <div v-if="position.properties">
<div v-if="position.properties"> <div style="display: flex; justify-content: center;">
<div style="display: flex; justify-content: center;">
<span <span
style="width: 100px;height: 30px;background-color: #666; style="width: 100px;height: 50px;background-color: #666;
border-radius: 5px; margin-right: 5px; color: white; border-radius: 5px; margin-right: 5px; color: white;
text-align: center; white-space: pre-wrap; display: flex; align-items: center; justify-content: center; flex-direction: column "> text-align: center; white-space: pre-wrap; display: flex; align-items: center; justify-content: center; flex-direction: column ">
{{ getYuhunNames(position.properties.yuhun.yuhunSetEffect) }} {{ getYuhunNames(position.properties.yuhun.yuhunSetEffect) }}<br/>{{
</span>
</div>
</div>
<div v-if="position.properties">
<div style="display: flex; justify-content: center;">
<span
style="width: 100px;height: 30px;background-color: #666;
border-radius: 5px; margin-right: 5px; color: white;
text-align: center; white-space: pre-wrap; display: flex; align-items: center; justify-content: center; flex-direction: column ">
{{
t('yuhun_target.shortName.' + position.properties.yuhun.target) t('yuhun_target.shortName.' + position.properties.yuhun.target)
}}·{{ getYuhunPropertyNames(position.properties.yuhun) }} }}·{{ getYuhunPropertyNames(position.properties.yuhun) }}
</span> </span>
</div> </div>
</div> <div>
<div v-if="position.properties">
<div>
<span <span
style=" width: 100%; height: 30px; border-radius: 5px; margin-right: 5px; color: red; text-align: left; white-space: pre-wrap; display: flex; align-items: center; justify-content: center; flex-direction: column "> style="display: inline-block; width: 100px; height: 30px; border-radius: 5px; margin-right: 5px; color: red; text-align: center; white-space: pre-wrap; display: flex; align-items: center; justify-content: center; flex-direction: column ">
{{ position.properties.desc }} {{ position.properties.desc }}
</span> </span>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -93,17 +81,11 @@
<script setup lang="ts"> <script setup lang="ts">
import {ref, reactive, onMounted} from 'vue'; import {ref, reactive, onMounted} from 'vue';
import shikigami from "../data/Shikigami.json" import shikigami from "../data/Shikigami.json"
import ShikigamiSelect from "@/components/ShikigamiSelect.vue"; import ShikigamiSelect from "@/components/flow/nodes/yys/ShikigamiSelect.vue";
import ShikigamiProperty from "@/components/ShikigamiProperty.vue"; import ShikigamiProperty from "@/components/flow/nodes/yys/ShikigamiProperty.vue";
import _ from "lodash"; import _ from "lodash";
import {useI18n} from "vue-i18n"; import {useI18n} from "vue-i18n";
import draggable from 'vuedraggable'; import draggable from 'vuedraggable';
import MindMap from "simple-mind-map";
import {ElMessageBox} from "element-plus";
import {useGlobalMessage} from "@/ts/useGlobalMessage";
const { showMessage } = useGlobalMessage();
const {t} = useI18n() const {t} = useI18n()
@ -127,20 +109,6 @@ const addGroupElement = () => {
editShikigami(props.groups[0].groupInfo.length - 1); editShikigami(props.groups[0].groupInfo.length - 1);
}; };
const removeGroupElement = async (positionIndex) =>{
try {
await ElMessageBox.confirm('确定要删除此元素吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
props.groups[0].groupInfo.splice(positionIndex, 1);
showMessage('success', '删除成功!');
} catch (error) {
showMessage('info', '已取消删除');
}
}
const editShikigami = (positionIndex) => { const editShikigami = (positionIndex) => {
// console.log("==== ===", groupIndex, positionIndex); // console.log("==== ===", groupIndex, positionIndex);
state.showSelectShikigami = true; state.showSelectShikigami = true;
@ -247,15 +215,14 @@ const getYuhunPropertyNames = (yuhun) => {
.group-card { .group-card {
position: relative; position: relative;
display: flex; display: flex;
flex-direction: row; flex-direction: column;
justify-content: left; justify-content: center;
align-items: center; align-items: center;
.avatar-container { .avatar-container {
position: relative; position: relative;
width: 50px; width: 100px;
height: 50px; height: 100px;
padding-right: 10px;
} }
.avatar-container span { .avatar-container span {
@ -276,12 +243,11 @@ const getYuhunPropertyNames = (yuhun) => {
.opt-btn { .opt-btn {
position: absolute; position: absolute;
top: 10px; top: 0px;
left: 0px; right: 0px;
z-index: 10; z-index: 10;
opacity: 0; opacity: 0;
} }
} }
/* 当鼠标悬停在容器上时显示按钮 */ /* 当鼠标悬停在容器上时显示按钮 */
@ -298,22 +264,4 @@ const getYuhunPropertyNames = (yuhun) => {
display: block; display: block;
cursor: pointer; cursor: pointer;
} }
.property-wrap {
display: flex;
flex-direction: row;
}
.shikigami-name {
width: 100px;
}
#mindMapContainer {
margin: 0;
padding: 0;
height: 400px;
min-width: 500px;
width: 100%;
}
</style> </style>

View File

@ -0,0 +1,202 @@
<script setup lang="ts">
import { ref } from 'vue';
import ShikigamiSelect from './nodes/yys/ShikigamiSelect.vue';
//
const baseComponents = [
{
id: 'rect',
name: '长方形',
type: 'rect',
description: '基础长方形节点'
},
{
id: 'ellipse',
name: '圆形',
type: 'ellipse',
description: '基础圆形节点'
},
{
id: 'image',
name: '图片',
type: 'imageNode',
description: '可上传图片的节点'
},
{
id: 'text',
name: '文字编辑框',
type: 'textNode',
description: '可编辑富文本的节点'
}
//
];
const yysComponents = [
{
id: 'shikigami-select',
name: '式神选择器',
type: 'shikigamiSelect',
description: '用于选择式神的组件'
},
{
id: 'yuhun-select',
name: '御魂选择器',
type: 'yuhunSelect',
description: '用于选择御魂的组件'
},
{
id: 'property-select',
name: '属性选择器',
type: 'propertySelect',
description: '用于设置属性要求的组件'
}
];
const emit = defineEmits(['add-node']);
//
const handleDragStart = (event, component) => {
//
event.dataTransfer.setData('application/json', JSON.stringify({
id: `${component.type}-${Date.now()}`,
type: component.type,
label: component.name,
data: { componentType: component.type }
}));
//
event.dataTransfer.effectAllowed = 'copy';
};
//
const handleComponentClick = (component) => {
// ID
const nodeId = `${component.type}-${Date.now()}`;
//
const newNode = {
id: nodeId,
type: component.type,
label: component.name,
position: { x: 100, y: 100 }, //
data: { componentType: component.type }
};
//
emit('add-node', newNode);
};
</script>
<template>
<div class="components-panel">
<h3>组件库</h3>
<div class="components-group">
<div class="group-title">基础组件</div>
<div class="components-list">
<div
v-for="component in baseComponents"
:key="component.id"
class="component-item"
@click="handleComponentClick(component)"
draggable="true"
@dragstart="handleDragStart($event, component)"
>
<div class="component-icon">
<i class="el-icon-plus"></i>
</div>
<div class="component-info">
<div class="component-name">{{ component.name }}</div>
<div class="component-desc">{{ component.description }}</div>
</div>
</div>
</div>
</div>
<div class="components-group">
<div class="group-title">阴阳师</div>
<div class="components-list">
<div
v-for="component in yysComponents"
:key="component.id"
class="component-item"
@click="handleComponentClick(component)"
draggable="true"
@dragstart="handleDragStart($event, component)"
>
<div class="component-icon">
<i class="el-icon-plus"></i>
</div>
<div class="component-info">
<div class="component-name">{{ component.name }}</div>
<div class="component-desc">{{ component.description }}</div>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.components-panel {
padding: 10px;
background-color: #f5f7fa;
border-bottom: 1px solid #e4e7ed;
margin-bottom: 10px;
}
.components-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.component-item {
display: flex;
align-items: center;
padding: 8px;
border: 1px solid #dcdfe6;
border-radius: 4px;
cursor: pointer;
background-color: white;
transition: all 0.3s;
}
.component-item:hover {
background-color: #f2f6fc;
border-color: #c6e2ff;
}
.component-icon {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background-color: #ecf5ff;
border-radius: 4px;
margin-right: 10px;
}
.component-info {
flex: 1;
}
.component-name {
font-weight: bold;
margin-bottom: 4px;
}
.component-desc {
font-size: 12px;
color: #909399;
}
.components-group {
margin-bottom: 18px;
}
.group-title {
font-weight: bold;
font-size: 15px;
margin-bottom: 6px;
color: #333;
}
</style>

View File

@ -0,0 +1,271 @@
<script setup lang="ts">
import { ref, onMounted, shallowRef, markRaw } from 'vue';
import { VueFlow, useVueFlow, Panel, NodeTypes } from '@vue-flow/core';
import { Background } from '@vue-flow/background';
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';
const props = defineProps({
height: {
type: String,
default: '100%'
}
});
const emit = defineEmits(['open-shikigami-select', 'open-yuhun-select', 'open-property-select']);
//
const nodeTypes = shallowRef({
shikigamiSelect: markRaw(ShikigamiSelectNode),
yuhunSelect: markRaw(YuhunSelectNode),
propertySelect: markRaw(PropertySelectNode),
imageNode: markRaw(ImageNode),
textNode: markRaw(TextNode)
});
// - 使ref
const initialNodes = [
{ id: '1', label: '开始', position: { x: 100, y: 100 }, type: 'input' }
];
// 线 - 使ref
const initialEdges = [];
// 使VueFlowAPIref
const { nodes, edges, onNodesChange, onEdgesChange, onConnect, addNodes, setTransform, getViewport, updateNode } = useVueFlow({
defaultNodes: initialNodes,
defaultEdges: initialEdges,
nodeTypes
});
//
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: '<div>双击右侧可编辑文字</div>',
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);
};
onMounted(() => {
console.log('FlowEditor 组件已挂载');
});
</script>
<template>
<div class="flow-editor" :style="{ height }">
<div class="editor-layout">
<!-- 左侧组件面板 -->
<div class="components-sidebar">
<ComponentsPanel @add-node="handleAddNode" />
</div>
<!-- 中间流程图区域 -->
<div class="flow-container">
<VueFlow
:nodes="nodes"
:edges="edges"
@nodes-change="onNodesChange"
@edges-change="onEdgesChange"
@connect="onConnect"
fit-view-on-init
@drop="handleDrop"
@dragover="handleDragOver"
>
<Background pattern-color="#aaa" gap="8" />
<Controls />
<Panel position="top-right" class="flow-panel">
<div>流程图编辑器 (模仿 draw.io)</div>
</Panel>
</VueFlow>
</div>
<!-- 右侧属性面板 -->
<PropertyPanel
:height="height"
@open-shikigami-select="handleOpenShikigamiSelect"
@open-yuhun-select="handleOpenYuhunSelect"
@open-property-select="handleOpenPropertySelect"
/>
</div>
</div>
</template>
<style scoped>
.flow-editor {
display: flex;
flex-direction: column;
width: 100%;
}
.editor-layout {
display: flex;
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);
}
</style>

View File

@ -0,0 +1,254 @@
<script setup lang="ts">
import { ref, computed, watch } from 'vue';
import { useVueFlow } from '@vue-flow/core';
import { QuillEditor } from '@vueup/vue-quill';
import '@vueup/vue-quill/dist/vue-quill.snow.css';
const props = defineProps({
height: {
type: String,
default: '100%'
}
});
const emit = defineEmits(['open-shikigami-select', 'open-yuhun-select', 'open-property-select']);
// 使VueFlowstore
const { findNode, getNodes } = useVueFlow();
// getNodesref
const nodes = getNodes;
//
const selectedNode = ref(null);
//
watch(nodes, (newNodes) => {
//
const selected = newNodes.find(node => node.selected);
selectedNode.value = selected || null;
}, { deep: true });
//
const hasNodeSelected = computed(() => !!selectedNode.value);
//
const nodeType = computed(() => {
if (!selectedNode.value) return '';
return selectedNode.value.type || 'default';
});
//
const handleSelectShikigami = () => {
if (selectedNode.value) {
emit('open-shikigami-select', selectedNode.value);
}
};
//
const handleSelectYuhun = () => {
if (selectedNode.value) {
emit('open-yuhun-select', selectedNode.value);
}
};
//
const handleSelectProperty = () => {
if (selectedNode.value) {
emit('open-property-select', selectedNode.value);
}
};
const updateNodeData = (key, value) => {
if (!selectedNode.value) return;
const node = findNode(selectedNode.value.id);
if (node) {
node.data = { ...node.data, [key]: value };
}
};
const handleImageUpload = (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (evt) => {
updateNodeData('url', evt.target.result);
};
reader.readAsDataURL(file);
};
const quillToolbar = [
[{ header: 1 }, { header: 2 }],
['bold', 'italic', 'underline', 'strike'],
[{ color: [] }, { background: [] }],
[{ list: 'bullet' }, { list: 'ordered' }],
[{ align: [] }],
['clean']
];
</script>
<template>
<div class="property-panel" :style="{ height }">
<div class="panel-header">
<h3>属性编辑</h3>
</div>
<div v-if="!hasNodeSelected" class="no-selection">
<p>请选择一个节点以编辑其属性</p>
</div>
<div v-else class="property-content">
<div class="property-section">
<div class="section-header">基本信息</div>
<div class="property-item">
<div class="property-label">节点ID</div>
<div class="property-value">{{ selectedNode.id }}</div>
</div>
<div class="property-item">
<div class="property-label">节点类型</div>
<div class="property-value">{{ nodeType }}</div>
</div>
</div>
<!-- 式神选择节点的特定属性 -->
<div v-if="nodeType === 'shikigamiSelect'" class="property-section">
<div class="section-header">式神属性</div>
<div class="property-item">
<el-button
type="primary"
@click="handleSelectShikigami"
style="width: 100%"
>
选择式神
</el-button>
</div>
</div>
<!-- 御魂选择节点的特定属性 -->
<div v-if="nodeType === 'yuhunSelect'" class="property-section">
<div class="section-header">御魂属性</div>
<div class="property-item">
<el-button
type="primary"
@click="handleSelectYuhun"
style="width: 100%"
>
选择御魂
</el-button>
</div>
</div>
<!-- 属性选择节点的特定属性 -->
<div v-if="nodeType === 'propertySelect'" class="property-section">
<div class="section-header">属性设置</div>
<div class="property-item">
<el-button
type="primary"
@click="handleSelectProperty"
style="width: 100%"
>
设置属性
</el-button>
</div>
</div>
<!-- 图片节点属性 -->
<div v-if="nodeType === 'imageNode'" class="property-section">
<div class="section-header">图片设置</div>
<div class="property-item">
<input type="file" accept="image/*" @change="handleImageUpload" />
<div v-if="selectedNode.data && selectedNode.data.url" style="margin-top:8px;">
<img :src="selectedNode.data.url" alt="预览" style="max-width:100%;max-height:100px;" />
</div>
</div>
</div>
<!-- 文本节点属性 -->
<div v-if="nodeType === 'textNode'" class="property-section">
<div class="section-header">文本编辑</div>
<div class="property-item">
<QuillEditor
v-model:content="selectedNode.data.html"
contentType="html"
:toolbar="quillToolbar"
theme="snow"
style="height:120px;"
@update:content="val => updateNodeData('html', val)"
/>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.property-panel {
background-color: #f5f7fa;
border-left: 1px solid #e4e7ed;
width: 280px;
display: flex;
flex-direction: column;
overflow-y: auto;
}
.panel-header {
padding: 10px;
border-bottom: 1px solid #e4e7ed;
background-color: #e4e7ed;
}
.panel-header h3 {
margin: 0;
font-size: 16px;
color: #303133;
}
.no-selection {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: #909399;
font-size: 14px;
padding: 20px;
text-align: center;
}
.property-content {
padding: 10px;
}
.property-section {
margin-bottom: 20px;
background-color: white;
border-radius: 4px;
border: 1px solid #dcdfe6;
overflow: hidden;
}
.section-header {
font-weight: bold;
padding: 10px;
background-color: #ecf5ff;
border-bottom: 1px solid #dcdfe6;
}
.property-item {
padding: 10px;
border-bottom: 1px solid #f0f0f0;
}
.property-item:last-child {
border-bottom: none;
}
.property-label {
font-size: 13px;
color: #606266;
margin-bottom: 5px;
}
.property-value {
font-size: 14px;
word-break: break-all;
}
</style>

View File

@ -0,0 +1,56 @@
<script setup lang="ts">
import { ref, watch } from 'vue';
import { Handle, useVueFlow } from '@vue-flow/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
});
const nodeWidth = ref(180);
const nodeHeight = ref(120);
// props.data
watch(() => props.data, (newData) => {
if (newData && newData.width) nodeWidth.value = newData.width;
if (newData && newData.height) nodeHeight.value = newData.height;
}, { immediate: true });
</script>
<template>
<div class="image-node" :style="{ width: `${nodeWidth}px`, height: `${nodeHeight}px` }">
<NodeResizer v-if="selected" :min-width="60" :min-height="60" :max-width="400" :max-height="400" />
<Handle type="target" position="left" :id="`${id}-target`" />
<div class="image-content">
<img v-if="props.data && props.data.url" :src="props.data.url" alt="图片节点" style="width:100%;height:100%;object-fit:contain;" />
<div v-else class="image-placeholder">未上传图片</div>
</div>
<Handle type="source" position="right" :id="`${id}-source`" />
</div>
</template>
<style scoped>
.image-node {
background: #fff;
border: 1px solid #dcdfe6;
border-radius: 4px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
overflow: hidden;
}
.image-content {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.image-placeholder {
color: #bbb;
font-size: 14px;
}
</style>

View File

@ -0,0 +1,51 @@
<script setup lang="ts">
import { ref, watch } from 'vue';
import { Handle, useVueFlow } from '@vue-flow/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
});
const nodeWidth = ref(200);
const nodeHeight = ref(120);
const html = ref('');
watch(() => props.data, (newData) => {
if (newData && newData.html !== undefined) html.value = newData.html;
if (newData && newData.width) nodeWidth.value = newData.width;
if (newData && newData.height) nodeHeight.value = newData.height;
}, { immediate: true });
</script>
<template>
<div class="text-node" :style="{ width: `${nodeWidth}px`, height: `${nodeHeight}px` }">
<NodeResizer v-if="selected" :min-width="80" :min-height="40" :max-width="400" :max-height="400" />
<Handle type="target" position="left" :id="`${id}-target`" />
<div class="text-content" v-html="html"></div>
<Handle type="source" position="right" :id="`${id}-source`" />
</div>
</template>
<style scoped>
.text-node {
background: #fff;
border: 1px solid #dcdfe6;
border-radius: 4px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
overflow: hidden;
}
.text-content {
width: 100%;
height: 100%;
padding: 8px;
font-size: 15px;
color: #333;
word-break: break-all;
overflow: auto;
}
</style>

View File

@ -0,0 +1,562 @@
<template>
<YuhunSelect
:showSelectYuhun="showYuhunSelect"
:currentYuhun="currentYuhun"
@closeSelectYuhun="closeYuhunSelect"
@updateYuhun="updateYuhunSelect"
/>
<el-dialog
v-model="show"
title="属性选择器"
@close="cancel"
:before-close="cancel"
width="80%"
>
<el-form :model="property" label-width="120px">
<el-tabs v-model="activeTab">
<!-- 基础属性选项卡 -->
<el-tab-pane label="基础属性" name="basic">
<el-form-item label="属性类型">
<el-select v-model="property.type" @change="handleTypeChange">
<el-option v-for="type in propertyTypes" :key="type.value" :label="type.label" :value="type.value"/>
</el-select>
</el-form-item>
<!-- 攻击属性 -->
<div v-if="property.type === 'attack'">
<el-form-item label="攻击值类型">
<el-radio-group v-model="property.attackType">
<el-radio label="fixed" size="large">固定值</el-radio>
<el-radio label="percentage" size="large">百分比</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="攻击值">
<el-input-number v-model="property.attackValue" :min="0" :precision="property.attackType === 'percentage' ? 1 : 0" />
<span v-if="property.attackType === 'percentage'">%</span>
</el-form-item>
</div>
<!-- 生命属性 -->
<div v-if="property.type === 'health'">
<el-form-item label="生命值类型">
<el-radio-group v-model="property.healthType">
<el-radio label="fixed" size="large">固定值</el-radio>
<el-radio label="percentage" size="large">百分比</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="生命值">
<el-input-number v-model="property.healthValue" :min="0" :precision="property.healthType === 'percentage' ? 1 : 0" />
<span v-if="property.healthType === 'percentage'">%</span>
</el-form-item>
</div>
<!-- 防御属性 -->
<div v-if="property.type === 'defense'">
<el-form-item label="防御值类型">
<el-radio-group v-model="property.defenseType">
<el-radio label="fixed" size="large">固定值</el-radio>
<el-radio label="percentage" size="large">百分比</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="防御值">
<el-input-number v-model="property.defenseValue" :min="0" :precision="property.defenseType === 'percentage' ? 1 : 0" />
<span v-if="property.defenseType === 'percentage'">%</span>
</el-form-item>
</div>
<!-- 速度属性 -->
<div v-if="property.type === 'speed'">
<el-form-item label="速度值">
<el-input-number v-model="property.speedValue" :min="0" :precision="0" />
</el-form-item>
</div>
<!-- 暴击相关属性 -->
<div v-if="property.type === 'crit'">
<el-form-item label="暴击率">
<el-input-number v-model="property.critRate" :min="0" :max="100" :precision="1" />
<span>%</span>
</el-form-item>
</div>
<div v-if="property.type === 'critDmg'">
<el-form-item label="暴击伤害">
<el-input-number v-model="property.critDmg" :min="0" :precision="1" />
<span>%</span>
</el-form-item>
</div>
<!-- 效果命中与抵抗 -->
<div v-if="property.type === 'effectHit'">
<el-form-item label="效果命中">
<el-input-number v-model="property.effectHitValue" :min="0" :precision="1" />
<span>%</span>
</el-form-item>
</div>
<div v-if="property.type === 'effectResist'">
<el-form-item label="效果抵抗">
<el-input-number v-model="property.effectResistValue" :min="0" :precision="1" />
<span>%</span>
</el-form-item>
</div>
<!-- 所有属性都显示的字段 -->
<el-form-item label="优先级">
<el-select v-model="property.priority">
<el-option label="必须" value="required"/>
<el-option label="推荐" value="recommended"/>
<el-option label="可选" value="optional"/>
</el-select>
</el-form-item>
</el-tab-pane>
<!-- 式神要求选项卡 -->
<el-tab-pane label="式神要求" name="shikigami">
<el-form-item label="等级要求">
<el-radio-group v-model="property.levelRequired" class="ml-4">
<el-radio value="40" size="large">40</el-radio>
<el-radio value="0" size="large">献祭</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="技能要求">
<el-radio-group v-model="property.skillRequiredMode" class="ml-4">
<el-radio value="all" size="large">全满</el-radio>
<el-radio value="111" size="large">111</el-radio>
<el-radio value="custom" size="large">自定义</el-radio>
</el-radio-group>
<div v-if="property.skillRequiredMode === 'custom'" style="display: flex; flex-direction: row; gap: 10px; width: 100%;">
<el-select v-for="(value, index) in property.skillRequired" :key="index" :placeholder="value"
style="margin-bottom: 10px;" @change="updateSkillRequired(index, $event)">
<el-option label="*" value="X"/>
<el-option label="1" value="1"/>
<el-option label="2" value="2"/>
<el-option label="3" value="3"/>
<el-option label="4" value="4"/>
<el-option label="5" value="5"/>
</el-select>
</div>
</el-form-item>
</el-tab-pane>
<!-- 御魂要求选项卡 -->
<el-tab-pane label="御魂要求" name="yuhun">
<div style="display: flex; flex-direction: row; width: 100%;">
<div style="display: flex; flex-direction: column; width: 50%;">
<el-form-item label="御魂套装">
<div style="display: flex; flex-direction: row; flex-wrap: wrap; gap: 5px;">
<img
v-for="(effect, index) in property.yuhun.yuhunSetEffect"
:key="index"
style="width: 50px; height: 50px;"
:src="effect.avatar"
class="image"
@click="openYuhunSelect(index)"
/>
<el-button type="primary" @click="openYuhunSelect(-1)">
<el-icon :size="20">
<CirclePlus/>
</el-icon>
</el-button>
</div>
</el-form-item>
<el-form-item label="御魂效果目标">
<el-select v-model="yuhunTarget" @change="handleYuhunTargetChange">
<el-option v-for="option in yuhunTargetOptions" :key="option.value" :label="t(option.label)" :value="option.value"/>
</el-select>
</el-form-item>
</div>
<div style="display: flex; flex-direction: column; width: 50%;">
<el-form-item label="2号位主属性">
<el-select multiple collapse-tags collapse-tags-tooltip :max-collapse-tags="2"
v-model="property.yuhun.property2">
<el-option label="攻击加成" value="Attack"/>
<el-option label="防御加成" value="Defense"/>
<el-option label="生命加成" value="Health"/>
<el-option label="速度" value="Speed"/>
</el-select>
</el-form-item>
<el-form-item label="4号位主属性">
<el-select multiple collapse-tags collapse-tags-tooltip :max-collapse-tags="2"
v-model="property.yuhun.property4">
<el-option label="攻击加成" value="Attack"/>
<el-option label="防御加成" value="Defense"/>
<el-option label="生命加成" value="Health"/>
<el-option label="效果命中" value="ControlHit"/>
<el-option label="效果抵抗" value="ControlMiss"/>
</el-select>
</el-form-item>
<el-form-item label="6号位主属性">
<el-select multiple collapse-tags collapse-tags-tooltip :max-collapse-tags="2"
v-model="property.yuhun.property6">
<el-option label="攻击加成" value="Attack"/>
<el-option label="防御加成" value="Defense"/>
<el-option label="生命加成" value="Health"/>
<el-option label="暴击" value="Crit"/>
<el-option label="暴击伤害" value="CritDamage"/>
</el-select>
</el-form-item>
</div>
</div>
</el-tab-pane>
<!-- 效果指标选项卡 -->
<el-tab-pane label="效果指标" name="effect">
<el-form-item label="伤害期望">
<el-input-number v-model="property.expectedDamage" :min="0" />
</el-form-item>
<el-form-item label="生存能力">
<el-slider v-model="property.survivalRate" :step="10" :marks="{0: '弱', 50: '中', 100: '强'}" />
</el-form-item>
<el-form-item label="输出偏向">
<el-select v-model="property.damageType">
<el-option label="普攻" value="normal"/>
<el-option label="技能" value="skill"/>
<el-option label="均衡" value="balanced"/>
</el-select>
</el-form-item>
</el-tab-pane>
</el-tabs>
<el-form-item label="额外描述">
<el-input v-model="property.description" type="textarea"/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="confirm">确认</el-button>
<el-button @click="cancel">取消</el-button>
</el-form-item>
</el-form>
</el-dialog>
</template>
<script setup>
import { ref, watch, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { CirclePlus } from '@element-plus/icons-vue';
import YuhunSelect from "@/components/flow/nodes/yys/YuhunSelect.vue";
// i18n
const { t } = useI18n();
const props = defineProps({
currentProperty: {
type: Object,
default: () => ({}),
},
showPropertySelect: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['closePropertySelect', 'updateProperty']);
//
const propertyTypes = [
{ label: '攻击', value: 'attack' },
{ label: '生命', value: 'health' },
{ label: '防御', value: 'defense' },
{ label: '速度', value: 'speed' },
{ label: '暴击率', value: 'crit' },
{ label: '暴击伤害', value: 'critDmg' },
{ label: '效果命中', value: 'effectHit' },
{ label: '效果抵抗', value: 'effectResist' },
];
//
const activeTab = ref('basic');
//
const showYuhunSelect = ref(false);
const currentYuhun = ref({ name: '未选择御魂', avatar: '', type: '' });
const yuhunIndex = ref(-1);
const yuhunTarget = ref('1');
//
const yuhunTargetOptions = [
{ label: 'yuhun_target.fullName.0', value: '0' },
{ label: 'yuhun_target.fullName.1', value: '1' },
{ label: 'yuhun_target.fullName.2', value: '2' },
{ label: 'yuhun_target.fullName.3', value: '3' },
{ label: 'yuhun_target.fullName.4', value: '4' },
{ label: 'yuhun_target.fullName.5', value: '5' },
{ label: 'yuhun_target.fullName.6', value: '6' },
{ label: 'yuhun_target.fullName.7', value: '7' },
{ label: 'yuhun_target.fullName.8', value: '8' },
{ label: 'yuhun_target.fullName.9', value: '9' },
{ label: 'yuhun_target.fullName.10', value: '10' },
{ label: 'yuhun_target.fullName.11', value: '11' },
{ label: 'yuhun_target.fullName.12', value: '12' },
];
//
const property = ref({
//
type: 'attack',
attackType: 'percentage',
attackValue: 0,
healthType: 'percentage',
healthValue: 0,
defenseType: 'percentage',
defenseValue: 0,
speedValue: 0,
critRate: 0,
critDmg: 0,
effectHitValue: 0,
effectResistValue: 0,
priority: 'optional',
//
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 show = ref(false);
// props
watch(() => props.showPropertySelect, (newVal) => {
show.value = newVal;
});
watch(() => props.currentProperty, (newVal) => {
if (newVal && Object.keys(newVal).length > 0) {
// 使
property.value = { ...newVal };
//
if (newVal.yuhun && newVal.yuhun.target) {
yuhunTarget.value = newVal.yuhun.target.toString();
}
}
}, { deep: true });
//
const handleTypeChange = (type) => {
console.log('属性类型变更为:', type);
};
//
const updateSkillRequired = (index, value) => {
property.value.skillRequired[index] = value;
};
//
const handleYuhunTargetChange = (value) => {
switch (value) {
case "0": {
property.value.yuhun.target = 0;
property.value.yuhun.property2 = ["Attack", "Defense", "Health", "Speed"];
property.value.yuhun.property4 = ["Attack", "Defense", "Health", "ControlHit", "ControlMiss"];
property.value.yuhun.property6 = ["Attack", "Defense", "Health", "Crit", "CritDamage"];
break;
}
case "1": {
property.value.yuhun.target = 1;
property.value.yuhun.property2 = ["Attack"];
property.value.yuhun.property4 = ["Attack"];
property.value.yuhun.property6 = ["Crit", "CritDamage"];
break;
}
case "2": {
property.value.yuhun.target = 2;
property.value.yuhun.property2 = ["Speed"];
property.value.yuhun.property4 = ["ControlHit"];
property.value.yuhun.property6 = ["Attack", "Defense", "Health", "Crit", "CritDamage"];
break;
}
case "3": {
property.value.yuhun.target = 3;
property.value.yuhun.property2 = ["Speed"];
property.value.yuhun.property4 = ["ControlMiss"];
property.value.yuhun.property6 = ["Attack", "Defense", "Health", "Crit", "CritDamage"];
break;
}
case "4": {
property.value.yuhun.target = 4;
property.value.yuhun.property2 = ["Health"];
property.value.yuhun.property4 = ["Health"];
property.value.yuhun.property6 = ["Health"];
break;
}
case "5": {
property.value.yuhun.target = 5;
property.value.yuhun.property2 = ["Attack"];
property.value.yuhun.property4 = ["Attack"];
property.value.yuhun.property6 = ["Attack"];
break;
}
case "6": {
property.value.yuhun.target = 6;
property.value.yuhun.property2 = ["Defense"];
property.value.yuhun.property4 = ["Defense"];
property.value.yuhun.property6 = ["Defense"];
break;
}
case "7": {
property.value.yuhun.target = 7;
property.value.yuhun.property2 = ["Speed"];
property.value.yuhun.property4 = ["Attack", "Defense", "Health", "ControlHit", "ControlMiss"];
property.value.yuhun.property6 = ["Attack", "Defense", "Health", "Crit", "CritDamage"];
break;
}
case "8": {
property.value.yuhun.target = 8;
property.value.yuhun.property2 = ["Attack", "Defense", "Health", "Speed"];
property.value.yuhun.property4 = ["Attack", "Defense", "Health", "ControlHit", "ControlMiss"];
property.value.yuhun.property6 = ["Crit"];
break;
}
case "9": {
property.value.yuhun.target = 9;
property.value.yuhun.property2 = ["Attack", "Defense", "Health", "Speed"];
property.value.yuhun.property4 = ["Attack", "Defense", "Health", "ControlHit", "ControlMiss"];
property.value.yuhun.property6 = ["CritDamage"];
break;
}
case "10": {
property.value.yuhun.target = 10;
property.value.yuhun.property2 = ["Speed"];
property.value.yuhun.property4 = ["Health"];
property.value.yuhun.property6 = ["Crit", "CritDamage"];
break;
}
case "11": {
property.value.yuhun.target = 11;
property.value.yuhun.property2 = ["Speed"];
property.value.yuhun.property4 = ["ControlHit", "ControlMiss"];
property.value.yuhun.property6 = ["Attack", "Defense", "Health", "Crit", "CritDamage"];
break;
}
case "12": {
property.value.yuhun.target = 12;
property.value.yuhun.property2 = ["Defense"];
property.value.yuhun.property4 = ["Defense"];
property.value.yuhun.property6 = ["Crit", "CritDamage"];
break;
}
}
};
//
const openYuhunSelect = (index) => {
yuhunIndex.value = index;
showYuhunSelect.value = true;
};
//
const closeYuhunSelect = () => {
showYuhunSelect.value = false;
};
//
const updateYuhunSelect = (yuhun, operator) => {
showYuhunSelect.value = false;
if (operator === "Update") {
if (yuhunIndex.value >= 0) {
property.value.yuhun.yuhunSetEffect[yuhunIndex.value] = JSON.parse(JSON.stringify(yuhun));
} else {
property.value.yuhun.yuhunSetEffect.push(JSON.parse(JSON.stringify(yuhun)));
}
} else if (operator === "Remove") {
if (yuhunIndex.value >= 0) {
property.value.yuhun.yuhunSetEffect.splice(yuhunIndex.value, 1);
}
}
};
//
const cancel = () => {
emit('closePropertySelect');
resetData();
};
//
const confirm = () => {
//
const result = JSON.parse(JSON.stringify(property.value));
emit('updateProperty', result);
resetData();
};
//
const resetData = () => {
yuhunTarget.value = '1';
property.value = {
//
type: 'attack',
attackType: 'percentage',
attackValue: 0,
healthType: 'percentage',
healthValue: 0,
defenseType: 'percentage',
defenseValue: 0,
speedValue: 0,
critRate: 0,
critDmg: 0,
effectHitValue: 0,
effectResistValue: 0,
priority: 'optional',
//
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: '',
};
};
</script>
<style scoped>
.el-form-item {
margin-bottom: 18px;
}
.image {
border-radius: 4px;
border: 1px solid #dcdfe6;
}
</style>

View File

@ -0,0 +1,402 @@
<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';
const props = defineProps({
data: Object,
id: String,
selected: Boolean
});
// Vue Flow
const { findNode, updateNode } = useVueFlow();
//
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;
}
});
onUnmounted(() => {
//
window.removeEventListener('update-property', handlePropertyUpdate);
});
//
const getPropertyTypeName = () => {
const typeMap = {
'attack': '攻击',
'health': '生命',
'defense': '防御',
'speed': '速度',
'crit': '暴击率',
'critDmg': '暴击伤害',
'effectHit': '效果命中',
'effectResist': '效果抵抗',
'未选择': '未选择'
};
return typeMap[currentProperty.value.type] || currentProperty.value.type;
};
//
const getPriorityName = () => {
const priorityMap = {
'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>
<div class="property-node" :class="[currentProperty.priority ? `priority-${currentProperty.priority}` : '']" :style="{ width: `${nodeWidth}px`, height: `${nodeHeight}px` }">
<NodeResizer
v-if="selected"
:min-width="150"
:min-height="150"
:max-width="300"
:max-height="300"
/>
<!-- 输入连接点 -->
<Handle type="target" position="left" :id="`${id}-target`" />
<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-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>
<style scoped>
.node-content {
background-color: white;
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 0;
cursor: pointer;
}
: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;
align-items: center;
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;
border: 1px dashed #c0c4cc;
display: flex;
align-items: center;
justify-content: center;
color: #909399;
font-size: 12px;
border-radius: 4px;
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;
margin-top: 5px;
border-top: 1px dashed #ebeef5;
padding-top: 5px;
word-break: break-all;
}
</style>

View File

@ -117,10 +117,10 @@ import { Quill, QuillEditor } from '@vueup/vue-quill'
import '@vueup/vue-quill/dist/vue-quill.bubble.css' import '@vueup/vue-quill/dist/vue-quill.bubble.css'
import '@vueup/vue-quill/dist/vue-quill.snow.css' // import '@vueup/vue-quill/dist/vue-quill.snow.css' //
import * as ElementPlusIconsVue from '@element-plus/icons-vue' import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import shikigamiData from '../data/Shikigami.json'; import shikigamiData from '../../../../data/Shikigami.json';
import _ from 'lodash'; import _ from 'lodash';
import {Action, ElMessage, ElMessageBox} from "element-plus"; import {Action, ElMessage, ElMessageBox} from "element-plus";
import { useGlobalMessage } from '../ts/useGlobalMessage'; import { useGlobalMessage } from '../../../../ts/useGlobalMessage';
import draggable from 'vuedraggable'; import draggable from 'vuedraggable';
const props = defineProps<{ const props = defineProps<{

View File

@ -123,10 +123,10 @@
</template> </template>
<script setup> <script setup>
import propertyData from "../data/property.json"; import propertyData from "../../../../data/property.json";
import {ref, watch, computed} from 'vue' import {ref, watch, computed} from 'vue'
import ShikigamiSelect from "@/components/ShikigamiSelect.vue"; import ShikigamiSelect from "@/components/flow/nodes/yys/ShikigamiSelect.vue";
import YuhunSelect from "@/components/YuhunSelect.vue"; import YuhunSelect from "@/components/flow/nodes/yys/YuhunSelect.vue";
import {useI18n} from 'vue-i18n' import {useI18n} from 'vue-i18n'
// import YuhunSelect from "./YuhunSelect.vue"; // import YuhunSelect from "./YuhunSelect.vue";

View File

@ -47,7 +47,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch, computed } from 'vue' import { ref, watch, computed } from 'vue'
import type { TabsPaneContext } from 'element-plus' import type { TabsPaneContext } from 'element-plus'
import shikigamiData from "../data/Shikigami.json" import shikigamiData from "../../../../data/Shikigami.json"
interface Shikigami { interface Shikigami {
name: string name: string

View File

@ -0,0 +1,202 @@
<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';
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);
// 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);
//
if (props.data && props.data.shikigami) {
currentShikigami.value = props.data.shikigami;
}
});
onUnmounted(() => {
//
window.removeEventListener('update-shikigami', handleShikigamiUpdate);
});
// 使
defineExpose({
updateNodeShikigami
});
</script>
<template>
<div class="shikigami-node" :style="{ width: `${nodeWidth}px`, height: `${nodeHeight}px` }">
<NodeResizer
v-if="selected"
:min-width="150"
:min-height="150"
:max-width="300"
:max-height="300"
/>
<!-- 输入连接点 -->
<Handle type="target" position="left" :id="`${id}-target`" />
<div class="node-content">
<div class="node-header">
<div class="node-title">式神选择</div>
</div>
<div class="node-body">
<div v-if="currentShikigami.avatar" class="shikigami-avatar">
<img :src="currentShikigami.avatar" alt="式神头像" />
</div>
<div v-else class="shikigami-placeholder">
点击选择式神
</div>
<div class="shikigami-name">{{ currentShikigami.name }}</div>
</div>
</div>
<!-- 输出连接点 -->
<Handle type="source" position="right" :id="`${id}-source`" />
</div>
</template>
<style scoped>
.node-content {
background-color: white;
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 0;
cursor: pointer;
}
: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;
}
.node-header {
padding: 8px 10px;
background-color: #ecf5ff;
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;
}
.shikigami-avatar {
width: 80px;
height: 80px;
margin-bottom: 8px;
transition: width 0.2s, height 0.2s;
}
.shikigami-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 4px;
}
.shikigami-placeholder {
width: 80px;
height: 80px;
border: 1px dashed #c0c4cc;
display: flex;
align-items: center;
justify-content: center;
color: #909399;
font-size: 12px;
border-radius: 4px;
margin-bottom: 8px;
transition: width 0.2s, height 0.2s;
}
.shikigami-name {
font-size: 14px;
margin-top: 5px;
}
</style>

View File

@ -0,0 +1,129 @@
<template>
<el-dialog
v-model="show"
title="请选择御魂"
@close="cancel"
:before-close="cancel"
>
<span>当前选择御魂{{ current.name }}</span>
<div style="display: flex; align-items: center;">
<el-input
placeholder="请输入内容"
v-model="searchText"
style="width: 200px; margin-right: 10px;"
/>
</div>
<el-tabs
v-model="activeName"
type="card"
class="demo-tabs"
@tab-click="handleClick"
editable
>
<el-tab-pane
v-for="(type, index) in yuhunTypes"
:key="index"
:label="type.label"
:name="type.name"
>
<div style="max-height: 600px; overflow-y: auto;">
<el-space wrap size="large">
<div style="display: flex;flex-direction: column;justify-content: center" v-for="i in filterYuhunByTypeAndSearch(type.name, searchText)" :key="i.name">
<el-button
style="width: 100px; height: 100px;"
@click.stop="confirm(i)"
>
<img :src="i.avatar" style="width: 99px; height: 99px;">
</el-button>
<span style="text-align: center; display: block;">{{i.name}}</span>
</div>
</el-space>
</div>
</el-tab-pane>
</el-tabs>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, watch, computed } from 'vue'
import type { TabsPaneContext } from 'element-plus'
import yuhunData from "../../../../data/Yuhun.json"
interface Yuhun {
name: string
shortName?: string
type: string
avatar: string
}
const props = defineProps({
currentYuhun: {
type: Object as () => Yuhun,
default: () => ({ name: '' })
},
showSelectYuhun: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['closeSelectYuhun', 'updateYuhun'])
const searchText = ref('') //
const activeName = ref('ALL')
let current = ref({name:''})
const show = ref(false)
const yuhunTypes = [
{ label: "全部", name: "ALL" },
{ label: "攻击类", name: "attack" },
{ label: "暴击类", name: "Crit" },
{ label: "生命类", name: "Health" },
{ label: "防御类", name: "Defense" },
{ label: "效果命中", name: "Effect" },
{ label: "效果抵抗", name: "EffectResist" },
{ label: "特殊类", name: "Special" }
]
watch(() => props.showSelectYuhun, (newVal) => {
show.value = newVal
})
watch(() => props.currentYuhun, (newVal) => {
console.log("YuhunSelect.vue" + current.value.name)
current.value = newVal
console.log("YuhunSelect.vue" + current.value.name)
}, {deep: true})
const handleClick = (tab: TabsPaneContext) => {
console.log('Tab clicked:', tab)
}
const cancel = () => {
emit('closeSelectYuhun')
show.value = false
}
const confirm = (yuhun: Yuhun) => {
emit('updateYuhun', yuhun)
searchText.value=''
activeName.value='ALL'
// cancel()
}
//
const filterYuhunByTypeAndSearch = (type: string, search: string) => {
let filteredList = yuhunData;
if (type.toLowerCase() !== 'all') {
filteredList = filteredList.filter(item =>
item.type.toLowerCase() === type.toLowerCase()
);
}
if (search.trim() !== '') {
return filteredList.filter(item =>
item.name.toLowerCase().includes(search.toLowerCase())
);
}
return filteredList;
}
</script>

View File

@ -0,0 +1,230 @@
<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';
const props = defineProps({
data: Object,
id: String,
selected: Boolean
});
// Vue Flow
const { findNode, updateNode } = useVueFlow();
//
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);
//
if (props.data && props.data.yuhun) {
currentYuhun.value = props.data.yuhun;
}
});
onUnmounted(() => {
//
window.removeEventListener('update-yuhun', handleYuhunUpdate);
});
// 使
defineExpose({
updateNodeYuhun
});
</script>
<template>
<div class="yuhun-node" :style="{ width: `${nodeWidth}px`, height: `${nodeHeight}px` }">
<NodeResizer
v-if="selected"
:min-width="150"
:min-height="150"
:max-width="300"
:max-height="300"
/>
<!-- 输入连接点 -->
<Handle type="target" position="left" :id="`${id}-target`" />
<div class="node-content">
<div class="node-header">
<div class="node-title">御魂选择</div>
</div>
<div class="node-body">
<div v-if="currentYuhun.avatar" class="yuhun-avatar">
<img :src="currentYuhun.avatar" alt="御魂图片" />
</div>
<div v-else class="yuhun-placeholder">
点击选择御魂
</div>
<div class="yuhun-name">{{ currentYuhun.name }}</div>
<div v-if="currentYuhun.type" class="yuhun-type">{{ currentYuhun.type }}</div>
</div>
</div>
<!-- 输出连接点 -->
<Handle type="source" position="right" :id="`${id}-source`" />
</div>
</template>
<style scoped>
.node-content {
background-color: white;
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 0;
cursor: pointer;
}
: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;
}
.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;
}
.yuhun-avatar {
width: 80px;
height: 80px;
margin-bottom: 8px;
transition: width 0.2s, height 0.2s;
}
.yuhun-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 4px;
}
.yuhun-placeholder {
width: 80px;
height: 80px;
border: 1px dashed #c0c4cc;
display: flex;
align-items: center;
justify-content: center;
color: #909399;
font-size: 12px;
border-radius: 4px;
margin-bottom: 8px;
transition: width 0.2s, height 0.2s;
}
.yuhun-name {
font-size: 14px;
margin-top: 5px;
}
.yuhun-type {
font-size: 12px;
color: #909399;
margin-top: 3px;
}
</style>

View File

@ -1,9 +1,4 @@
[ [
{
"avatar": "/assets/Shikigami/ssr/583.png",
"name": "卑弥呼",
"rarity": "SSR"
},
{ {
"avatar": "/assets/Shikigami/l/582.png", "avatar": "/assets/Shikigami/l/582.png",
"name": "巡音流歌", "name": "巡音流歌",

123
src/ts/dialogStore.js Normal file
View File

@ -0,0 +1,123 @@
// src/store/dialogStore.js
import { defineStore } from 'pinia';
export const useDialogStore = defineStore('dialog', {
state: () => ({
// 对话框可见性
shikigamiVisible: false,
yuhunVisible: false,
propertyVisible: false,
// 当前选中的节点信息
selectedNode: null,
// 当前选中的数据(式神、御魂、属性)
currentShikigami: { name: '未选择式神', avatar: '', rarity: '' },
currentYuhun: { name: '未选择御魂', avatar: '', type: '' },
currentProperty: { type: '未选择属性', priority: 'optional', description: '' },
}),
actions: {
// 打开式神选择对话框
openShikigamiDialog(node) {
this.selectedNode = node;
this.shikigamiVisible = true;
},
// 关闭式神选择对话框
closeShikigamiDialog() {
this.shikigamiVisible = false;
},
// 更新式神信息
updateShikigami(shikigami) {
if (this.selectedNode) {
try {
// 获取节点引用,尝试多种方式获取 Vue 组件实例
const nodeElement = document.querySelector(`[data-id="${this.selectedNode.id}"]`);
if (nodeElement) {
let nodeInstance = null;
// 方法1通过 __vueParentComponent (Vue 3)
if (nodeElement.__vueParentComponent && nodeElement.__vueParentComponent.ctx) {
nodeInstance = nodeElement.__vueParentComponent.ctx;
}
// 方法2通过 __vue_app__ (Vue 3)
else if (nodeElement.__vue_app__) {
nodeInstance = nodeElement.__vue_app__;
}
// 如果找到实例并且有更新方法,调用它
if (nodeInstance && nodeInstance.updateNodeShikigami) {
nodeInstance.updateNodeShikigami(shikigami);
console.log('式神信息已更新:', shikigami.name);
} else {
console.warn('无法调用节点更新方法');
// 备用方案:通过全局事件总线传递更新
window.dispatchEvent(new CustomEvent('update-shikigami', {
detail: { nodeId: this.selectedNode.id, shikigami },
}));
}
}
} catch (error) {
console.error('更新式神信息时出错:', error);
}
}
// 关闭对话框
this.shikigamiVisible = false;
},
// 打开御魂选择对话框
openYuhunDialog(node) {
this.selectedNode = node;
this.yuhunVisible = true;
},
// 关闭御魂选择对话框
closeYuhunDialog() {
this.yuhunVisible = false;
},
// 更新御魂信息
updateYuhun(yuhun) {
this.currentYuhun = yuhun;
this.closeYuhunDialog();
this.updateNodeData('yuhun', yuhun);
},
// 打开属性选择对话框
openPropertyDialog(node) {
this.selectedNode = node;
this.propertyVisible = true;
},
// 关闭属性选择对话框
closePropertyDialog() {
this.propertyVisible = false;
},
// 更新属性信息
updateProperty(property) {
this.currentProperty = property;
this.closePropertyDialog();
this.updateNodeData('property', property);
},
// 统一更新节点数据(通过事件总线或直接调用方法)
updateNodeData(type, data) {
if (this.selectedNode) {
// 方法1通过事件总线触发更新推荐
window.dispatchEvent(new CustomEvent(`update-${type}`, {
detail: { nodeId: this.selectedNode.id, data }
}));
// 方法2直接调用节点实例的方法如果节点组件支持
// const nodeElement = document.querySelector(`[data-id="${this.selectedNode.id}"]`);
// if (nodeElement && nodeElement.__vueParentComponent?.ctx?.[`updateNode${type.charAt(0).toUpperCase() + type.slice(1)}`]) {
// nodeElement.__vueParentComponent.ctx[`updateNode${type.charAt(0).toUpperCase() + type.slice(1)}`](data);
// }
}
},
},
});