feat: 新增tmagic-form-runtime
This commit is contained in:
25
runtime/tmagic-form/src/App.vue
Normal file
25
runtime/tmagic-form/src/App.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<MForm ref="mForm" :id="config?.id" :data-magic-id="config?.id" :config="formConfig" :init-values="values"></MForm>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { watch } from 'vue';
|
||||
|
||||
import { MForm } from '@tmagic/form';
|
||||
import type StageCore from '@tmagic/stage';
|
||||
|
||||
import { useFormConfig } from './useFormConfig';
|
||||
|
||||
const props = defineProps<{
|
||||
stage: StageCore;
|
||||
}>();
|
||||
|
||||
const { mForm, formConfig, config, values } = useFormConfig(props.stage.renderer.contentWindow);
|
||||
|
||||
watch(formConfig, async () => {
|
||||
setTimeout(() => {
|
||||
const page = props.stage.renderer.getDocument()?.querySelector<HTMLElement>('.m-form');
|
||||
page && props.stage.renderer.contentWindow?.magic.onPageElUpdate(page);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
161
runtime/tmagic-form/src/component-group-list.ts
Normal file
161
runtime/tmagic-form/src/component-group-list.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import type { ComponentGroup } from '@tmagic/editor';
|
||||
|
||||
export const COMPONENT_GROUP_LIST: ComponentGroup[] = [
|
||||
{
|
||||
title: '容器',
|
||||
items: [
|
||||
{
|
||||
text: '普通容器',
|
||||
type: 'container',
|
||||
data: {
|
||||
items: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
text: '表格',
|
||||
type: 'table',
|
||||
data: {
|
||||
items: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
text: '组列表',
|
||||
type: 'group-list',
|
||||
data: {
|
||||
items: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
text: '面板',
|
||||
type: 'panel',
|
||||
data: {
|
||||
items: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
text: '行',
|
||||
type: 'row',
|
||||
data: {
|
||||
items: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '表单组件',
|
||||
items: [
|
||||
{
|
||||
text: '输入框',
|
||||
type: 'text',
|
||||
data: {
|
||||
text: '输入框',
|
||||
name: 'text',
|
||||
},
|
||||
},
|
||||
{
|
||||
text: '数字输入框',
|
||||
type: 'number',
|
||||
data: {
|
||||
text: '数字输入框',
|
||||
name: 'number',
|
||||
},
|
||||
},
|
||||
{
|
||||
text: '文本域',
|
||||
type: 'textarea',
|
||||
data: {
|
||||
text: '文本域',
|
||||
name: 'textarea',
|
||||
},
|
||||
},
|
||||
{
|
||||
text: '链接',
|
||||
type: 'link',
|
||||
data: {
|
||||
text: '链接',
|
||||
name: 'link',
|
||||
},
|
||||
},
|
||||
{
|
||||
text: '日期',
|
||||
type: 'datetime',
|
||||
data: {
|
||||
text: '日期',
|
||||
name: 'datetime',
|
||||
},
|
||||
},
|
||||
{
|
||||
text: '时间',
|
||||
type: 'time',
|
||||
data: {
|
||||
text: '时间',
|
||||
name: 'time',
|
||||
},
|
||||
},
|
||||
{
|
||||
text: '选中器',
|
||||
type: 'select',
|
||||
data: {
|
||||
text: '选中器',
|
||||
name: 'select',
|
||||
},
|
||||
},
|
||||
{
|
||||
text: '级联选择器',
|
||||
type: 'cascader',
|
||||
data: {
|
||||
text: '级联选择器',
|
||||
name: 'cascader',
|
||||
},
|
||||
},
|
||||
{
|
||||
text: '开关',
|
||||
type: 'switch',
|
||||
data: {
|
||||
text: '开关',
|
||||
name: 'switch',
|
||||
},
|
||||
},
|
||||
{
|
||||
text: '多选框',
|
||||
type: 'checkbox',
|
||||
data: {
|
||||
text: '多选框',
|
||||
name: 'checkbox',
|
||||
},
|
||||
},
|
||||
{
|
||||
text: '多选组',
|
||||
type: 'checkboxGroup',
|
||||
data: {
|
||||
text: '多选组',
|
||||
name: 'checkboxGroup',
|
||||
},
|
||||
},
|
||||
{
|
||||
text: '单选框',
|
||||
type: 'radio',
|
||||
data: {
|
||||
text: '单选框',
|
||||
name: 'radio',
|
||||
},
|
||||
},
|
||||
{
|
||||
text: '单选组',
|
||||
type: 'radioGroup',
|
||||
data: {
|
||||
text: '单选组',
|
||||
name: 'radioGroup',
|
||||
},
|
||||
},
|
||||
{
|
||||
text: '取色器',
|
||||
type: 'colorPicker',
|
||||
data: {
|
||||
text: '取色器',
|
||||
name: 'colorPicker',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
14
runtime/tmagic-form/src/form-config/checkbox.ts
Normal file
14
runtime/tmagic-form/src/form-config/checkbox.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { createForm } from '@tmagic/form';
|
||||
|
||||
export default createForm([
|
||||
{
|
||||
name: 'activeValue',
|
||||
text: '选中时的值',
|
||||
defaultValue: true,
|
||||
},
|
||||
{
|
||||
name: 'inactiveValue',
|
||||
text: '没有选中时的值',
|
||||
defaultValue: false,
|
||||
},
|
||||
]);
|
||||
33
runtime/tmagic-form/src/form-config/common.ts
Normal file
33
runtime/tmagic-form/src/form-config/common.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { createForm } from '@tmagic/form';
|
||||
|
||||
export default createForm([
|
||||
{
|
||||
name: 'id',
|
||||
type: 'hidden',
|
||||
},
|
||||
{
|
||||
name: 'type',
|
||||
type: 'hidden',
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
text: '表单key',
|
||||
extra: '字段名',
|
||||
},
|
||||
{
|
||||
name: 'text',
|
||||
text: '标签文本',
|
||||
extra: 'label 标签的文本',
|
||||
},
|
||||
{
|
||||
name: 'labelWidth',
|
||||
text: '标签宽度',
|
||||
extra: '表单域标签的的宽度,例如 "50px"。支持 auto。',
|
||||
},
|
||||
{
|
||||
name: 'disabled',
|
||||
text: '是否禁用',
|
||||
type: 'switch',
|
||||
defaultValue: false,
|
||||
},
|
||||
]);
|
||||
3
runtime/tmagic-form/src/form-config/display.ts
Normal file
3
runtime/tmagic-form/src/form-config/display.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { createForm } from '@tmagic/form';
|
||||
|
||||
export default createForm([]);
|
||||
17
runtime/tmagic-form/src/form-config/index.ts
Normal file
17
runtime/tmagic-form/src/form-config/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { FormConfig } from '@tmagic/form';
|
||||
|
||||
import checkbox from './checkbox';
|
||||
import display from './display';
|
||||
import number from './number';
|
||||
import switchConfig from './switch';
|
||||
import text from './text';
|
||||
|
||||
const configs: Record<string, FormConfig> = {
|
||||
text,
|
||||
checkbox,
|
||||
display,
|
||||
number,
|
||||
switch: switchConfig,
|
||||
};
|
||||
|
||||
export default configs;
|
||||
23
runtime/tmagic-form/src/form-config/number.ts
Normal file
23
runtime/tmagic-form/src/form-config/number.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { createForm } from '@tmagic/form';
|
||||
|
||||
export default createForm([
|
||||
{
|
||||
type: 'number',
|
||||
name: 'min',
|
||||
text: '最小值',
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
name: 'max',
|
||||
text: '最大值',
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
name: 'step',
|
||||
text: '步数',
|
||||
},
|
||||
{
|
||||
name: 'placeholder',
|
||||
text: 'placeholder',
|
||||
},
|
||||
]);
|
||||
3
runtime/tmagic-form/src/form-config/switch.ts
Normal file
3
runtime/tmagic-form/src/form-config/switch.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { createForm } from '@tmagic/form';
|
||||
|
||||
export default createForm([]);
|
||||
33
runtime/tmagic-form/src/form-config/text.ts
Normal file
33
runtime/tmagic-form/src/form-config/text.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { createForm } from '@tmagic/form';
|
||||
|
||||
export default createForm([
|
||||
{
|
||||
name: 'placeholder',
|
||||
text: 'placeholder',
|
||||
},
|
||||
{
|
||||
name: 'append',
|
||||
legend: '后置按钮',
|
||||
type: 'fieldset',
|
||||
labelWidth: '80px',
|
||||
checkbox: true,
|
||||
expand: true,
|
||||
items: [
|
||||
{
|
||||
name: 'type',
|
||||
type: 'hidden',
|
||||
defaultValue: 'button',
|
||||
},
|
||||
{
|
||||
name: 'text',
|
||||
text: '按钮文案',
|
||||
},
|
||||
{
|
||||
name: 'handler',
|
||||
type: 'vs-code',
|
||||
height: '400px',
|
||||
text: '点击',
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
86
runtime/tmagic-form/src/index.ts
Normal file
86
runtime/tmagic-form/src/index.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { createApp, onBeforeUnmount } from 'vue';
|
||||
import cssStyle from 'element-plus/dist/index.css?raw';
|
||||
|
||||
import { editorService, Layout, propsService, uiService } from '@tmagic/editor';
|
||||
import MagicForm, { type FormConfig } from '@tmagic/form';
|
||||
import type StageCore from '@tmagic/stage';
|
||||
import { injectStyle } from '@tmagic/utils';
|
||||
|
||||
import commonConfig from './form-config/common';
|
||||
import App from './App.vue';
|
||||
import formConfigs from './form-config';
|
||||
|
||||
export * from './component-group-list';
|
||||
|
||||
export const propsConfigs = formConfigs;
|
||||
|
||||
export const canSelect = (el: HTMLElement) => Boolean(el.dataset.magicId);
|
||||
|
||||
export const useRuntime = () => {
|
||||
const render = (stage: StageCore) => {
|
||||
injectStyle(stage.renderer.getDocument()!, cssStyle);
|
||||
injectStyle(
|
||||
stage.renderer.getDocument()!,
|
||||
`
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
::-webkit-scrollbar {
|
||||
width: 0;
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
const el: HTMLDivElement = globalThis.document.createElement('div');
|
||||
el.id = 'app';
|
||||
el.style.overflow = 'auto';
|
||||
|
||||
createApp(App, {
|
||||
stage,
|
||||
})
|
||||
.use(MagicForm)
|
||||
.mount(el);
|
||||
|
||||
setTimeout(() => {
|
||||
uiService.set('showRule', false);
|
||||
});
|
||||
|
||||
return el;
|
||||
};
|
||||
|
||||
propsService.usePlugin({
|
||||
afterFillConfig(config: FormConfig, itemConfig: FormConfig) {
|
||||
return [
|
||||
{
|
||||
type: 'tab',
|
||||
items: [
|
||||
{
|
||||
title: '属性',
|
||||
labelWidth: '80px',
|
||||
items: [...commonConfig, ...itemConfig],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
},
|
||||
});
|
||||
|
||||
editorService.usePlugin({
|
||||
afterGetLayout() {
|
||||
return Layout.RELATIVE;
|
||||
},
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
propsService.removeAllPlugins();
|
||||
editorService.removeAllPlugins();
|
||||
});
|
||||
|
||||
return {
|
||||
render,
|
||||
};
|
||||
};
|
||||
143
runtime/tmagic-form/src/useFormConfig.ts
Normal file
143
runtime/tmagic-form/src/useFormConfig.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import { computed, nextTick, onBeforeUnmount, reactive, ref } from 'vue';
|
||||
|
||||
import Core from '@tmagic/core';
|
||||
import { type FormConfig, initValue, MForm } from '@tmagic/form';
|
||||
import type { Id, MApp, MNode } from '@tmagic/schema';
|
||||
import type { RemoveData, RuntimeWindow, UpdateData } from '@tmagic/stage';
|
||||
import { getNodePath, replaceChildNode } from '@tmagic/utils';
|
||||
|
||||
export const useFormConfig = (contentWindow: RuntimeWindow | null) => {
|
||||
const mForm = ref<InstanceType<typeof MForm>>();
|
||||
|
||||
const root = ref<MApp>();
|
||||
const values = ref({});
|
||||
const curPageId = ref<Id>();
|
||||
const selectedId = ref<Id>();
|
||||
|
||||
const config = computed(
|
||||
() => root.value?.items?.find((item: MNode) => item.id === curPageId.value) || root.value?.items?.[0],
|
||||
);
|
||||
|
||||
const formConfig = computed(() => (config.value?.items || []) as FormConfig);
|
||||
|
||||
const app = new Core({
|
||||
ua: contentWindow?.navigator.userAgent,
|
||||
platform: 'editor',
|
||||
});
|
||||
|
||||
const resetValues = () => {
|
||||
initValue(mForm.value?.formState, {
|
||||
initValues: {},
|
||||
config: formConfig.value,
|
||||
}).then((value) => {
|
||||
values.value = value;
|
||||
});
|
||||
};
|
||||
|
||||
const runtimeReadyHandler = ({ data }: any) => {
|
||||
if (!data.tmagicRuntimeReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
contentWindow?.magic?.onRuntimeReady({
|
||||
getApp() {
|
||||
return app;
|
||||
},
|
||||
|
||||
updateRootConfig(config: MApp) {
|
||||
root.value = config;
|
||||
app?.setConfig(config, curPageId.value);
|
||||
},
|
||||
|
||||
updatePageId(id: Id) {
|
||||
curPageId.value = id;
|
||||
app?.setPage(id);
|
||||
},
|
||||
|
||||
select(id: Id) {
|
||||
selectedId.value = id;
|
||||
|
||||
if (app?.getPage(id)) {
|
||||
this.updatePageId?.(id);
|
||||
}
|
||||
|
||||
const el = document.getElementById(`${id}`);
|
||||
if (el) return el;
|
||||
// 未在当前文档下找到目标元素,可能是还未渲染,等待渲染完成后再尝试获取
|
||||
return nextTick().then(() => document.getElementById(`${id}`) as HTMLElement);
|
||||
},
|
||||
|
||||
add({ config, parentId }: UpdateData) {
|
||||
if (!root.value) throw new Error('error');
|
||||
if (!selectedId.value) throw new Error('error');
|
||||
if (!parentId) throw new Error('error');
|
||||
|
||||
const parent = getNodePath(parentId, [root.value]).pop();
|
||||
if (!parent) throw new Error('未找到父节点');
|
||||
|
||||
if (config.type !== 'page') {
|
||||
const parentNode = app?.page?.getNode(parent.id);
|
||||
parentNode && app?.page?.initNode(config, parentNode);
|
||||
}
|
||||
|
||||
if (parent.id !== selectedId.value) {
|
||||
const index = parent.items?.findIndex((child: MNode) => child.id === selectedId.value);
|
||||
parent.items?.splice(index + 1, 0, config);
|
||||
} else {
|
||||
// 新增节点添加到配置中
|
||||
parent.items?.push(config);
|
||||
}
|
||||
|
||||
resetValues();
|
||||
},
|
||||
|
||||
update({ config, parentId }: UpdateData) {
|
||||
if (!root.value || !app) throw new Error('error');
|
||||
|
||||
const newNode = app.dataSourceManager?.compiledNode(config) || config;
|
||||
replaceChildNode(reactive(newNode), [root.value], parentId);
|
||||
|
||||
const nodeInstance = app.page?.getNode(config.id);
|
||||
if (nodeInstance) {
|
||||
nodeInstance.setData(config);
|
||||
}
|
||||
|
||||
resetValues();
|
||||
},
|
||||
|
||||
remove({ id, parentId }: RemoveData) {
|
||||
if (!root.value) throw new Error('error');
|
||||
|
||||
const node = getNodePath(id, [root.value]).pop();
|
||||
if (!node) throw new Error('未找到目标元素');
|
||||
|
||||
const parent = getNodePath(parentId, [root.value]).pop();
|
||||
if (!parent) throw new Error('未找到父元素');
|
||||
|
||||
if (node.type === 'page') {
|
||||
app?.deletePage();
|
||||
} else {
|
||||
app?.page?.deleteNode(node.id);
|
||||
}
|
||||
|
||||
const index = parent.items?.findIndex((child: MNode) => child.id === node.id);
|
||||
parent.items.splice(index, 1);
|
||||
|
||||
resetValues();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
contentWindow?.addEventListener('message', runtimeReadyHandler);
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
contentWindow?.removeEventListener('message', runtimeReadyHandler);
|
||||
});
|
||||
|
||||
return {
|
||||
mForm,
|
||||
config,
|
||||
formConfig,
|
||||
values,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user