mirror of
https://github.com/Powerful-517/yys-editor.git
synced 2026-03-05 15:05:27 +00:00
refactor: 重构属性编辑面板,支持Tab分离和节点类型切换
- 将属性面板分为游戏属性和图像属性两个Tab - 游戏属性Tab包含节点基本信息、类型切换和特定属性 - 图像属性Tab包含所有样式设置(填充、描边、阴影等) - 资产选择器节点支持在式神和御魂之间切换 - 切换节点类型时自动清空已选资产 - 优化AssetSelectorPanel,移除重复的资产库选择器
This commit is contained in:
@@ -3,9 +3,10 @@ import { useDialogs } from '../ts/useDialogs'
|
||||
import ShikigamiSelect from './flow/nodes/yys/ShikigamiSelect.vue'
|
||||
import YuhunSelect from './flow/nodes/yys/YuhunSelect.vue'
|
||||
import PropertySelect from './flow/nodes/yys/PropertySelect.vue'
|
||||
import GenericImageSelector from './common/GenericImageSelector.vue'
|
||||
import { useFilesStore } from '../ts/useStore'
|
||||
|
||||
const { dialogs, closeDialog } = useDialogs();
|
||||
const { dialogs, closeDialog, closeGenericSelector } = useDialogs();
|
||||
const filesStore = useFilesStore();
|
||||
</script>
|
||||
|
||||
@@ -40,4 +41,16 @@ const filesStore = useFilesStore();
|
||||
closeDialog('property');
|
||||
}"
|
||||
/>
|
||||
<GenericImageSelector
|
||||
v-if="dialogs.generic.show && dialogs.generic.config"
|
||||
v-model="dialogs.generic.show"
|
||||
:config="dialogs.generic.config"
|
||||
@select="data => {
|
||||
dialogs.generic.callback?.(data);
|
||||
closeGenericSelector();
|
||||
}"
|
||||
@update:modelValue="value => {
|
||||
if (!value) closeGenericSelector();
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
114
src/components/common/GenericImageSelector.vue
Normal file
114
src/components/common/GenericImageSelector.vue
Normal file
@@ -0,0 +1,114 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="show"
|
||||
:title="config.title"
|
||||
>
|
||||
<span v-if="config.currentItem">
|
||||
当前选择:{{ config.currentItem[config.itemRender.labelField] }}
|
||||
</span>
|
||||
|
||||
<!-- 搜索框 -->
|
||||
<div v-if="config.searchable !== false" style="display: flex; align-items: center;">
|
||||
<el-input
|
||||
v-model="searchText"
|
||||
placeholder="请输入内容"
|
||||
style="width: 200px; margin-right: 10px;"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Tab分组 -->
|
||||
<el-tabs
|
||||
v-model="activeTab"
|
||||
type="card"
|
||||
class="demo-tabs"
|
||||
>
|
||||
<el-tab-pane
|
||||
v-for="group in config.groups"
|
||||
:key="group.name"
|
||||
:label="group.label"
|
||||
:name="group.name"
|
||||
>
|
||||
<div style="max-height: 600px; overflow-y: auto;">
|
||||
<el-space wrap size="large">
|
||||
<div
|
||||
v-for="item in filteredItems(group)"
|
||||
:key="item[config.itemRender.labelField]"
|
||||
style="display: flex; flex-direction: column; justify-content: center"
|
||||
>
|
||||
<el-button
|
||||
:style="`width: ${imageSize}px; height: ${imageSize}px;`"
|
||||
@click="handleSelect(item)"
|
||||
>
|
||||
<img
|
||||
:src="item[config.itemRender.imageField]"
|
||||
:style="`width: ${imageSize - 1}px; height: ${imageSize - 1}px;`"
|
||||
>
|
||||
</el-button>
|
||||
<span style="text-align: center; display: block;">
|
||||
{{ item[config.itemRender.labelField] }}
|
||||
</span>
|
||||
</div>
|
||||
</el-space>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import type { SelectorConfig, GroupConfig } from '@/types/selector'
|
||||
|
||||
const props = defineProps<{
|
||||
config: SelectorConfig
|
||||
modelValue: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean]
|
||||
'select': [item: any]
|
||||
}>()
|
||||
|
||||
const show = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
const searchText = ref('')
|
||||
const activeTab = ref('ALL')
|
||||
const imageSize = computed(() => props.config.itemRender.imageSize || 100)
|
||||
|
||||
// 过滤逻辑
|
||||
const filteredItems = (group: GroupConfig) => {
|
||||
let items = props.config.dataSource
|
||||
|
||||
// 分组过滤
|
||||
if (group.name !== 'ALL') {
|
||||
if (group.filter) {
|
||||
items = items.filter(group.filter)
|
||||
} else {
|
||||
items = items.filter(item =>
|
||||
item[props.config.groupField]?.toLowerCase() === group.name.toLowerCase()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索过滤
|
||||
if (searchText.value.trim()) {
|
||||
const searchFields = props.config.searchFields || [props.config.itemRender.labelField]
|
||||
items = items.filter(item =>
|
||||
searchFields.some(field =>
|
||||
item[field]?.toLowerCase().includes(searchText.value.toLowerCase())
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
const handleSelect = (item: any) => {
|
||||
emit('select', item)
|
||||
searchText.value = ''
|
||||
activeTab.value = 'ALL'
|
||||
}
|
||||
</script>
|
||||
@@ -58,6 +58,16 @@ const componentGroups = [
|
||||
id: 'yys',
|
||||
title: '阴阳师',
|
||||
components: [
|
||||
{
|
||||
id: 'asset-selector',
|
||||
name: '资产选择器',
|
||||
type: 'assetSelector',
|
||||
description: '通用资产选择器(式神/御魂等)',
|
||||
data: {
|
||||
assetLibrary: 'shikigami',
|
||||
selectedAsset: null
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'shikigami-select',
|
||||
name: '式神选择器',
|
||||
|
||||
@@ -73,6 +73,7 @@ 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 AssetSelectorNode from './nodes/common/AssetSelectorNode.vue';
|
||||
// import TextNode from './nodes/common/TextNode.vue';
|
||||
import PropertyPanel from './PropertyPanel.vue';
|
||||
import { useGlobalMessage } from '@/ts/useGlobalMessage';
|
||||
@@ -660,6 +661,7 @@ function registerNodes(lfInstance: LogicFlow) {
|
||||
register({ type: 'propertySelect', component: PropertySelectNode }, lfInstance);
|
||||
|
||||
register({ type: 'imageNode', component: ImageNode }, lfInstance);
|
||||
register({ type: 'assetSelector', component: AssetSelectorNode }, lfInstance);
|
||||
// register({ type: 'textNode', component: TextNode }, lfInstance);
|
||||
}
|
||||
|
||||
@@ -915,6 +917,11 @@ onMounted(() => {
|
||||
normalizeAllNodes();
|
||||
});
|
||||
|
||||
// 监听节点点击事件,更新选中节点
|
||||
lfInstance.on(EventType.NODE_CLICK, ({ data }) => {
|
||||
selectedNode.value = data;
|
||||
});
|
||||
|
||||
// 监听空白点击事件,取消选中
|
||||
lfInstance.on(EventType.BLANK_CLICK, () => {
|
||||
selectedNode.value = null;
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import ShikigamiPanel from './panels/ShikigamiPanel.vue';
|
||||
import YuhunPanel from './panels/YuhunPanel.vue';
|
||||
import PropertyRulePanel from './panels/PropertyRulePanel.vue';
|
||||
import ImagePanel from './panels/ImagePanel.vue';
|
||||
import TextPanel from './panels/TextPanel.vue';
|
||||
import StylePanel from './panels/StylePanel.vue';
|
||||
import AssetSelectorPanel from './panels/AssetSelectorPanel.vue';
|
||||
import { ASSET_LIBRARIES } from '@/types/nodeTypes';
|
||||
import { getLogicFlowInstance } from '@/ts/useLogicFlow';
|
||||
|
||||
const props = defineProps({
|
||||
height: {
|
||||
@@ -26,15 +29,36 @@ const nodeType = computed(() => {
|
||||
return selectedNode.value.type || 'default';
|
||||
});
|
||||
|
||||
const activeTab = ref('game');
|
||||
|
||||
const panelMap: Record<string, any> = {
|
||||
shikigamiSelect: ShikigamiPanel,
|
||||
yuhunSelect: YuhunPanel,
|
||||
propertySelect: PropertyRulePanel,
|
||||
imageNode: ImagePanel,
|
||||
textNode: TextPanel
|
||||
textNode: TextPanel,
|
||||
assetSelector: AssetSelectorPanel
|
||||
};
|
||||
|
||||
const panelComponent = computed(() => panelMap[nodeType.value] || null);
|
||||
|
||||
// 判断是否支持节点类型切换(仅资产选择器节点支持)
|
||||
const supportsTypeSwitch = computed(() => nodeType.value === 'assetSelector');
|
||||
|
||||
// 当前资产库类型
|
||||
const currentAssetLibrary = computed({
|
||||
get: () => selectedNode.value?.properties?.assetLibrary || 'shikigami',
|
||||
set: (value) => {
|
||||
const lf = getLogicFlowInstance();
|
||||
if (!lf || !selectedNode.value) return;
|
||||
|
||||
lf.setProperties(selectedNode.value.id, {
|
||||
...selectedNode.value.properties,
|
||||
assetLibrary: value,
|
||||
selectedAsset: null // 切换类型时清空已选资产
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -48,27 +72,53 @@ const panelComponent = computed(() => panelMap[nodeType.value] || null);
|
||||
</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>
|
||||
<!-- Tab 切换 -->
|
||||
<el-tabs v-model="activeTab" class="property-tabs">
|
||||
<!-- 游戏属性 Tab -->
|
||||
<el-tab-pane label="游戏属性" name="game">
|
||||
<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>
|
||||
|
||||
<StylePanel :node="selectedNode" />
|
||||
<!-- 节点类型切换(仅资产选择器支持) -->
|
||||
<div v-if="supportsTypeSwitch" class="property-section">
|
||||
<div class="section-header">节点类型</div>
|
||||
<div class="property-item">
|
||||
<div class="property-label">资产类型</div>
|
||||
<el-select v-model="currentAssetLibrary" placeholder="选择资产类型" style="width: 100%">
|
||||
<el-option
|
||||
v-for="lib in ASSET_LIBRARIES"
|
||||
:key="lib.id"
|
||||
:label="lib.label"
|
||||
:value="lib.id"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<component v-if="panelComponent" :is="panelComponent" :node="selectedNode" />
|
||||
<div v-else class="property-section">
|
||||
<div class="section-header">暂无特定属性</div>
|
||||
<div class="property-item">
|
||||
<div class="property-value">当前节点类型无需额外配置。</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 特定节点属性面板 -->
|
||||
<component v-if="panelComponent" :is="panelComponent" :node="selectedNode" />
|
||||
<div v-else class="property-section">
|
||||
<div class="section-header">暂无特定属性</div>
|
||||
<div class="property-item">
|
||||
<div class="property-value">当前节点类型无需额外配置。</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 图像属性 Tab -->
|
||||
<el-tab-pane label="图像属性" name="style">
|
||||
<StylePanel :node="selectedNode" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -108,6 +158,21 @@ const panelComponent = computed(() => panelMap[nodeType.value] || null);
|
||||
|
||||
.property-content {
|
||||
padding: 10px;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.property-tabs {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.property-tabs :deep(.el-tabs__content) {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.property-section {
|
||||
|
||||
102
src/components/flow/nodes/common/AssetSelectorNode.vue
Normal file
102
src/components/flow/nodes/common/AssetSelectorNode.vue
Normal file
@@ -0,0 +1,102 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, inject, onMounted, onBeforeUnmount } from 'vue';
|
||||
import { toTextStyle } from '@/ts/nodeStyle';
|
||||
import { useNodeAppearance } from '@/ts/useNodeAppearance';
|
||||
|
||||
const currentAsset = ref({ name: '未选择资产', avatar: '', library: 'shikigami' });
|
||||
const getNode = inject('getNode') as (() => any) | undefined;
|
||||
const zIndex = ref(1);
|
||||
let intervalId: number | null = null;
|
||||
|
||||
// 使用轮询方式定期更新 zIndex
|
||||
onMounted(() => {
|
||||
const node = getNode?.();
|
||||
if (node) {
|
||||
zIndex.value = node.zIndex ?? 1;
|
||||
|
||||
// 每 100ms 检查一次 zIndex 是否变化
|
||||
intervalId = window.setInterval(() => {
|
||||
const currentZIndex = node.zIndex ?? 1;
|
||||
if (zIndex.value !== currentZIndex) {
|
||||
zIndex.value = currentZIndex;
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (intervalId !== null) {
|
||||
clearInterval(intervalId);
|
||||
}
|
||||
});
|
||||
|
||||
const { containerStyle, textStyle } = useNodeAppearance({
|
||||
onPropsChange(props) {
|
||||
if (props.selectedAsset) {
|
||||
currentAsset.value = props.selectedAsset;
|
||||
}
|
||||
if (props.assetLibrary && !props.selectedAsset) {
|
||||
// 如果切换了资产库但没有选中资产,更新占位文本
|
||||
currentAsset.value = {
|
||||
name: '未选择资产',
|
||||
avatar: '',
|
||||
library: props.assetLibrary
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const mergedContainerStyle = computed(() => ({ ...containerStyle.value, boxSizing: 'border-box' }));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="node-content" :style="mergedContainerStyle">
|
||||
<div class="zindex-badge">{{ zIndex }}</div>
|
||||
<img
|
||||
v-if="currentAsset.avatar"
|
||||
:src="currentAsset.avatar"
|
||||
:alt="currentAsset.name"
|
||||
class="asset-image"
|
||||
draggable="false"
|
||||
/>
|
||||
<div v-else class="placeholder-text" :style="textStyle">点击选择资产</div>
|
||||
<div class="name-text" :style="textStyle">{{ currentAsset.name }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.node-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
.zindex-badge {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
background: rgba(64, 158, 255, 0.9);
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
padding: 2px 6px;
|
||||
border-radius: 10px;
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
}
|
||||
.asset-image {
|
||||
width: 85%;
|
||||
height: 85%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.placeholder-text {
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
}
|
||||
.name-text {
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
margin-top: 8px;
|
||||
}
|
||||
</style>
|
||||
83
src/components/flow/panels/AssetSelectorPanel.vue
Normal file
83
src/components/flow/panels/AssetSelectorPanel.vue
Normal file
@@ -0,0 +1,83 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { useDialogs } from '@/ts/useDialogs';
|
||||
import { getLogicFlowInstance } from '@/ts/useLogicFlow';
|
||||
import { SELECTOR_PRESETS } from '@/configs/selectorPresets';
|
||||
|
||||
const props = defineProps<{
|
||||
node: any;
|
||||
}>();
|
||||
|
||||
const { openGenericSelector } = useDialogs();
|
||||
|
||||
// 当前选中的资产库
|
||||
const currentLibrary = computed(() => props.node.properties?.assetLibrary || 'shikigami');
|
||||
|
||||
// 当前选中的资产
|
||||
const currentAsset = computed(() => {
|
||||
return props.node.properties?.selectedAsset || { name: '未选择' };
|
||||
});
|
||||
|
||||
// 打开选择器
|
||||
const handleOpenSelector = () => {
|
||||
const lf = getLogicFlowInstance();
|
||||
const node = props.node;
|
||||
if (!lf || !node) return;
|
||||
|
||||
const library = currentLibrary.value;
|
||||
const config = SELECTOR_PRESETS[library];
|
||||
|
||||
if (!config) {
|
||||
console.error('未找到资产库配置:', library);
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置当前选中项
|
||||
config.currentItem = node.properties?.selectedAsset;
|
||||
|
||||
openGenericSelector(config, (selectedItem) => {
|
||||
lf.setProperties(node.id, {
|
||||
...node.properties,
|
||||
selectedAsset: selectedItem,
|
||||
assetLibrary: library
|
||||
});
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="property-section">
|
||||
<div class="section-header">资产属性</div>
|
||||
|
||||
<div class="property-item">
|
||||
<div class="property-label">当前选择</div>
|
||||
<span>{{ currentAsset.name }}</span>
|
||||
<el-button type="primary" @click="handleOpenSelector" style="width: 100%; margin-top: 8px">
|
||||
选择资产
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.property-section {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
font-weight: bold;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
}
|
||||
|
||||
.property-item {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.property-label {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user