feat: 添加阴阳师和技能选择器,完成资产选择器统一架构迁移

- 新增阴阳师和阴阳师技能资产类型配置
- 添加 54 张阴阳师和技能图片资源
- 将式神和御魂选择器迁移到统一的 assetSelector 架构
- 删除 10 个冗余的独立节点和面板组件
- 统一使用 GenericImageSelector 通用选择器
- 完全实现配置驱动的设计理念
- 减少约 800+ 行重复代码

所有资产类型(式神/御魂/阴阳师/技能)现在都通过单一的 assetSelector 节点和通用选择器处理
This commit is contained in:
2026-02-17 01:39:24 +08:00
parent 40e9dcef78
commit 777fc2c944
68 changed files with 135 additions and 536 deletions

View File

@@ -1,7 +1,5 @@
<script setup lang="ts">
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'
@@ -11,26 +9,6 @@ const filesStore = useFilesStore();
</script>
<template>
<ShikigamiSelect
v-if="dialogs.shikigami.show"
:showSelectShikigami="dialogs.shikigami.show"
:currentShikigami="dialogs.shikigami.data"
@closeSelectShikigami="closeDialog('shikigami')"
@updateShikigami="data => {
dialogs.shikigami.callback?.(data);
closeDialog('shikigami');
}"
/>
<YuhunSelect
v-if="dialogs.yuhun.show"
:showSelectYuhun="dialogs.yuhun.show"
:currentYuhun="dialogs.yuhun.data"
@closeSelectYuhun="closeDialog('yuhun')"
@updateYuhun="data => {
dialogs.yuhun.callback?.(data);
closeDialog('yuhun');
}"
/>
<PropertySelect
v-if="dialogs.property.show"
:showPropertySelect="dialogs.property.show"

View File

@@ -71,19 +71,21 @@ const componentGroups = [
{
id: 'shikigami-select',
name: '式神选择器',
type: 'shikigamiSelect',
type: 'assetSelector',
description: '用于选择式神的组件',
data: {
shikigami: { name: '未选择式神', avatar: '', rarity: '' }
assetLibrary: 'shikigami',
selectedAsset: null
}
},
{
id: 'yuhun-select',
name: '御魂选择器',
type: 'yuhunSelect',
type: 'assetSelector',
description: '用于选择御魂的组件',
data: {
yuhun: { name: '未选择御魂', avatar: '', type: '' }
assetLibrary: 'yuhun',
selectedAsset: null
}
},
{
@@ -113,6 +115,26 @@ const componentGroups = [
damageType: "balanced"
}
}
},
{
id: 'onmyoji-select',
name: '阴阳师选择器',
type: 'assetSelector',
description: '用于选择阴阳师的组件',
data: {
assetLibrary: 'onmyoji',
selectedAsset: null
}
},
{
id: 'onmyoji-skill-select',
name: '阴阳师技能选择器',
type: 'assetSelector',
description: '用于选择阴阳师技能的组件',
data: {
assetLibrary: 'onmyojiSkill',
selectedAsset: null
}
}
]
},

View File

