mirror of
https://github.com/Powerful-517/yys-editor.git
synced 2025-05-19 08:35:25 +00:00
init commit
This commit is contained in:
parent
13db9c4e7b
commit
721acb9033
@ -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",
|
||||||
|
117
src/App.vue
117
src/App.vue
@ -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>
|
|
@ -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>
|
|
@ -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>
|
@ -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>
|
202
src/components/flow/ComponentsPanel.vue
Normal file
202
src/components/flow/ComponentsPanel.vue
Normal 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>
|
271
src/components/flow/FlowEditor.vue
Normal file
271
src/components/flow/FlowEditor.vue
Normal 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 = [];
|
||||||
|
|
||||||
|
// 使用VueFlow的API,传入普通数组而非ref
|
||||||
|
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>
|
254
src/components/flow/PropertyPanel.vue
Normal file
254
src/components/flow/PropertyPanel.vue
Normal 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']);
|
||||||
|
|
||||||
|
// 使用VueFlow的store获取当前选中的节点
|
||||||
|
const { findNode, getNodes } = useVueFlow();
|
||||||
|
// getNodes是一个ref对象,而不是函数
|
||||||
|
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>
|
56
src/components/flow/nodes/common/ImageNode.vue
Normal file
56
src/components/flow/nodes/common/ImageNode.vue
Normal 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>
|
51
src/components/flow/nodes/common/TextNode.vue
Normal file
51
src/components/flow/nodes/common/TextNode.vue
Normal 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>
|
562
src/components/flow/nodes/yys/PropertySelect.vue
Normal file
562
src/components/flow/nodes/yys/PropertySelect.vue
Normal 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>
|
402
src/components/flow/nodes/yys/PropertySelectNode.vue
Normal file
402
src/components/flow/nodes/yys/PropertySelectNode.vue
Normal 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>
|
@ -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<{
|
@ -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";
|
||||||
|
|
@ -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
|
202
src/components/flow/nodes/yys/ShikigamiSelectNode.vue
Normal file
202
src/components/flow/nodes/yys/ShikigamiSelectNode.vue
Normal 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>
|
129
src/components/flow/nodes/yys/YuhunSelect.vue
Normal file
129
src/components/flow/nodes/yys/YuhunSelect.vue
Normal 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>
|
230
src/components/flow/nodes/yys/YuhunSelectNode.vue
Normal file
230
src/components/flow/nodes/yys/YuhunSelectNode.vue
Normal 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>
|
@ -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
123
src/ts/dialogStore.js
Normal 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);
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user