feat: add useSchemaSync, PropertyPanel, useStencil, ComponentPanel
This commit is contained in:
parent
f91bc84911
commit
69ab0a547d
@ -0,0 +1,44 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { Ref } from 'vue';
|
||||||
|
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { Input, Spin } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { useStencil } from '../composables/useStencil';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
stencilContainer: Ref<HTMLElement | null>;
|
||||||
|
graph: Ref<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
const searchText = ref('');
|
||||||
|
const stencilDiv = ref<HTMLElement | null>(null);
|
||||||
|
|
||||||
|
const { loading, initStencil } = useStencil(stencilDiv, props.graph);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.graph,
|
||||||
|
(g) => {
|
||||||
|
if (g) initStencil();
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex h-full w-60 flex-col border-r bg-gray-50">
|
||||||
|
<div class="border-b p-3">
|
||||||
|
<Input
|
||||||
|
v-model:value="searchText"
|
||||||
|
placeholder="搜索组件..."
|
||||||
|
size="small"
|
||||||
|
allow-clear
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Spin :spinning="loading">
|
||||||
|
<div ref="stencilDiv" class="flex-1 overflow-auto"></div>
|
||||||
|
</Spin>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,286 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, reactive, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Col,
|
||||||
|
Divider,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
InputNumber,
|
||||||
|
message,
|
||||||
|
Row,
|
||||||
|
Select,
|
||||||
|
Switch,
|
||||||
|
} from 'ant-design-vue';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
nodeId: null | string;
|
||||||
|
nodeData: null | Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update', nodeId: string, data: Record<string, any>): void;
|
||||||
|
(e: 'delete', nodeId: string): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const formState = reactive({
|
||||||
|
fieldKey: '',
|
||||||
|
title: '',
|
||||||
|
placeholder: '',
|
||||||
|
required: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const validationState = reactive({
|
||||||
|
minLength: undefined as number | undefined,
|
||||||
|
maxLength: undefined as number | undefined,
|
||||||
|
pattern: '',
|
||||||
|
customMessage: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const dataSourceType = ref<'remote' | 'static'>('static');
|
||||||
|
const staticOptions = reactive<{ label: string; value: string }[]>([]);
|
||||||
|
const remoteConfig = reactive({
|
||||||
|
url: '',
|
||||||
|
method: 'GET',
|
||||||
|
labelField: 'name',
|
||||||
|
valueField: 'id',
|
||||||
|
});
|
||||||
|
|
||||||
|
const supportedProps = computed(() => {
|
||||||
|
if (!props.nodeData?.supportedProps) return null;
|
||||||
|
try {
|
||||||
|
return JSON.parse(props.nodeData.supportedProps);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasDataSource = computed(
|
||||||
|
() => supportedProps.value?.dataSource?.supported === true,
|
||||||
|
);
|
||||||
|
const hasValidation = computed(
|
||||||
|
() =>
|
||||||
|
Array.isArray(supportedProps.value?.validation) &&
|
||||||
|
supportedProps.value.validation.length > 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.nodeData,
|
||||||
|
(data) => {
|
||||||
|
if (!data) return;
|
||||||
|
formState.fieldKey = data.fieldKey ?? '';
|
||||||
|
formState.title = data.title ?? '';
|
||||||
|
formState.required = data.required ?? false;
|
||||||
|
const schemaNode = data.schemaNode ?? {};
|
||||||
|
const compProps = schemaNode['x-component-props'] ?? {};
|
||||||
|
formState.placeholder = compProps.placeholder ?? '';
|
||||||
|
const ds = schemaNode['x-data-source'];
|
||||||
|
if (ds) {
|
||||||
|
dataSourceType.value = ds.type ?? 'static';
|
||||||
|
if (ds.type === 'static' && Array.isArray(ds.options)) {
|
||||||
|
staticOptions.splice(0, staticOptions.length, ...ds.options);
|
||||||
|
} else if (ds.type === 'remote') {
|
||||||
|
Object.assign(remoteConfig, {
|
||||||
|
url: ds.url ?? '',
|
||||||
|
method: ds.method ?? 'GET',
|
||||||
|
labelField: ds.labelField ?? 'name',
|
||||||
|
valueField: ds.valueField ?? 'id',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
staticOptions.splice(0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true, deep: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
function handleSave() {
|
||||||
|
if (!props.nodeId || !props.nodeData) return;
|
||||||
|
const schemaNode = {
|
||||||
|
...props.nodeData.schemaNode,
|
||||||
|
title: formState.title,
|
||||||
|
required: formState.required,
|
||||||
|
};
|
||||||
|
if (formState.placeholder) {
|
||||||
|
schemaNode['x-component-props'] = {
|
||||||
|
...schemaNode['x-component-props'],
|
||||||
|
placeholder: formState.placeholder,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (hasDataSource.value) {
|
||||||
|
schemaNode['x-data-source'] =
|
||||||
|
dataSourceType.value === 'static'
|
||||||
|
? {
|
||||||
|
type: 'static',
|
||||||
|
options: [...staticOptions],
|
||||||
|
}
|
||||||
|
: { type: 'remote', ...remoteConfig };
|
||||||
|
}
|
||||||
|
const newData = {
|
||||||
|
...props.nodeData,
|
||||||
|
fieldKey: formState.fieldKey,
|
||||||
|
title: formState.title,
|
||||||
|
required: formState.required,
|
||||||
|
schemaNode,
|
||||||
|
};
|
||||||
|
emit('update', props.nodeId, newData);
|
||||||
|
message.success('属性已更新');
|
||||||
|
}
|
||||||
|
|
||||||
|
function addOption() {
|
||||||
|
staticOptions.push({ label: '', value: '' });
|
||||||
|
}
|
||||||
|
function removeOption(index: number) {
|
||||||
|
staticOptions.splice(index, 1);
|
||||||
|
}
|
||||||
|
function handleDelete() {
|
||||||
|
if (props.nodeId) emit('delete', props.nodeId);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="nodeId && nodeData"
|
||||||
|
class="flex h-full w-80 flex-col border-l bg-white"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between border-b px-4 py-3">
|
||||||
|
<span class="text-sm font-semibold">属性配置</span>
|
||||||
|
<Button size="small" danger @click="handleDelete">删除</Button>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 overflow-auto p-4">
|
||||||
|
<div class="mb-2 text-xs font-medium text-gray-500">基础属性</div>
|
||||||
|
<Form layout="vertical" size="small">
|
||||||
|
<Form.Item label="组件ID (字段名)" required>
|
||||||
|
<Input
|
||||||
|
v-model:value="formState.fieldKey"
|
||||||
|
placeholder="如: leaveType"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="标题" required>
|
||||||
|
<Input v-model:value="formState.title" placeholder="如: 请假类型" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
v-if="
|
||||||
|
supportedProps?.basic?.some((p: any) => p.key === 'placeholder')
|
||||||
|
"
|
||||||
|
label="提示文字"
|
||||||
|
>
|
||||||
|
<Input v-model:value="formState.placeholder" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="必填">
|
||||||
|
<Switch v-model:checked="formState.required" />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
|
||||||
|
<template v-if="hasValidation">
|
||||||
|
<Divider style="margin: 12px 0" />
|
||||||
|
<div class="mb-2 text-xs font-medium text-gray-500">校验规则</div>
|
||||||
|
<Form layout="vertical" size="small">
|
||||||
|
<Row :gutter="8">
|
||||||
|
<Col
|
||||||
|
v-if="
|
||||||
|
supportedProps?.validation?.some(
|
||||||
|
(p: any) => p.key === 'minLength',
|
||||||
|
)
|
||||||
|
"
|
||||||
|
:span="12"
|
||||||
|
>
|
||||||
|
<Form.Item label="最小长度">
|
||||||
|
<InputNumber
|
||||||
|
v-model:value="validationState.minLength"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col
|
||||||
|
v-if="
|
||||||
|
supportedProps?.validation?.some(
|
||||||
|
(p: any) => p.key === 'maxLength',
|
||||||
|
)
|
||||||
|
"
|
||||||
|
:span="12"
|
||||||
|
>
|
||||||
|
<Form.Item label="最大长度">
|
||||||
|
<InputNumber
|
||||||
|
v-model:value="validationState.maxLength"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Form.Item
|
||||||
|
v-if="
|
||||||
|
supportedProps?.validation?.some((p: any) => p.key === 'pattern')
|
||||||
|
"
|
||||||
|
label="正则"
|
||||||
|
>
|
||||||
|
<Input v-model:value="validationState.pattern" />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-if="hasDataSource">
|
||||||
|
<Divider style="margin: 12px 0" />
|
||||||
|
<div class="mb-2 text-xs font-medium text-gray-500">数据源</div>
|
||||||
|
<Form layout="vertical" size="small">
|
||||||
|
<Form.Item label="来源类型">
|
||||||
|
<Select v-model:value="dataSourceType">
|
||||||
|
<Select.Option value="static">静态选项</Select.Option>
|
||||||
|
<Select.Option value="remote">远程API</Select.Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
<template v-if="dataSourceType === 'static'">
|
||||||
|
<div
|
||||||
|
v-for="(opt, i) in staticOptions"
|
||||||
|
:key="i"
|
||||||
|
class="mb-2 flex gap-1"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
v-model:value="opt.label"
|
||||||
|
placeholder="Label"
|
||||||
|
class="flex-1"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
v-model:value="opt.value"
|
||||||
|
placeholder="Value"
|
||||||
|
class="flex-1"
|
||||||
|
/>
|
||||||
|
<Button size="small" danger @click="removeOption(i)">×</Button>
|
||||||
|
</div>
|
||||||
|
<Button size="small" type="dashed" block @click="addOption">
|
||||||
|
+ 添加选项
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
<template v-if="dataSourceType === 'remote'">
|
||||||
|
<Form.Item label="API URL">
|
||||||
|
<Input v-model:value="remoteConfig.url" placeholder="/api/xxx" />
|
||||||
|
</Form.Item>
|
||||||
|
<Row :gutter="8">
|
||||||
|
<Col :span="12">
|
||||||
|
<Form.Item label="Label字段">
|
||||||
|
<Input v-model:value="remoteConfig.labelField" />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col :span="12">
|
||||||
|
<Form.Item label="Value字段">
|
||||||
|
<Input v-model:value="remoteConfig.valueField" />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</template>
|
||||||
|
</Form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<Divider style="margin: 12px 0" />
|
||||||
|
<Button type="primary" block @click="handleSave">保存属性</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="flex h-full w-80 items-center justify-center border-l bg-gray-50 text-sm text-gray-400"
|
||||||
|
>
|
||||||
|
点击节点查看属性
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,168 @@
|
|||||||
|
import type { Graph } from '@antv/x6';
|
||||||
|
|
||||||
|
import type { Ref } from 'vue';
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { portConfig } from '../config/port-config';
|
||||||
|
|
||||||
|
export function useSchemaSync(
|
||||||
|
graph: Ref<Graph | null>,
|
||||||
|
initialSchema?: string,
|
||||||
|
) {
|
||||||
|
const schemaJson = ref(initialSchema ?? '{"type":"object","properties":{}}');
|
||||||
|
const nodeCount = ref(0);
|
||||||
|
const edgeCount = ref(0);
|
||||||
|
|
||||||
|
function schemaToCanvas() {
|
||||||
|
const g = graph.value;
|
||||||
|
if (!g) return;
|
||||||
|
|
||||||
|
g.clearCells();
|
||||||
|
|
||||||
|
let schema: { properties: Record<string, any>; type: string; };
|
||||||
|
try {
|
||||||
|
schema = JSON.parse(schemaJson.value);
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!schema.properties) return;
|
||||||
|
|
||||||
|
let yOffset = 40;
|
||||||
|
const xOffset = 80;
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(schema.properties)) {
|
||||||
|
const nodeData: Record<string, any> = {
|
||||||
|
fieldKey: key,
|
||||||
|
title: value.title ?? key,
|
||||||
|
componentType: value['x-component'] ?? 'Input',
|
||||||
|
required: value.required ?? false,
|
||||||
|
schemaNode: value,
|
||||||
|
};
|
||||||
|
|
||||||
|
g.addNode({
|
||||||
|
shape: 'form-field-node',
|
||||||
|
id: key,
|
||||||
|
x: xOffset,
|
||||||
|
y: yOffset,
|
||||||
|
data: nodeData,
|
||||||
|
ports: portConfig,
|
||||||
|
});
|
||||||
|
|
||||||
|
yOffset += 90;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recreate edges from x-reactions
|
||||||
|
for (const [sourceKey, value] of Object.entries(schema.properties)) {
|
||||||
|
const reactions = value['x-reactions'] as any[] | undefined;
|
||||||
|
if (!reactions) continue;
|
||||||
|
for (const reaction of reactions) {
|
||||||
|
const targetKey = reaction.target;
|
||||||
|
if (!targetKey) continue;
|
||||||
|
const type = reaction.type === 'data' ? 'data' : 'condition';
|
||||||
|
const color = type === 'data' ? '#52c41a' : '#1677ff';
|
||||||
|
const dasharray = type === 'data' ? '' : '5,5';
|
||||||
|
const label = type === 'data' ? '数据' : '条件';
|
||||||
|
try {
|
||||||
|
g.addEdge({
|
||||||
|
source: sourceKey,
|
||||||
|
target: targetKey,
|
||||||
|
attrs: {
|
||||||
|
line: {
|
||||||
|
stroke: color,
|
||||||
|
strokeWidth: 2,
|
||||||
|
strokeDasharray: dasharray,
|
||||||
|
targetMarker: { name: 'block', width: 12, height: 8 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
labels: [
|
||||||
|
{
|
||||||
|
position: 0.5,
|
||||||
|
attrs: {
|
||||||
|
label: { text: label, fill: color, fontSize: 12 },
|
||||||
|
rect: {
|
||||||
|
fill: '#fff',
|
||||||
|
stroke: color,
|
||||||
|
strokeWidth: 1,
|
||||||
|
rx: 4,
|
||||||
|
ry: 4,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
data: {
|
||||||
|
linkageType: type,
|
||||||
|
when: reaction.when,
|
||||||
|
expression: reaction.expression,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// edge target may not exist
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCounts();
|
||||||
|
}
|
||||||
|
|
||||||
|
function canvasToSchema(): string {
|
||||||
|
const g = graph.value;
|
||||||
|
if (!g) return '{}';
|
||||||
|
|
||||||
|
const properties: Record<string, any> = {};
|
||||||
|
|
||||||
|
g.getNodes().forEach((node) => {
|
||||||
|
const data = node.getData() ?? {};
|
||||||
|
const key = data.fieldKey || node.id;
|
||||||
|
const schemaNode = data.schemaNode ? { ...data.schemaNode } : {};
|
||||||
|
schemaNode.title = data.title;
|
||||||
|
schemaNode.required = data.required;
|
||||||
|
if (data.componentType) schemaNode['x-component'] = data.componentType;
|
||||||
|
properties[key] = schemaNode;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Build x-reactions from edges
|
||||||
|
const reactionMap = new Map<string, any[]>();
|
||||||
|
for (const edge of g.getEdges()) {
|
||||||
|
const sourceCell = edge.getSourceCell();
|
||||||
|
const targetCell = edge.getTargetCell();
|
||||||
|
if (!sourceCell || !targetCell) continue;
|
||||||
|
const data = edge.getData() ?? {};
|
||||||
|
const sourceKey = sourceCell.id;
|
||||||
|
if (!reactionMap.has(sourceKey)) reactionMap.set(sourceKey, []);
|
||||||
|
const reaction: any = {
|
||||||
|
type: data.linkageType ?? 'condition',
|
||||||
|
target: targetCell.id,
|
||||||
|
};
|
||||||
|
if (data.when) reaction.when = data.when;
|
||||||
|
if (data.expression) reaction.expression = data.expression;
|
||||||
|
if (data.action) reaction.action = data.action;
|
||||||
|
reactionMap.get(sourceKey)?.push(reaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [sourceKey, reactions] of reactionMap) {
|
||||||
|
if (properties[sourceKey]) {
|
||||||
|
properties[sourceKey]['x-reactions'] = reactions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify({ type: 'object', properties }, null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCounts() {
|
||||||
|
const g = graph.value;
|
||||||
|
if (!g) return;
|
||||||
|
nodeCount.value = g.getNodes().length;
|
||||||
|
edgeCount.value = g.getEdges().length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
schemaJson,
|
||||||
|
nodeCount,
|
||||||
|
edgeCount,
|
||||||
|
schemaToCanvas,
|
||||||
|
canvasToSchema,
|
||||||
|
updateCounts,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -0,0 +1,99 @@
|
|||||||
|
import type { Graph } from '@antv/x6';
|
||||||
|
|
||||||
|
import type { Ref } from 'vue';
|
||||||
|
|
||||||
|
import type { FormComponentDto } from '#/api/core';
|
||||||
|
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import { Stencil } from '@antv/x6-plugin-stencil';
|
||||||
|
|
||||||
|
import { getFormComponentsApi } from '#/api/core';
|
||||||
|
|
||||||
|
import { NODE_HEIGHT, NODE_WIDTH } from '../config/graph-config';
|
||||||
|
import { portConfig } from '../config/port-config';
|
||||||
|
|
||||||
|
export function useStencil(
|
||||||
|
stencilContainerRef: Ref<HTMLElement | null>,
|
||||||
|
graph: Ref<Graph | null>,
|
||||||
|
) {
|
||||||
|
const components = ref<FormComponentDto[]>([]);
|
||||||
|
const categories = ref<string[]>([]);
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
async function loadComponents() {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const res = await getFormComponentsApi({ isActive: true });
|
||||||
|
components.value = res ?? [];
|
||||||
|
categories.value = [...new Set(res.map((c) => c.category))];
|
||||||
|
} catch {
|
||||||
|
components.value = [];
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initStencil() {
|
||||||
|
if (!stencilContainerRef.value || !graph.value) return;
|
||||||
|
|
||||||
|
const g = graph.value;
|
||||||
|
const groups = categories.value.map((cat) => ({
|
||||||
|
name: cat,
|
||||||
|
title: cat,
|
||||||
|
collapsable: true,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const stencil = new Stencil({
|
||||||
|
title: '表单组件',
|
||||||
|
target: g,
|
||||||
|
groups,
|
||||||
|
search: (cell: any, keyword: string) => {
|
||||||
|
if (!keyword) return true;
|
||||||
|
const data = cell.getData?.();
|
||||||
|
return data?.displayName?.includes(keyword) ?? false;
|
||||||
|
},
|
||||||
|
stencilGraphWidth: 220,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const category of categories.value) {
|
||||||
|
const catComponents = components.value.filter(
|
||||||
|
(c) => c.category === category,
|
||||||
|
);
|
||||||
|
const nodes = catComponents.map((comp) => {
|
||||||
|
let schema: Record<string, any> = {};
|
||||||
|
try {
|
||||||
|
schema = JSON.parse(comp.defaultSchema);
|
||||||
|
} catch {
|
||||||
|
/* empty */
|
||||||
|
}
|
||||||
|
return g.createNode({
|
||||||
|
shape: 'form-field-node',
|
||||||
|
width: NODE_WIDTH,
|
||||||
|
height: NODE_HEIGHT,
|
||||||
|
data: {
|
||||||
|
componentId: comp.id,
|
||||||
|
componentType: comp.name,
|
||||||
|
displayName: comp.displayName,
|
||||||
|
icon: comp.icon,
|
||||||
|
fieldKey: '',
|
||||||
|
title: schema.title ?? '未命名',
|
||||||
|
required: schema.required ?? false,
|
||||||
|
schemaNode: schema,
|
||||||
|
supportedProps: comp.supportedProps,
|
||||||
|
},
|
||||||
|
ports: portConfig,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
stencil.load(nodes, category);
|
||||||
|
}
|
||||||
|
|
||||||
|
stencilContainerRef.value.append(stencil.container);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await loadComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
return { components, categories, loading, initStencil, loadComponents };
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user