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": {
|
||||
"@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",
|
||||
"element-plus": "^2.9.1",
|
||||
"html2canvas": "^1.4.1",
|
||||
"pinia": "^3.0.1",
|
||||
"simple-mind-map": "^0.13.1-fix.2",
|
||||
"vue": "^3.3.10",
|
||||
"vue-i18n": "^11.1.1",
|
||||
"vue3-draggable-resizable": "^1.6.5",
|
||||
|
109
src/App.vue
109
src/App.vue
@ -1,34 +1,68 @@
|
||||
<script setup lang="ts">
|
||||
import Yys from './components/Yys.vue';
|
||||
import Toolbar from './components/Toolbar.vue';
|
||||
import ProjectExplorer from './components/ProjectExplorer.vue';
|
||||
import { computed, ref, onMounted, onUnmounted } from "vue";
|
||||
import { useFilesStore } from "@/ts/files";
|
||||
import Vue3DraggableResizable from 'vue3-draggable-resizable';
|
||||
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 { updateNode } = useVueFlow();
|
||||
|
||||
const yysRef = ref(null);
|
||||
const width = ref('100%');
|
||||
const height = ref('100vh');
|
||||
const toolbarHeight = 48; // 工具栏的高度
|
||||
const windowHeight = ref(window.innerHeight);
|
||||
const contentHeight = computed(() => `${windowHeight.value - toolbarHeight}px`);
|
||||
|
||||
const onResizing = (x, y, width, height) => {
|
||||
width.value = width;
|
||||
height.value = height;
|
||||
// Dialogs and Selected Node Management
|
||||
const showShikigamiDialog = ref(false);
|
||||
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({
|
||||
x: 400,
|
||||
y: 20,
|
||||
width: 1080,
|
||||
height: windowHeight.value - toolbarHeight,
|
||||
isActive: false,
|
||||
// Handle Dialogs Close
|
||||
const closeDialogForType = (type: string) => {
|
||||
switch (type) {
|
||||
case 'shikigami': showShikigamiDialog.value = false; break;
|
||||
case 'yuhun': showYuhunDialog.value = false; break;
|
||||
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 = (
|
||||
targetName: String | undefined,
|
||||
@ -43,7 +77,7 @@ const handleTabsEdit = (
|
||||
label: newFileName,
|
||||
name: newFileName,
|
||||
visible: true,
|
||||
type: 'PVE',
|
||||
type: 'FLOW',
|
||||
groups: [
|
||||
{
|
||||
shortDescription: " ",
|
||||
@ -85,10 +119,7 @@ const activeFileGroups = computed(() => {
|
||||
<!-- 侧边栏和工作区 -->
|
||||
<div class="main-content">
|
||||
<!-- 侧边栏 -->
|
||||
<aside class="sidebar">
|
||||
<ProjectExplorer :allFiles="filesStore.fileList"/>
|
||||
</aside>
|
||||
|
||||
<ProjectExplorer />
|
||||
<!-- 工作区 -->
|
||||
<div class="workspace">
|
||||
<el-tabs
|
||||
@ -104,14 +135,44 @@ const activeFileGroups = computed(() => {
|
||||
:label="file.label"
|
||||
:name="file.name.toString()"
|
||||
>
|
||||
<main 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' "/>
|
||||
</main>
|
||||
<div id="main-container" :style="{ height: contentHeight, overflow: 'auto' }">
|
||||
<!-- 流程图编辑器 -->
|
||||
<FlowEditor
|
||||
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-tabs>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
@ -153,8 +214,8 @@ const activeFileGroups = computed(() => {
|
||||
height: 100%; /* 确保内容区域占满父容器 */
|
||||
overflow-y: auto; /* 允许内容滚动 */
|
||||
min-height: 100vh; /* 允许容器扩展 */
|
||||
//display: inline-block;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
</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>
|
||||
<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%;"
|
||||
handle=".drag-handle">
|
||||
<template class="group" #item="{ element: group, index: groupIndex }">
|
||||
<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>
|
||||
</el-row>
|
||||
</template>
|
||||
@ -11,16 +111,344 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref, reactive, toRefs, nextTick} from 'vue';
|
||||
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<{
|
||||
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>
|
||||
|
||||
<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 {
|
||||
@ -31,6 +459,190 @@ const props = defineProps<{
|
||||
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>
|
||||
.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="pvp-mindmap">
|
||||
<div id="mindMapContainer"></div>
|
||||
|
||||
</div>
|
||||
<div class="pvp-shikigami-editor">
|
||||
<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">
|
||||
<template #item="{element : position, index:positionIndex}">
|
||||
<div class="group-card">
|
||||
<div style="width: 20px;padding-left: 10px">
|
||||
{{positionIndex + 1}}
|
||||
</div>
|
||||
<div class="opt-btn" data-html2canvas-ignore="true">
|
||||
<!-- Add delete button here -->
|
||||
<el-button type="danger" icon="Delete" circle @click="removeGroupElement(positionIndex)"/>
|
||||
@ -40,49 +37,40 @@
|
||||
style="cursor: pointer; vertical-align: bottom;"
|
||||
class="avatar-image"
|
||||
@click="editShikigami(positionIndex)"/>
|
||||
|
||||
<!-- 文字图层 -->
|
||||
<!-- <span v-if="position.properties">{{ position.properties.levelRequired }}级 {{ position.properties.skillRequired.join('') }}</span>-->
|
||||
</div>
|
||||
<div class="opt-foot">
|
||||
<div class="property-wrap">
|
||||
<div class="shikigami-name">
|
||||
<div style="display: flex; justify-content: center;">
|
||||
<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(positionIndex)">{{ t('editProperties') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="position.properties">
|
||||
<div style="display: flex; justify-content: center;">
|
||||
<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;
|
||||
text-align: center; white-space: pre-wrap; display: flex; align-items: center; justify-content: center; flex-direction: column ">
|
||||
{{ getYuhunNames(position.properties.yuhun.yuhunSetEffect) }}
|
||||
</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 ">
|
||||
{{
|
||||
{{ getYuhunNames(position.properties.yuhun.yuhunSetEffect) }}<br/>{{
|
||||
t('yuhun_target.shortName.' + position.properties.yuhun.target)
|
||||
}}·{{ getYuhunPropertyNames(position.properties.yuhun) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="position.properties">
|
||||
<div>
|
||||
<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 }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
</el-space>
|
||||
@ -93,17 +81,11 @@
|
||||
<script setup lang="ts">
|
||||
import {ref, reactive, onMounted} from 'vue';
|
||||
import shikigami from "../data/Shikigami.json"
|
||||
import ShikigamiSelect from "@/components/ShikigamiSelect.vue";
|
||||
import ShikigamiProperty from "@/components/ShikigamiProperty.vue";
|
||||
import ShikigamiSelect from "@/components/flow/nodes/yys/ShikigamiSelect.vue";
|
||||
import ShikigamiProperty from "@/components/flow/nodes/yys/ShikigamiProperty.vue";
|
||||
import _ from "lodash";
|
||||
import {useI18n} from "vue-i18n";
|
||||
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()
|
||||
@ -127,20 +109,6 @@ const addGroupElement = () => {
|
||||
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) => {
|
||||
// console.log("==== 选择式神 ===", groupIndex, positionIndex);
|
||||
state.showSelectShikigami = true;
|
||||
@ -247,15 +215,14 @@ const getYuhunPropertyNames = (yuhun) => {
|
||||
.group-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: left;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.avatar-container {
|
||||
position: relative;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
padding-right: 10px;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.avatar-container span {
|
||||
@ -276,12 +243,11 @@ const getYuhunPropertyNames = (yuhun) => {
|
||||
|
||||
.opt-btn {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
z-index: 10;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* 当鼠标悬停在容器上时显示按钮 */
|
||||
@ -298,22 +264,4 @@ const getYuhunPropertyNames = (yuhun) => {
|
||||
display: block;
|
||||
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>
|
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.snow.css' // 引入样式文件
|
||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||
import shikigamiData from '../data/Shikigami.json';
|
||||
import shikigamiData from '../../../../data/Shikigami.json';
|
||||
import _ from 'lodash';
|
||||
import {Action, ElMessage, ElMessageBox} from "element-plus";
|
||||
import { useGlobalMessage } from '../ts/useGlobalMessage';
|
||||
import { useGlobalMessage } from '../../../../ts/useGlobalMessage';
|
||||
import draggable from 'vuedraggable';
|
||||
|
||||
const props = defineProps<{
|
@ -123,10 +123,10 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import propertyData from "../data/property.json";
|
||||
import propertyData from "../../../../data/property.json";
|
||||
import {ref, watch, computed} from 'vue'
|
||||
import ShikigamiSelect from "@/components/ShikigamiSelect.vue";
|
||||
import YuhunSelect from "@/components/YuhunSelect.vue";
|
||||
import ShikigamiSelect from "@/components/flow/nodes/yys/ShikigamiSelect.vue";
|
||||
import YuhunSelect from "@/components/flow/nodes/yys/YuhunSelect.vue";
|
||||
import {useI18n} from 'vue-i18n'
|
||||
// import YuhunSelect from "./YuhunSelect.vue";
|
||||
|
@ -47,7 +47,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, computed } from 'vue'
|
||||
import type { TabsPaneContext } from 'element-plus'
|
||||
import shikigamiData from "../data/Shikigami.json"
|
||||
import shikigamiData from "../../../../data/Shikigami.json"
|
||||
|
||||
interface Shikigami {
|
||||
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",
|
||||
"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