mirror of
https://github.com/Powerful-517/yys-editor.git
synced 2026-03-05 15:05:27 +00:00
merge: resolve pull conflicts with origin/develop
This commit is contained in:
@@ -86,6 +86,8 @@ const filteredItems = (group: GroupConfig) => {
|
|||||||
if (group.name !== 'ALL') {
|
if (group.name !== 'ALL') {
|
||||||
if (group.filter) {
|
if (group.filter) {
|
||||||
items = items.filter(group.filter)
|
items = items.filter(group.filter)
|
||||||
|
} else if (!props.config.groupField) {
|
||||||
|
items = []
|
||||||
} else {
|
} else {
|
||||||
items = items.filter(item =>
|
items = items.filter(item =>
|
||||||
item[props.config.groupField]?.toLowerCase() === group.name.toLowerCase()
|
item[props.config.groupField]?.toLowerCase() === group.name.toLowerCase()
|
||||||
|
|||||||
@@ -47,7 +47,10 @@ const componentGroups = [
|
|||||||
type: 'textNode',
|
type: 'textNode',
|
||||||
description: '可编辑文本的节点',
|
description: '可编辑文本的节点',
|
||||||
data: {
|
data: {
|
||||||
text: '双击编辑文字',
|
text: {
|
||||||
|
content: '<p>请输入文本</p>',
|
||||||
|
rich: true
|
||||||
|
},
|
||||||
width: 200,
|
width: 200,
|
||||||
height: 120
|
height: 120
|
||||||
}
|
}
|
||||||
@@ -279,4 +282,4 @@ const handleMouseDown = (e, component) => {
|
|||||||
margin-bottom: 6px;
|
margin-bottom: 6px;
|
||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,30 +1,95 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// LogicFlow 会自动处理文本节点的渲染和编辑
|
import { computed, ref } from 'vue';
|
||||||
|
import { useNodeAppearance } from '@/ts/useNodeAppearance';
|
||||||
|
import '@vueup/vue-quill/dist/vue-quill.snow.css';
|
||||||
|
|
||||||
|
const DEFAULT_HTML = '<p>请输入文本</p>';
|
||||||
|
|
||||||
|
const richHtml = ref(DEFAULT_HTML);
|
||||||
|
|
||||||
|
const escapeHtml = (value: string) =>
|
||||||
|
value
|
||||||
|
.replaceAll('&', '&')
|
||||||
|
.replaceAll('<', '<')
|
||||||
|
.replaceAll('>', '>')
|
||||||
|
.replaceAll('"', '"')
|
||||||
|
.replaceAll("'", ''');
|
||||||
|
|
||||||
|
const normalizeTextHtml = (rawText: any): string => {
|
||||||
|
if (typeof rawText === 'string') {
|
||||||
|
const trimmed = rawText.trim();
|
||||||
|
if (!trimmed) return DEFAULT_HTML;
|
||||||
|
return trimmed.startsWith('<') ? trimmed : `<p>${escapeHtml(rawText)}</p>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rawText && typeof rawText === 'object') {
|
||||||
|
const content = typeof rawText.content === 'string' ? rawText.content : '';
|
||||||
|
if (!content.trim()) return DEFAULT_HTML;
|
||||||
|
return rawText.rich === false ? `<p>${escapeHtml(content)}</p>` : content;
|
||||||
|
}
|
||||||
|
|
||||||
|
return DEFAULT_HTML;
|
||||||
|
};
|
||||||
|
|
||||||
|
const { containerStyle, textStyle } = useNodeAppearance({
|
||||||
|
onPropsChange(props) {
|
||||||
|
richHtml.value = normalizeTextHtml(props?.text);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const DEFAULT_BG = '#ffffff';
|
||||||
|
const DEFAULT_BORDER = '#dcdfe6';
|
||||||
|
const DEFAULT_SHADOW = '0px 2px 4px rgba(0,0,0,0.1)';
|
||||||
|
|
||||||
|
const displayContainerStyle = computed(() => {
|
||||||
|
const style: Record<string, any> = { ...(containerStyle.value || {}) };
|
||||||
|
const background = String(style.background ?? '').toLowerCase();
|
||||||
|
const borderColor = String(style.borderColor ?? '').toLowerCase();
|
||||||
|
const boxShadow = String(style.boxShadow ?? '').replaceAll(/\s+/g, '');
|
||||||
|
|
||||||
|
// Keep user-customized style, but make legacy/default text-node chrome transparent.
|
||||||
|
if (!background || background === DEFAULT_BG || background === 'rgb(255,255,255)') {
|
||||||
|
style.background = 'transparent';
|
||||||
|
}
|
||||||
|
if (!borderColor || borderColor === DEFAULT_BORDER || borderColor === 'rgb(220,223,230)') {
|
||||||
|
style.borderColor = 'transparent';
|
||||||
|
}
|
||||||
|
if (!boxShadow || boxShadow === DEFAULT_SHADOW.replaceAll(/\s+/g, '')) {
|
||||||
|
style.boxShadow = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
return style;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="text-content">
|
<div class="text-node" :style="displayContainerStyle">
|
||||||
<!-- LogicFlow 会自动渲染 text 内容 -->
|
<div class="text-content ql-editor" :style="textStyle" v-html="richHtml"></div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.text-node {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.text-content {
|
.text-content {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
text-align: center;
|
|
||||||
outline: none;
|
|
||||||
word-break: break-word;
|
|
||||||
padding: 4px;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
cursor: text;
|
padding: 8px;
|
||||||
|
overflow: auto;
|
||||||
|
word-break: break-word;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-content:focus {
|
.text-content :deep(p) {
|
||||||
background: rgba(64, 158, 255, 0.1);
|
margin: 0 0 0.5em;
|
||||||
border-radius: 2px;
|
}
|
||||||
|
|
||||||
|
.text-content :deep(p:last-child) {
|
||||||
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,131 +1,74 @@
|
|||||||
import { HtmlNodeModel } from '@logicflow/core';
|
import { HtmlNodeModel } from '@logicflow/core';
|
||||||
|
|
||||||
|
const DEFAULT_TEXT_HTML = '<p>请输入文本</p>';
|
||||||
|
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 {
|
class TextNodeModel extends HtmlNodeModel {
|
||||||
initNodeData(data: any) {
|
initNodeData(data: any) {
|
||||||
super.initNodeData(data);
|
super.initNodeData(data);
|
||||||
|
|
||||||
// 从 data 中读取宽高,支持调整大小后的持久化
|
this.width = parseSize(data?.properties?.width, 200);
|
||||||
if (data.properties?.width) {
|
this.height = parseSize(data?.properties?.height, 120);
|
||||||
this.width = data.properties.width;
|
|
||||||
} else {
|
|
||||||
this.width = 200;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.properties?.height) {
|
this.setProperty('width', this.width);
|
||||||
this.height = data.properties.height;
|
this.setProperty('height', this.height);
|
||||||
} else {
|
this.setProperty('text', normalizeTextProperty(data?.properties?.text));
|
||||||
this.height = 120;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算 Label 宽度
|
const hasStyle = Object.prototype.hasOwnProperty.call(data?.properties || {}, 'style');
|
||||||
const labelWidth = this.width - 20;
|
if (!hasStyle) {
|
||||||
|
this.setProperty('style', DEFAULT_TEXT_STYLE);
|
||||||
// 初始化或更新 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,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setAttributes() {
|
setAttributes() {
|
||||||
// 设置默认尺寸(如果 initNodeData 中没有设置)
|
if (!this.width) this.width = 200;
|
||||||
if (!this.width) {
|
if (!this.height) this.height = 120;
|
||||||
this.width = 200;
|
|
||||||
}
|
|
||||||
if (!this.height) {
|
|
||||||
this.height = 120;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 监听节点大小变化,更新 Label 宽度
|
|
||||||
resize(deltaX: number, deltaY: number) {
|
resize(deltaX: number, deltaY: number) {
|
||||||
const result = super.resize?.(deltaX, deltaY);
|
const result = super.resize?.(deltaX, deltaY);
|
||||||
|
|
||||||
// 持久化宽高到 properties
|
|
||||||
this.setProperty('width', this.width);
|
this.setProperty('width', this.width);
|
||||||
this.setProperty('height', this.height);
|
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;
|
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;
|
export default TextNodeModel;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { computed } from 'vue';
|
|||||||
import { useDialogs } from '@/ts/useDialogs';
|
import { useDialogs } from '@/ts/useDialogs';
|
||||||
import { getLogicFlowInstance } from '@/ts/useLogicFlow';
|
import { getLogicFlowInstance } from '@/ts/useLogicFlow';
|
||||||
import { SELECTOR_PRESETS } from '@/configs/selectorPresets';
|
import { SELECTOR_PRESETS } from '@/configs/selectorPresets';
|
||||||
|
import type { SelectorConfig } from '@/types/selector';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
node: any;
|
node: any;
|
||||||
@@ -10,30 +11,29 @@ const props = defineProps<{
|
|||||||
|
|
||||||
const { openGenericSelector } = useDialogs();
|
const { openGenericSelector } = useDialogs();
|
||||||
|
|
||||||
// 当前选中的资产库
|
|
||||||
const currentLibrary = computed(() => props.node.properties?.assetLibrary || 'shikigami');
|
const currentLibrary = computed(() => props.node.properties?.assetLibrary || 'shikigami');
|
||||||
|
|
||||||
// 当前选中的资产
|
|
||||||
const currentAsset = computed(() => {
|
const currentAsset = computed(() => {
|
||||||
return props.node.properties?.selectedAsset || { name: '未选择' };
|
return props.node.properties?.selectedAsset || { name: '未选择' };
|
||||||
});
|
});
|
||||||
|
|
||||||
// 打开选择器
|
|
||||||
const handleOpenSelector = () => {
|
const handleOpenSelector = () => {
|
||||||
const lf = getLogicFlowInstance();
|
const lf = getLogicFlowInstance();
|
||||||
const node = props.node;
|
const node = props.node;
|
||||||
if (!lf || !node) return;
|
if (!lf || !node) return;
|
||||||
|
|
||||||
const library = currentLibrary.value;
|
const library = currentLibrary.value;
|
||||||
const config = SELECTOR_PRESETS[library];
|
const preset = SELECTOR_PRESETS[library];
|
||||||
|
|
||||||
if (!config) {
|
if (!preset) {
|
||||||
console.error('未找到资产库配置:', library);
|
console.error('未找到资产库配置:', library);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置当前选中项
|
const config: SelectorConfig = {
|
||||||
config.currentItem = node.properties?.selectedAsset;
|
...preset,
|
||||||
|
currentItem: node.properties?.selectedAsset || null
|
||||||
|
};
|
||||||
|
|
||||||
openGenericSelector(config, (selectedItem) => {
|
openGenericSelector(config, (selectedItem) => {
|
||||||
lf.setProperties(node.id, {
|
lf.setProperties(node.id, {
|
||||||
@@ -80,4 +80,4 @@ const handleOpenSelector = () => {
|
|||||||
color: #606266;
|
color: #606266;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,22 +1,121 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const props = defineProps<{
|
import { ref, watch } from 'vue';
|
||||||
node: any;
|
import { QuillEditor } from '@vueup/vue-quill';
|
||||||
}>();
|
import '@vueup/vue-quill/dist/vue-quill.snow.css';
|
||||||
|
import { getLogicFlowInstance } from '@/ts/useLogicFlow';
|
||||||
|
|
||||||
|
const props = defineProps<{ node: any }>();
|
||||||
|
|
||||||
|
const DEFAULT_HTML = '<p>请输入文本</p>';
|
||||||
|
|
||||||
|
const editorHtml = ref(DEFAULT_HTML);
|
||||||
|
|
||||||
|
const toolbarOptions = [
|
||||||
|
[{ header: [1, 2, 3, false] }],
|
||||||
|
['bold', 'italic', 'underline', 'strike'],
|
||||||
|
[{ color: [] }, { background: [] }],
|
||||||
|
[{ list: 'ordered' }, { list: 'bullet' }],
|
||||||
|
[{ align: [] }],
|
||||||
|
['clean']
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const escapeHtml = (value: string) =>
|
||||||
|
value
|
||||||
|
.replaceAll('&', '&')
|
||||||
|
.replaceAll('<', '<')
|
||||||
|
.replaceAll('>', '>')
|
||||||
|
.replaceAll('"', '"')
|
||||||
|
.replaceAll("'", ''');
|
||||||
|
|
||||||
|
const normalizeTextHtml = (rawText: any): string => {
|
||||||
|
if (typeof rawText === 'string') {
|
||||||
|
const trimmed = rawText.trim();
|
||||||
|
if (!trimmed) return DEFAULT_HTML;
|
||||||
|
return trimmed.startsWith('<') ? trimmed : `<p>${escapeHtml(rawText)}</p>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rawText && typeof rawText === 'object') {
|
||||||
|
const content = typeof rawText.content === 'string' ? rawText.content : '';
|
||||||
|
if (!content.trim()) return DEFAULT_HTML;
|
||||||
|
return rawText.rich === false ? `<p>${escapeHtml(content)}</p>` : content;
|
||||||
|
}
|
||||||
|
|
||||||
|
return DEFAULT_HTML;
|
||||||
|
};
|
||||||
|
|
||||||
|
const syncEditorFromNode = (node?: any) => {
|
||||||
|
const next = normalizeTextHtml(node?.properties?.text);
|
||||||
|
if (next !== editorHtml.value) {
|
||||||
|
editorHtml.value = next;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.node,
|
||||||
|
(node) => {
|
||||||
|
syncEditorFromNode(node);
|
||||||
|
},
|
||||||
|
{ immediate: true, deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleContentChange = (value: string) => {
|
||||||
|
const lf = getLogicFlowInstance();
|
||||||
|
const node = props.node;
|
||||||
|
if (!lf || !node) return;
|
||||||
|
|
||||||
|
const nextHtml = value?.trim() ? value : DEFAULT_HTML;
|
||||||
|
editorHtml.value = nextHtml;
|
||||||
|
|
||||||
|
const current = normalizeTextHtml(node?.properties?.text);
|
||||||
|
if (current === nextHtml) return;
|
||||||
|
|
||||||
|
lf.setProperties(node.id, {
|
||||||
|
...(node.properties || {}),
|
||||||
|
text: {
|
||||||
|
content: nextHtml,
|
||||||
|
rich: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="property-section">
|
<div class="property-section">
|
||||||
<div class="section-header">文本节点</div>
|
<div class="section-header">文本节点</div>
|
||||||
|
|
||||||
<div class="property-item">
|
<div class="property-item">
|
||||||
<label class="property-label">内容</label>
|
<label class="property-label">内容</label>
|
||||||
<div class="property-value">
|
<div class="editor-wrapper">
|
||||||
{{ props.node?.properties?.text || '未设置' }}
|
<QuillEditor
|
||||||
</div>
|
:content="editorHtml"
|
||||||
</div>
|
contentType="html"
|
||||||
<div class="property-item">
|
theme="snow"
|
||||||
<div class="property-note">
|
:toolbar="toolbarOptions"
|
||||||
💡 提示:双击画布中的节点即可编辑文字
|
@update:content="handleContentChange"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.editor-wrapper {
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-wrapper :deep(.ql-toolbar.ql-snow) {
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1px solid #ebeef5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-wrapper :deep(.ql-container.ql-snow) {
|
||||||
|
border: none;
|
||||||
|
min-height: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-wrapper :deep(.ql-editor) {
|
||||||
|
min-height: 180px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* 定义各种资产类型的选择器配置
|
* 定义各种资产类型的选择器配置
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { SelectorConfig } from '@/types/selector'
|
import type { GroupConfig, SelectorConfig } from '@/types/selector'
|
||||||
import shikigamiData from '@/data/Shikigami.json'
|
import shikigamiData from '@/data/Shikigami.json'
|
||||||
import yuhunData from '@/data/Yuhun.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: '11', name: '神乐', avatar: '/assets/downloaded_images/hero_11_11.png' },
|
||||||
{ id: '12', name: '八百比丘尼', avatar: '/assets/downloaded_images/hero_12_12.png' },
|
{ id: '12', name: '八百比丘尼', avatar: '/assets/downloaded_images/hero_12_12.png' },
|
||||||
{ id: '13', name: '源博雅', avatar: '/assets/downloaded_images/hero_13_13.png' },
|
{ id: '13', name: '源博雅', avatar: '/assets/downloaded_images/hero_13_13.png' },
|
||||||
{ id: '15', name: '不知火', avatar: '/assets/downloaded_images/hero_15_15.png' },
|
{ id: '15', name: '源赖光', avatar: '/assets/downloaded_images/hero_15_15.png' },
|
||||||
{ id: '16', name: '鬼灯', avatar: '/assets/downloaded_images/hero_16_16.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: '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: '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: '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: '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: '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: '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: '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: '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: '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: '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: '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: '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: '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: '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: '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: '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: '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: '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<Record<string, any>>,
|
||||||
|
field: string,
|
||||||
|
options?: {
|
||||||
|
order?: string[]
|
||||||
|
labelMap?: Record<string, string>
|
||||||
|
}
|
||||||
|
): GroupConfig[] => {
|
||||||
|
const raw = new Set<string>()
|
||||||
|
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<string, SelectorConfig> = {
|
export const SELECTOR_PRESETS: Record<string, SelectorConfig> = {
|
||||||
@@ -85,17 +149,7 @@ export const SELECTOR_PRESETS: Record<string, SelectorConfig> = {
|
|||||||
title: '请选择式神',
|
title: '请选择式神',
|
||||||
dataSource: shikigamiData,
|
dataSource: shikigamiData,
|
||||||
groupField: 'rarity',
|
groupField: 'rarity',
|
||||||
groups: [
|
groups: shikigamiGroups,
|
||||||
{ 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' }
|
|
||||||
],
|
|
||||||
itemRender: {
|
itemRender: {
|
||||||
imageField: 'avatar',
|
imageField: 'avatar',
|
||||||
labelField: 'name'
|
labelField: 'name'
|
||||||
@@ -106,16 +160,7 @@ export const SELECTOR_PRESETS: Record<string, SelectorConfig> = {
|
|||||||
title: '请选择御魂',
|
title: '请选择御魂',
|
||||||
dataSource: yuhunData,
|
dataSource: yuhunData,
|
||||||
groupField: 'type',
|
groupField: 'type',
|
||||||
groups: [
|
groups: yuhunGroups,
|
||||||
{ label: '全部', name: 'ALL' },
|
|
||||||
{ label: '攻击类', name: 'attack' },
|
|
||||||
{ label: '暴击类', name: 'Crit' },
|
|
||||||
{ label: '生命类', name: 'Health' },
|
|
||||||
{ label: '防御类', name: 'Defense' },
|
|
||||||
{ label: '效果命中', name: 'Effect' },
|
|
||||||
{ label: '效果抵抗', name: 'EffectResist' },
|
|
||||||
{ label: '特殊类', name: 'Special' }
|
|
||||||
],
|
|
||||||
itemRender: {
|
itemRender: {
|
||||||
imageField: 'avatar',
|
imageField: 'avatar',
|
||||||
labelField: 'name'
|
labelField: 'name'
|
||||||
@@ -126,9 +171,7 @@ export const SELECTOR_PRESETS: Record<string, SelectorConfig> = {
|
|||||||
title: '请选择阴阳师',
|
title: '请选择阴阳师',
|
||||||
dataSource: onmyojiData,
|
dataSource: onmyojiData,
|
||||||
groupField: null,
|
groupField: null,
|
||||||
groups: [
|
groups: [ALL_GROUP],
|
||||||
{ label: '全部', name: 'ALL' }
|
|
||||||
],
|
|
||||||
itemRender: {
|
itemRender: {
|
||||||
imageField: 'avatar',
|
imageField: 'avatar',
|
||||||
labelField: 'name'
|
labelField: 'name'
|
||||||
@@ -139,15 +182,7 @@ export const SELECTOR_PRESETS: Record<string, SelectorConfig> = {
|
|||||||
title: '请选择阴阳师技能',
|
title: '请选择阴阳师技能',
|
||||||
dataSource: onmyojiSkillData,
|
dataSource: onmyojiSkillData,
|
||||||
groupField: 'onmyojiName',
|
groupField: 'onmyojiName',
|
||||||
groups: [
|
groups: onmyojiSkillGroups,
|
||||||
{ label: '全部', name: 'ALL' },
|
|
||||||
{ label: '晴明', name: '晴明' },
|
|
||||||
{ label: '神乐', name: '神乐' },
|
|
||||||
{ label: '八百比丘尼', name: '八百比丘尼' },
|
|
||||||
{ label: '源博雅', name: '源博雅' },
|
|
||||||
{ label: '不知火', name: '不知火' },
|
|
||||||
{ label: '鬼灯', name: '鬼灯' }
|
|
||||||
],
|
|
||||||
itemRender: {
|
itemRender: {
|
||||||
imageField: 'avatar',
|
imageField: 'avatar',
|
||||||
labelField: 'name'
|
labelField: 'name'
|
||||||
|
|||||||
@@ -1,34 +1,23 @@
|
|||||||
/**
|
export interface GroupConfig<T = any> {
|
||||||
* 通用选择器配置接口
|
label: string
|
||||||
* 用于配置驱动的图片选择器组件
|
name: string
|
||||||
*/
|
filter?: (item: T) => boolean
|
||||||
|
}
|
||||||
|
|
||||||
export interface GroupConfig {
|
export interface SelectorItemRender {
|
||||||
label: string // Tab显示标签
|
imageField: string
|
||||||
name: string // Tab标识符
|
labelField: string
|
||||||
filter?: (item: any) => boolean // 自定义过滤函数(可选)
|
imageSize?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SelectorConfig<T = any> {
|
export interface SelectorConfig<T = any> {
|
||||||
// 基础配置
|
title: string
|
||||||
title: string // 对话框标题
|
dataSource: T[]
|
||||||
dataSource: T[] // 数据源
|
// Some selector data sources only use ALL tab, so this can be null/undefined.
|
||||||
groupField: string // 分组字段名 (如 'rarity', 'type')
|
groupField?: string | null
|
||||||
|
groups: GroupConfig<T>[]
|
||||||
// 分组配置
|
itemRender: SelectorItemRender
|
||||||
groups: GroupConfig[] // Tab分组配置
|
searchable?: boolean
|
||||||
|
searchFields?: string[]
|
||||||
// 展示配置
|
currentItem?: T | null
|
||||||
itemRender: {
|
}
|
||||||
imageField: string // 图片字段名 (如 'avatar')
|
|
||||||
labelField: string // 标签字段名 (如 'name')
|
|
||||||
imageSize?: number // 图片尺寸,默认100px
|
|
||||||
}
|
|
||||||
|
|
||||||
// 搜索配置
|
|
||||||
searchable?: boolean // 是否启用搜索,默认true
|
|
||||||
searchFields?: string[] // 搜索字段,默认使用labelField
|
|
||||||
|
|
||||||
// 当前选中项
|
|
||||||
currentItem?: T
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user