@@ -69,8 +69,6 @@ import '@logicflow/extension/es/index.css';
import { translateEdgeData, translateNodeData } from '@logicflow/core/es/keyboard/shortcut';
import { register } from '@logicflow/vue-node-registry';
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';
@@ -666,8 +664,6 @@ function distributeSelected(type: DistributeType) {
// 注册自定义节点
function registerNodes(lfInstance: LogicFlow) {
register({ type: 'shikigamiSelect', component: ShikigamiSelectNode }, lfInstance);
register({ type: 'yuhunSelect', component: YuhunSelectNode }, lfInstance);
register({ type: 'propertySelect', component: PropertySelectNode }, lfInstance);
register({ type: 'imageNode', component: ImageNode }, lfInstance);

View File

@@ -1,7 +1,5 @@
<script setup lang="ts">
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';
@@ -32,8 +30,6 @@ const nodeType = computed(() => {
const activeTab = ref('game');
const panelMap: Record<string, any> = {
shikigamiSelect: ShikigamiPanel,
yuhunSelect: YuhunPanel,
propertySelect: PropertyRulePanel,
imageNode: ImagePanel,
textNode: TextPanel,

View File

@@ -240,7 +240,7 @@
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";
// import YuhunSelect from "@/components/flow/nodes/yys/YuhunSelect.vue";
// 获取当前的 i18n 实例
const { t } = useI18n();

View File

@@ -1,120 +0,0 @@
<template>
<el-dialog
v-model="show"
title="请选择式神"
>
<span>当前选择式神{{ props.currentShikigami.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="(rarity, index) in rarityLevels"
:key="index"
:label="rarity.label"
:name="rarity.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 filterShikigamiByRarityAndSearch(rarity.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, computed } from 'vue'
import type { TabsPaneContext } from 'element-plus'
import shikigamiData from "../../../../data/Shikigami.json"
interface Shikigami {
name: string
avatar: string
rarity: string
}
const props = defineProps({
currentShikigami: {
type: Object as () => Shikigami,
default: () => ({ name: '未选择式神', avatar: '', rarity: '' })
},
showSelectShikigami: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['closeSelectShikigami', 'updateShikigami'])
const show = computed({
get() {
return props.showSelectShikigami
},
set(value) {
if (!value) {
emit('closeSelectShikigami')
}
}
})
const searchText = ref('')
const activeName = ref('ALL')
const rarityLevels = [
{ label: "全部", name: "ALL" },
{ label: "UR", name: "UR" },
{ label: "SP", name: "SP" },
{ label: "SSR", name: "SSR" },
{ label: "SR", name: "SR" },
{ label: "R", name: "R" },
{ label: "N", name: "N" },
{ label: "联动", name: "L" },
{ label: "呱太", name: "G" },
]
const handleClick = (tab: TabsPaneContext) => {
console.log('Tab clicked:', tab)
}
const confirm = (shikigami: Shikigami) => {
emit('updateShikigami', shikigami)
searchText.value = ''
activeName.value = 'ALL'
}
// 修改后的过滤函数
const filterShikigamiByRarityAndSearch = (rarity: string, search: string) => {
let filteredList = shikigamiData;
if (rarity.toLowerCase() !== 'all') {
filteredList = filteredList.filter(item =>
item.rarity.toLowerCase() === rarity.toLowerCase()
);
}
if (search.trim() !== '') {
return filteredList.filter(item =>
item.name.toLowerCase().includes(search.toLowerCase())
);
}
return filteredList;
}
</script>

View File

@@ -1,94 +0,0 @@
<script setup lang="ts">
import { computed, ref, inject, onMounted, onBeforeUnmount } from 'vue';
import { toTextStyle } from '@/ts/nodeStyle';
import { useNodeAppearance } from '@/ts/useNodeAppearance';
const currentShikigami = ref({ name: '未选择式神', avatar: '', rarity: '' });
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.shikigami) {
currentShikigami.value = props.shikigami;
}
}
});
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="currentShikigami.avatar"
:src="currentShikigami.avatar"
:alt="currentShikigami.name"
class="shikigami-image"
draggable="false"
/>
<div v-else class="placeholder-text" :style="textStyle">点击选择式神</div>
<div class="name-text" :style="textStyle">{{ currentShikigami.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;
}
.shikigami-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>

View File

@@ -1,120 +0,0 @@
<template>
<el-dialog
v-model="show"
title="请选择御魂"
>
<span>当前选择御魂{{ props.currentYuhun.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, 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: '未选择御魂', type: '', avatar: '' })
},
showSelectYuhun: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['closeSelectYuhun', 'updateYuhun'])
const show = computed({
get() {
return props.showSelectYuhun
},
set(value) {
if (!value) {
emit('closeSelectYuhun')
}
}
})
const searchText = ref('') // 搜索文本
const activeName = ref('ALL')
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" }
]
const handleClick = (tab: TabsPaneContext) => {
console.log('Tab clicked:', tab)
}
const confirm = (yuhun: Yuhun) => {
emit('updateYuhun', yuhun)
searchText.value = ''
activeName.value = 'ALL'
}
// 过滤函数
const filterYuhunByTypeAndSearch = (type: string, search: string) => {
let filteredList = yuhunData;
if (type.toLowerCase() !== 'all') {
filteredList = filteredList.filter(item =>
item.type.toLowerCase() === type.toLowerCase()
);
}
if (search.trim() !== '') {
return filteredList.filter(item =>
item.name.toLowerCase().includes(search.toLowerCase())
);
}
return filteredList;
}
</script>

View File

@@ -1,97 +0,0 @@
<script setup lang="ts">
import { ref, computed, inject, onMounted, onBeforeUnmount } from 'vue';
import { useNodeAppearance } from '@/ts/useNodeAppearance';
const currentYuhun = ref({ name: '未选择御魂', avatar: '', type: '' });
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.yuhun) {
currentYuhun.value = props.yuhun;
}
}
});
</script>
<template>
<div class="node-content" :style="containerStyle">
<div class="zindex-badge">{{ zIndex }}</div>
<img
v-if="currentYuhun.avatar"
:src="currentYuhun.avatar"
:alt="currentYuhun.name"
class="yuhun-image"
draggable="false"
/>
<div v-else class="placeholder-text" :style="textStyle">点击选择御魂</div>
<div class="name-text" :style="textStyle">{{ currentYuhun.name }}</div>
<div v-if="currentYuhun.type" class="type-text" :style="textStyle">{{ currentYuhun.type }}</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;
}
.yuhun-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;
}
.type-text {
font-size: 12px;
color: #909399;
margin-top: 4px;
}
</style>

View File

@@ -1,34 +0,0 @@
<script setup lang="ts">
import { useDialogs } from '@/ts/useDialogs';
import { getLogicFlowInstance } from '@/ts/useLogicFlow';
const props = defineProps<{
node: any;
}>();
const { openDialog } = useDialogs();
const handleOpenDialog = () => {
const lf = getLogicFlowInstance();
const node = props.node;
if (!lf || !node) return;
const currentData = node.properties?.shikigami;
openDialog('shikigami', currentData, node, (updatedData) => {
lf.setProperties(node.id, {
...node.properties,
shikigami: updatedData
});
});
};
</script>
<template>
<div class="property-section">
<div class="section-header">式神属性</div>
<div class="property-item">
<span>当前选择式神{{ node.properties?.shikigami?.name || '未选择' }}</span>
<el-button type="primary" @click="handleOpenDialog" style="width: 100%">选择式神</el-button>
</div>
</div>
</template>

View File

@@ -1,33 +0,0 @@
<script setup lang="ts">
import { useDialogs } from '@/ts/useDialogs';
import { getLogicFlowInstance } from '@/ts/useLogicFlow';
const props = defineProps<{
node: any;
}>();
const { openDialog } = useDialogs();
const handleOpenDialog = () => {
const lf = getLogicFlowInstance();
const node = props.node;
if (!lf || !node) return;
const currentData = node.properties?.yuhun;
openDialog('yuhun', currentData, node, (updatedData) => {
lf.setProperties(node.id, {
...node.properties,
yuhun: updatedData
});
});
};
</script>
<template>
<div class="property-section">
<div class="section-header">御魂属性</div>
<div class="property-item">
<el-button type="primary" @click="handleOpenDialog" style="width: 100%">选择御魂</el-button>
</div>
</div>
</template>