From 5e665966db10627d1473914c53fa509647415fe6 Mon Sep 17 00:00:00 2001 From: rookie4show Date: Tue, 24 Feb 2026 20:10:30 +0800 Subject: [PATCH 1/2] feat(flow): migrate text node to quill rendering and transparent default style --- src/components/flow/ComponentsPanel.vue | 7 +- src/components/flow/nodes/common/TextNode.vue | 93 ++++++++-- .../flow/nodes/common/TextNodeModel.ts | 159 ++++++------------ src/components/flow/panels/TextPanel.vue | 119 +++++++++++-- 4 files changed, 244 insertions(+), 134 deletions(-) diff --git a/src/components/flow/ComponentsPanel.vue b/src/components/flow/ComponentsPanel.vue index 524d394..3dfae15 100644 --- a/src/components/flow/ComponentsPanel.vue +++ b/src/components/flow/ComponentsPanel.vue @@ -47,7 +47,10 @@ const componentGroups = [ type: 'textNode', description: '可编辑文本的节点', data: { - text: '双击编辑文字', + text: { + content: '

请输入文本

', + rich: true + }, width: 200, height: 120 } @@ -279,4 +282,4 @@ const handleMouseDown = (e, component) => { margin-bottom: 6px; color: #333; } - \ No newline at end of file + diff --git a/src/components/flow/nodes/common/TextNode.vue b/src/components/flow/nodes/common/TextNode.vue index 9afe0b9..1e18711 100644 --- a/src/components/flow/nodes/common/TextNode.vue +++ b/src/components/flow/nodes/common/TextNode.vue @@ -1,30 +1,95 @@ diff --git a/src/components/flow/nodes/common/TextNodeModel.ts b/src/components/flow/nodes/common/TextNodeModel.ts index 98c6acb..b34b59b 100644 --- a/src/components/flow/nodes/common/TextNodeModel.ts +++ b/src/components/flow/nodes/common/TextNodeModel.ts @@ -1,131 +1,74 @@ import { HtmlNodeModel } from '@logicflow/core'; +const DEFAULT_TEXT_HTML = '

请输入文本

'; +const DEFAULT_TEXT_STYLE = { + fill: 'transparent', + stroke: '', + shadow: { + color: 'transparent', + blur: 0, + offsetX: 0, + offsetY: 0 + } +}; + +const parseSize = (value: unknown, fallback: number) => { + const next = Number(value); + return Number.isFinite(next) && next > 0 ? next : fallback; +}; + +const normalizeTextProperty = (rawText: any) => { + if (typeof rawText === 'string') { + return { + content: rawText, + rich: rawText.trim().startsWith('<') + }; + } + + if (rawText && typeof rawText === 'object') { + const content = typeof rawText.content === 'string' ? rawText.content : DEFAULT_TEXT_HTML; + const rich = rawText.rich == null ? content.trim().startsWith('<') : rawText.rich !== false; + return { + ...rawText, + content, + rich + }; + } + + return { + content: DEFAULT_TEXT_HTML, + rich: true + }; +}; + class TextNodeModel extends HtmlNodeModel { initNodeData(data: any) { super.initNodeData(data); - // 从 data 中读取宽高,支持调整大小后的持久化 - if (data.properties?.width) { - this.width = data.properties.width; - } else { - this.width = 200; - } + this.width = parseSize(data?.properties?.width, 200); + this.height = parseSize(data?.properties?.height, 120); - if (data.properties?.height) { - this.height = data.properties.height; - } else { - this.height = 120; - } + this.setProperty('width', this.width); + this.setProperty('height', this.height); + this.setProperty('text', normalizeTextProperty(data?.properties?.text)); - // 计算 Label 宽度 - const labelWidth = this.width - 20; - - // 初始化或更新 Label 配置 - if (data.properties?._label) { - // 如果已有 _label 配置,更新其宽度和坐标 - // 处理数组情况(兼容旧数据) - let currentLabel = data.properties._label; - if (Array.isArray(currentLabel)) { - currentLabel = currentLabel[0] || {}; - } - - this.setProperty('_label', { - value: currentLabel.value || '双击编辑文本', - content: currentLabel.content || currentLabel.value || '双击编辑文本', - x: data.x, - y: data.y, - labelWidth: labelWidth, - textOverflowMode: 'wrap', - editable: true, - draggable: false, - }); - } else if (data.properties?.text) { - // 如果有 text 属性但没有 _label,创建 _label - this.setProperty('_label', { - value: data.properties.text, - content: data.properties.text, - x: data.x, - y: data.y, - labelWidth: labelWidth, - textOverflowMode: 'wrap', - editable: true, - draggable: false, - }); - } else { - // 如果都没有,初始化一个默认的 label - this.setProperty('_label', { - value: '双击编辑文本', - content: '双击编辑文本', - x: data.x, - y: data.y, - labelWidth: labelWidth, - textOverflowMode: 'wrap', - editable: true, - draggable: false, - }); + const hasStyle = Object.prototype.hasOwnProperty.call(data?.properties || {}, 'style'); + if (!hasStyle) { + this.setProperty('style', DEFAULT_TEXT_STYLE); } } setAttributes() { - // 设置默认尺寸(如果 initNodeData 中没有设置) - if (!this.width) { - this.width = 200; - } - if (!this.height) { - this.height = 120; - } + if (!this.width) this.width = 200; + if (!this.height) this.height = 120; } - // 监听节点大小变化,更新 Label 宽度 resize(deltaX: number, deltaY: number) { const result = super.resize?.(deltaX, deltaY); - - // 持久化宽高到 properties this.setProperty('width', this.width); this.setProperty('height', this.height); - - // 更新 Label 宽度和坐标 - let currentLabel = this.properties._label || {}; - if (Array.isArray(currentLabel)) { - currentLabel = currentLabel[0] || {}; - } - - this.setProperty('_label', { - value: currentLabel.value || '双击编辑文本', - content: currentLabel.content || currentLabel.value || '双击编辑文本', - x: this.x, - y: this.y, - labelWidth: this.width - 20, - textOverflowMode: 'wrap', - editable: true, - draggable: false, - }); - return result; } - - // 当文本被编辑后,同步到 properties - updateText(value: string) { - super.updateText(value); - this.setProperty('text', value); - - // 同时更新 _label 中的 value - let currentLabel = this.properties._label || {}; - if (Array.isArray(currentLabel)) { - currentLabel = currentLabel[0] || {}; - } - - this.setProperty('_label', { - value: value, - content: value, - x: this.x, - y: this.y, - labelWidth: this.width - 20, - textOverflowMode: 'wrap', - editable: true, - draggable: false, - }); - } } export default TextNodeModel; diff --git a/src/components/flow/panels/TextPanel.vue b/src/components/flow/panels/TextPanel.vue index 1d17be6..039b6f2 100644 --- a/src/components/flow/panels/TextPanel.vue +++ b/src/components/flow/panels/TextPanel.vue @@ -1,22 +1,121 @@ + + From f8eb2f6563d1927687cc2a9c04ba2b60e23f148b Mon Sep 17 00:00:00 2001 From: rookie4show Date: Tue, 24 Feb 2026 20:28:03 +0800 Subject: [PATCH 2/2] refactor(selector): simplify presets with data-driven groups --- .../common/GenericImageSelector.vue | 2 + .../flow/panels/AssetSelectorPanel.vue | 16 +- src/configs/selectorPresets.ts | 143 +++++++++++------- src/types/selector.ts | 49 +++--- 4 files changed, 118 insertions(+), 92 deletions(-) diff --git a/src/components/common/GenericImageSelector.vue b/src/components/common/GenericImageSelector.vue index df8d1e1..05749ef 100644 --- a/src/components/common/GenericImageSelector.vue +++ b/src/components/common/GenericImageSelector.vue @@ -86,6 +86,8 @@ const filteredItems = (group: GroupConfig) => { if (group.name !== 'ALL') { if (group.filter) { items = items.filter(group.filter) + } else if (!props.config.groupField) { + items = [] } else { items = items.filter(item => item[props.config.groupField]?.toLowerCase() === group.name.toLowerCase() diff --git a/src/components/flow/panels/AssetSelectorPanel.vue b/src/components/flow/panels/AssetSelectorPanel.vue index 2ba94c7..c3363ac 100644 --- a/src/components/flow/panels/AssetSelectorPanel.vue +++ b/src/components/flow/panels/AssetSelectorPanel.vue @@ -3,6 +3,7 @@ import { computed } from 'vue'; import { useDialogs } from '@/ts/useDialogs'; import { getLogicFlowInstance } from '@/ts/useLogicFlow'; import { SELECTOR_PRESETS } from '@/configs/selectorPresets'; +import type { SelectorConfig } from '@/types/selector'; const props = defineProps<{ node: any; @@ -10,30 +11,29 @@ const props = defineProps<{ 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]; + const preset = SELECTOR_PRESETS[library]; - if (!config) { + if (!preset) { console.error('未找到资产库配置:', library); return; } - // 设置当前选中项 - config.currentItem = node.properties?.selectedAsset; + const config: SelectorConfig = { + ...preset, + currentItem: node.properties?.selectedAsset || null + }; openGenericSelector(config, (selectedItem) => { lf.setProperties(node.id, { @@ -80,4 +80,4 @@ const handleOpenSelector = () => { color: #606266; margin-bottom: 8px; } - + \ No newline at end of file diff --git a/src/configs/selectorPresets.ts b/src/configs/selectorPresets.ts index b7ff008..82a27e1 100644 --- a/src/configs/selectorPresets.ts +++ b/src/configs/selectorPresets.ts @@ -3,7 +3,7 @@ * 定义各种资产类型的选择器配置 */ -import type { SelectorConfig } from '@/types/selector' +import type { GroupConfig, SelectorConfig } from '@/types/selector' import shikigamiData from '@/data/Shikigami.json' import yuhunData from '@/data/Yuhun.json' @@ -13,8 +13,8 @@ const onmyojiData = [ { id: '11', name: '神乐', avatar: '/assets/downloaded_images/hero_11_11.png' }, { id: '12', name: '八百比丘尼', avatar: '/assets/downloaded_images/hero_12_12.png' }, { id: '13', name: '源博雅', avatar: '/assets/downloaded_images/hero_13_13.png' }, - { id: '15', name: '不知火', avatar: '/assets/downloaded_images/hero_15_15.png' }, - { id: '16', name: '鬼灯', avatar: '/assets/downloaded_images/hero_16_16.png' } + { id: '15', name: '源赖光', avatar: '/assets/downloaded_images/hero_15_15.png' }, + { id: '16', name: '藤原道长', avatar: '/assets/downloaded_images/hero_16_16.png' } ] // 阴阳师技能数据 @@ -59,25 +59,89 @@ const onmyojiSkillData = [ { onmyojiId: '13', onmyojiName: '源博雅', skillId: '9032', name: '通用技能2', avatar: '/assets/downloaded_images/hero_13_skill_9032.png' }, { onmyojiId: '13', onmyojiName: '源博雅', skillId: '9033', name: '通用技能3', avatar: '/assets/downloaded_images/hero_13_skill_9033.png' }, - // 不知火的技能 - { onmyojiId: '15', onmyojiName: '不知火', skillId: '1501', name: '技能1', avatar: '/assets/downloaded_images/hero_15_skill_1501.png' }, - { onmyojiId: '15', onmyojiName: '不知火', skillId: '1502', name: '技能2', avatar: '/assets/downloaded_images/hero_15_skill_1502.png' }, - { onmyojiId: '15', onmyojiName: '不知火', skillId: '1503', name: '技能3', avatar: '/assets/downloaded_images/hero_15_skill_1503.png' }, - { onmyojiId: '15', onmyojiName: '不知火', skillId: '1504', name: '技能4', avatar: '/assets/downloaded_images/hero_15_skill_1504.png' }, - { onmyojiId: '15', onmyojiName: '不知火', skillId: '1505', name: '技能5', avatar: '/assets/downloaded_images/hero_15_skill_1505.png' }, - { onmyojiId: '15', onmyojiName: '不知火', skillId: '1506', name: '技能6', avatar: '/assets/downloaded_images/hero_15_skill_1506.png' }, - { onmyojiId: '15', onmyojiName: '不知火', skillId: '1507', name: '技能7', avatar: '/assets/downloaded_images/hero_15_skill_1507.png' }, - { onmyojiId: '15', onmyojiName: '不知火', skillId: '1508', name: '技能8', avatar: '/assets/downloaded_images/hero_15_skill_1508.png' }, + // 源赖光的技能 + { onmyojiId: '15', onmyojiName: '源赖光', skillId: '1501', name: '技能1', avatar: '/assets/downloaded_images/hero_15_skill_1501.png' }, + { onmyojiId: '15', onmyojiName: '源赖光', skillId: '1502', name: '技能2', avatar: '/assets/downloaded_images/hero_15_skill_1502.png' }, + { onmyojiId: '15', onmyojiName: '源赖光', skillId: '1503', name: '技能3', avatar: '/assets/downloaded_images/hero_15_skill_1503.png' }, + { onmyojiId: '15', onmyojiName: '源赖光', skillId: '1504', name: '技能4', avatar: '/assets/downloaded_images/hero_15_skill_1504.png' }, + { onmyojiId: '15', onmyojiName: '源赖光', skillId: '1505', name: '技能5', avatar: '/assets/downloaded_images/hero_15_skill_1505.png' }, + { onmyojiId: '15', onmyojiName: '源赖光', skillId: '1506', name: '技能6', avatar: '/assets/downloaded_images/hero_15_skill_1506.png' }, + { onmyojiId: '15', onmyojiName: '源赖光', skillId: '1507', name: '技能7', avatar: '/assets/downloaded_images/hero_15_skill_1507.png' }, + { onmyojiId: '15', onmyojiName: '源赖光', skillId: '1508', name: '技能8', avatar: '/assets/downloaded_images/hero_15_skill_1508.png' }, - // 鬼灯的技能 - { onmyojiId: '16', onmyojiName: '鬼灯', skillId: '1601', name: '技能1', avatar: '/assets/downloaded_images/hero_16_skill_1601.png' }, - { onmyojiId: '16', onmyojiName: '鬼灯', skillId: '1602', name: '技能2', avatar: '/assets/downloaded_images/hero_16_skill_1602.png' }, - { onmyojiId: '16', onmyojiName: '鬼灯', skillId: '1603', name: '技能3', avatar: '/assets/downloaded_images/hero_16_skill_1603.png' }, - { onmyojiId: '16', onmyojiName: '鬼灯', skillId: '1604', name: '技能4', avatar: '/assets/downloaded_images/hero_16_skill_1604.png' }, - { onmyojiId: '16', onmyojiName: '鬼灯', skillId: '1605', name: '技能5', avatar: '/assets/downloaded_images/hero_16_skill_1605.png' }, - { onmyojiId: '16', onmyojiName: '鬼灯', skillId: '1606', name: '技能6', avatar: '/assets/downloaded_images/hero_16_skill_1606.png' }, - { onmyojiId: '16', onmyojiName: '鬼灯', skillId: '1607', name: '技能7', avatar: '/assets/downloaded_images/hero_16_skill_1607.png' }, - { onmyojiId: '16', onmyojiName: '鬼灯', skillId: '1608', name: '技能8', avatar: '/assets/downloaded_images/hero_16_skill_1608.png' } + // 藤原道长的技能 + { onmyojiId: '16', onmyojiName: '藤原道长', skillId: '1601', name: '技能1', avatar: '/assets/downloaded_images/hero_16_skill_1601.png' }, + { onmyojiId: '16', onmyojiName: '藤原道长', skillId: '1602', name: '技能2', avatar: '/assets/downloaded_images/hero_16_skill_1602.png' }, + { onmyojiId: '16', onmyojiName: '藤原道长', skillId: '1603', name: '技能3', avatar: '/assets/downloaded_images/hero_16_skill_1603.png' }, + { onmyojiId: '16', onmyojiName: '藤原道长', skillId: '1604', name: '技能4', avatar: '/assets/downloaded_images/hero_16_skill_1604.png' }, + { onmyojiId: '16', onmyojiName: '藤原道长', skillId: '1605', name: '技能5', avatar: '/assets/downloaded_images/hero_16_skill_1605.png' }, + { onmyojiId: '16', onmyojiName: '藤原道长', skillId: '1606', name: '技能6', avatar: '/assets/downloaded_images/hero_16_skill_1606.png' }, + { onmyojiId: '16', onmyojiName: '藤原道长', skillId: '1607', name: '技能7', avatar: '/assets/downloaded_images/hero_16_skill_1607.png' }, + { onmyojiId: '16', onmyojiName: '藤原道长', skillId: '1608', name: '技能8', avatar: '/assets/downloaded_images/hero_16_skill_1608.png' } +] + +const ALL_GROUP: GroupConfig = { label: '\u5168\u90e8', name: 'ALL' } + +const buildGroupsFromField = ( + dataSource: Array>, + field: string, + options?: { + order?: string[] + labelMap?: Record + } +): GroupConfig[] => { + const raw = new Set() + dataSource.forEach((item) => { + const value = item?.[field] + if (value != null && String(value).trim()) { + raw.add(String(value)) + } + }) + + const values = Array.from(raw) + const order = options?.order ?? [] + const ordered = [ + ...order.filter((value) => values.includes(value)), + ...values.filter((value) => !order.includes(value)).sort((a, b) => a.localeCompare(b)) + ] + + return [ + ALL_GROUP, + ...ordered.map((value) => ({ + label: options?.labelMap?.[value] ?? value, + name: value + })) + ] +} + +const shikigamiGroups = buildGroupsFromField(shikigamiData as any[], 'rarity', { + order: ['UR', 'SP', 'SSR', 'SR', 'R', 'N', 'L', 'G'], + labelMap: { + L: '\u8054\u52a8', + G: '\u5451\u592a' + } +}) + +const yuhunGroups = buildGroupsFromField(yuhunData as any[], 'type', { + order: ['attack', 'Crit', 'Health', 'Defense', 'ControlHit', 'ControlMiss', 'PVE', 'CritDamage'], + labelMap: { + attack: '\u653b\u51fb\u7c7b', + Crit: '\u66b4\u51fb\u7c7b', + Health: '\u751f\u547d\u7c7b', + Defense: '\u9632\u5fa1\u7c7b', + ControlHit: '\u6548\u679c\u547d\u4e2d', + ControlMiss: '\u6548\u679c\u62b5\u6297', + PVE: 'PVE', + CritDamage: '\u66b4\u51fb\u4f24\u5bb3' + } +}) + +const onmyojiSkillGroups = [ + ALL_GROUP, + ...onmyojiData.map((item) => ({ + label: item.name, + name: item.name + })) ] export const SELECTOR_PRESETS: Record = { @@ -85,17 +149,7 @@ export const SELECTOR_PRESETS: Record = { title: '请选择式神', dataSource: shikigamiData, groupField: 'rarity', - groups: [ - { 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' } - ], + groups: shikigamiGroups, itemRender: { imageField: 'avatar', labelField: 'name' @@ -106,16 +160,7 @@ export const SELECTOR_PRESETS: Record = { title: '请选择御魂', dataSource: yuhunData, groupField: 'type', - groups: [ - { label: '全部', name: 'ALL' }, - { label: '攻击类', name: 'attack' }, - { label: '暴击类', name: 'Crit' }, - { label: '生命类', name: 'Health' }, - { label: '防御类', name: 'Defense' }, - { label: '效果命中', name: 'Effect' }, - { label: '效果抵抗', name: 'EffectResist' }, - { label: '特殊类', name: 'Special' } - ], + groups: yuhunGroups, itemRender: { imageField: 'avatar', labelField: 'name' @@ -126,9 +171,7 @@ export const SELECTOR_PRESETS: Record = { title: '请选择阴阳师', dataSource: onmyojiData, groupField: null, - groups: [ - { label: '全部', name: 'ALL' } - ], + groups: [ALL_GROUP], itemRender: { imageField: 'avatar', labelField: 'name' @@ -139,15 +182,7 @@ export const SELECTOR_PRESETS: Record = { title: '请选择阴阳师技能', dataSource: onmyojiSkillData, groupField: 'onmyojiName', - groups: [ - { label: '全部', name: 'ALL' }, - { label: '晴明', name: '晴明' }, - { label: '神乐', name: '神乐' }, - { label: '八百比丘尼', name: '八百比丘尼' }, - { label: '源博雅', name: '源博雅' }, - { label: '不知火', name: '不知火' }, - { label: '鬼灯', name: '鬼灯' } - ], + groups: onmyojiSkillGroups, itemRender: { imageField: 'avatar', labelField: 'name' diff --git a/src/types/selector.ts b/src/types/selector.ts index 3e6aa0f..1127134 100644 --- a/src/types/selector.ts +++ b/src/types/selector.ts @@ -1,34 +1,23 @@ -/** - * 通用选择器配置接口 - * 用于配置驱动的图片选择器组件 - */ +export interface GroupConfig { + label: string + name: string + filter?: (item: T) => boolean +} -export interface GroupConfig { - label: string // Tab显示标签 - name: string // Tab标识符 - filter?: (item: any) => boolean // 自定义过滤函数(可选) +export interface SelectorItemRender { + imageField: string + labelField: string + imageSize?: number } export interface SelectorConfig { - // 基础配置 - title: string // 对话框标题 - dataSource: T[] // 数据源 - groupField: string // 分组字段名 (如 'rarity', 'type') - - // 分组配置 - groups: GroupConfig[] // Tab分组配置 - - // 展示配置 - itemRender: { - imageField: string // 图片字段名 (如 'avatar') - labelField: string // 标签字段名 (如 'name') - imageSize?: number // 图片尺寸,默认100px - } - - // 搜索配置 - searchable?: boolean // 是否启用搜索,默认true - searchFields?: string[] // 搜索字段,默认使用labelField - - // 当前选中项 - currentItem?: T -} + title: string + dataSource: T[] + // Some selector data sources only use ALL tab, so this can be null/undefined. + groupField?: string | null + groups: GroupConfig[] + itemRender: SelectorItemRender + searchable?: boolean + searchFields?: string[] + currentItem?: T | null +} \ No newline at end of file