feat: 新增tmagic-form-runtime

This commit is contained in:
roymondchen 2024-01-09 14:58:13 +08:00
parent a10ae0ddd1
commit f8443ed316
24 changed files with 694 additions and 147 deletions

View File

@ -18,6 +18,7 @@
"@tmagic/editor": "1.3.9",
"@tmagic/element-plus-adapter": "1.3.9",
"@tmagic/form": "1.3.9",
"@tmagic/tmagic-form-runtime": "1.0.0",
"@tmagic/schema": "1.3.9",
"@tmagic/stage": "1.3.9",
"@tmagic/utils": "1.3.9",

View File

@ -1,3 +0,0 @@
import type { FormConfig } from '@tmagic/form';
export default [] as FormConfig;

View File

@ -1,3 +0,0 @@
import type { FormConfig } from '@tmagic/form';
export default [] as FormConfig;

View File

@ -1,3 +0,0 @@
import type { FormConfig } from '@tmagic/form';
export default [] as FormConfig;

View File

@ -7,6 +7,7 @@
:props-configs="propsConfigs"
:render="render"
:can-select="canSelect"
:disabled-page-fragment="true"
:stage-rect="{ width: 'calc(100% - 70px)', height: '100%' }"
:moveable-options="{ resizable: false }"
>
@ -17,33 +18,26 @@
</template>
<script setup lang="tsx">
import { createApp, onBeforeUnmount, ref } from 'vue';
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { Document } from '@element-plus/icons-vue';
import cssStyle from 'element-plus/dist/index.css?raw';
import { MenuBarData, SideBarData, TMagicEditor, traverseNode } from '@tmagic/editor';
import { type MApp, NodeType } from '@tmagic/schema';
import {
ComponentGroup,
MenuBarData,
propsService,
SideBarData,
TMagicEditor,
traverseNode,
uiService,
} from '@tmagic/editor';
import MagicForm, { type FormConfig, MForm } from '@tmagic/form';
import { type MApp, type MNode, NodeType } from '@tmagic/schema';
import type StageCore from '@tmagic/stage';
import { guid, injectStyle } from '@tmagic/utils';
canSelect,
COMPONENT_GROUP_LIST as componentGroupList,
propsConfigs,
useRuntime,
} from '@tmagic/tmagic-form-runtime';
import { guid } from '@tmagic/utils';
import propsConfigs from '../configs/form-config';
import commonConfig from '../configs/form-config/common';
import formDsl from '../configs/formDsl';
formDsl.forEach((item) => {
traverseNode<any>(item, (item) => {
item.id = `${item.type}_${guid()}`;
item.type = item.type || (item.items ? 'container' : 'text');
item.id = `${item.type}_${guid()}`;
item.style = {
left: 0,
top: 0,
@ -55,95 +49,10 @@ formDsl.forEach((item) => {
const config = ref<MApp>({
type: NodeType.ROOT,
id: 'app_form',
items: [
{
type: NodeType.PAGE,
id: 'page_form',
layout: 'relative',
items: formDsl as unknown as MNode[],
},
],
items: [],
});
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(MForm, {
config: config.value.items[0].items,
initValues: {},
})
.use(MagicForm)
.mount(el);
stage.renderer.contentWindow?.magic?.onRuntimeReady({});
setTimeout(() => {
stage.renderer.contentWindow?.magic.onPageElUpdate(el.children[0] as HTMLElement);
uiService.set('showRule', false);
});
return el;
};
const componentGroupList: 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: [],
},
},
],
},
];
const { render } = useRuntime();
const router = useRouter();
@ -186,27 +95,4 @@ const sidebar: SideBarData = {
status: '组件',
items: ['component-list', 'layer'],
};
const canSelect = (el: HTMLElement) => Boolean(el.dataset.magicId);
propsService.usePlugin({
afterFillConfig(config: FormConfig, itemConfig: FormConfig) {
return [
{
type: 'tab',
items: [
{
title: '属性',
labelWidth: '80px',
items: [...commonConfig, ...itemConfig],
},
],
},
];
},
});
onBeforeUnmount(() => {
propsService.removeAllPlugins();
});
</script>

View File

@ -54,6 +54,10 @@ export default defineConfig({
{ find: /^@tmagic\/editor/, replacement: path.join(__dirname, '../packages/editor/src/index.ts') },
{ find: /^@tmagic\/schema/, replacement: path.join(__dirname, '../packages/schema/src/index.ts') },
{ find: /^@tmagic\/form/, replacement: path.join(__dirname, '../packages/form/src/index.ts') },
{
find: /^@tmagic\/tmagic-form-runtime/,
replacement: path.join(__dirname, '../runtime/tmagic-form/src/index.ts'),
},
{ find: /^@tmagic\/table/, replacement: path.join(__dirname, '../packages/table/src/index.ts') },
{ find: /^@tmagic\/stage/, replacement: path.join(__dirname, '../packages/stage/src/index.ts') },
{ find: /^@tmagic\/utils/, replacement: path.join(__dirname, '../packages/utils/src/index.ts') },

54
pnpm-lock.yaml generated
View File

@ -761,6 +761,9 @@ importers:
'@tmagic/stage':
specifier: 1.3.9
version: link:../packages/stage
'@tmagic/tmagic-form-runtime':
specifier: 1.0.0
version: link:../runtime/tmagic-form
'@tmagic/utils':
specifier: 1.3.9
version: link:../packages/utils
@ -890,6 +893,55 @@ importers:
specifier: ^5.0.7
version: 5.0.7(@types/node@18.19.3)(sass@1.35.1)(terser@5.14.2)
runtime/tmagic-form:
dependencies:
'@tmagic/core':
specifier: ^1.3.9
version: link:../../packages/core
'@tmagic/editor':
specifier: ^1.3.9
version: link:../../packages/editor
'@tmagic/form':
specifier: ^1.3.9
version: link:../../packages/form
'@tmagic/schema':
specifier: ^1.3.9
version: link:../../packages/schema
'@tmagic/utils':
specifier: ^1.3.9
version: link:../../packages/utils
element-plus:
specifier: ^2.4.3
version: 2.4.3(vue@3.3.8)
vue:
specifier: ^3.3.8
version: 3.3.8(typescript@5.0.4)
devDependencies:
'@tmagic/stage':
specifier: ^1.3.9
version: link:../../packages/stage
'@types/node':
specifier: ^18.19.0
version: 18.19.3
'@vitejs/plugin-vue':
specifier: ^4.5.2
version: 4.5.2(vite@5.0.7)(vue@3.3.8)
'@vue/compiler-sfc':
specifier: ^3.3.8
version: 3.3.8
rimraf:
specifier: ^3.0.2
version: 3.0.2
typescript:
specifier: ^5.0.4
version: 5.0.4
vite:
specifier: ^5.0.7
version: 5.0.7(@types/node@18.19.3)(sass@1.35.1)(terser@5.14.2)
vue-tsc:
specifier: ^1.8.25
version: 1.8.25(typescript@5.0.4)
runtime/vue2:
dependencies:
'@tmagic/cli':
@ -8936,7 +8988,7 @@ packages:
dependencies:
hosted-git-info: 4.1.0
is-core-module: 2.11.0
semver: 7.5.1
semver: 7.5.4
validate-npm-package-license: 3.0.4
dev: true

View File

@ -0,0 +1,38 @@
# TMagicFormRuntime
TMagicFormRuntime 基于@tmagic/form的编辑器runtime
## 环境准备
先基于[tmagic-editor](https://tencent.github.io/tmagic-editor/docs/guide/)将编辑器搭建起来
按住依赖
```bash
pnpm add @tmagic/tmagic-form-runtime
```
```html
<TMagicEditor
:component-group-list="componentGroupList"
:props-configs="propsConfigs"
:render="render"
:can-select="canSelect"
:disabled-page-fragment="true"
:stage-rect="{ width: 'calc(100% - 70px)', height: '100%' }"
:moveable-options="{ resizable: false }"
...
>
</TMagicEditor>
```
```ts
import {
canSelect,
COMPONENT_GROUP_LIST as componentGroupList,
propsConfigs,
useRuntime,
} from '@tmagic/tmagic-form-runtime';
const { render } = useRuntime();
```

View File

@ -0,0 +1,58 @@
{
"version": "1.0.0",
"name": "@tmagic/tmagic-form-runtime",
"type": "module",
"sideEffects": [
"dist/*",
"src/theme/*"
],
"main": "dist/tmagic-form-runtime.umd.cjs",
"module": "dist/tmagic-form-runtime.js",
"types": "types/index.d.ts",
"exports": {
".": {
"types": "./types/index.d.ts",
"import": "./dist/tmagic-form-runtime.js",
"require": "./dist/tmagic-form.umd-runtime.cjs"
},
"./*": "./*"
},
"license": "Apache-2.0",
"scripts": {
"build": "npm run build:type && vite build",
"build:type": "npm run clear:type && vue-tsc --declaration --emitDeclarationOnly --project tsconfig.build.json",
"clear:type": "rimraf ./types"
},
"engines": {
"node": ">=18"
},
"repository": {
"type": "git",
"url": "https://github.com/Tencent/tmagic-editor.git"
},
"dependencies": {
"@tmagic/core": "^1.3.9",
"@tmagic/editor": "^1.3.9",
"@tmagic/form": "^1.3.9",
"@tmagic/utils": "^1.3.9",
"@tmagic/schema": "^1.3.9",
"element-plus": "^2.4.3"
},
"peerDependencies": {
"@tmagic/editor": "^1.3.9",
"@tmagic/form": "^1.3.9",
"@tmagic/schema": "^1.3.9",
"element-plus": "^2.4.3",
"vue": "^3.3.8"
},
"devDependencies": {
"@tmagic/stage": "^1.3.9",
"@types/node": "^18.19.0",
"@vitejs/plugin-vue": "^4.5.2",
"@vue/compiler-sfc": "^3.3.8",
"rimraf": "^3.0.2",
"typescript": "^5.0.4",
"vite": "^5.0.7",
"vue-tsc": "^1.8.25"
}
}

View 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>

View 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',
},
},
],
},
];

View File

@ -0,0 +1,14 @@
import { createForm } from '@tmagic/form';
export default createForm([
{
name: 'activeValue',
text: '选中时的值',
defaultValue: true,
},
{
name: 'inactiveValue',
text: '没有选中时的值',
defaultValue: false,
},
]);

View File

@ -1,6 +1,6 @@
import type { FormConfig } from '@tmagic/form';
import { createForm } from '@tmagic/form';
export default [
export default createForm([
{
name: 'id',
type: 'hidden',
@ -24,4 +24,10 @@ export default [
text: '标签宽度',
extra: '表单域标签的的宽度,例如 "50px"。支持 auto。',
},
] as FormConfig;
{
name: 'disabled',
text: '是否禁用',
type: 'switch',
defaultValue: false,
},
]);

View File

@ -0,0 +1,3 @@
import { createForm } from '@tmagic/form';
export default createForm([]);

View File

@ -1,13 +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';
export default {
const configs: Record<string, FormConfig> = {
text,
checkbox,
display,
number,
switch: switchConfig,
};
export default configs;

View File

@ -1,6 +1,6 @@
import type { FormConfig } from '@tmagic/form';
import { createForm } from '@tmagic/form';
export default [
export default createForm([
{
type: 'number',
name: 'min',
@ -20,4 +20,4 @@ export default [
name: 'placeholder',
text: 'placeholder',
},
] as FormConfig;
]);

View File

@ -0,0 +1,3 @@
import { createForm } from '@tmagic/form';
export default createForm([]);

View File

@ -1,6 +1,6 @@
import type { FormConfig } from '@tmagic/form';
import { createForm } from '@tmagic/form';
export default [
export default createForm([
{
name: 'placeholder',
text: 'placeholder',
@ -10,7 +10,14 @@ export default [
legend: '后置按钮',
type: 'fieldset',
labelWidth: '80px',
checkbox: true,
expand: true,
items: [
{
name: 'type',
type: 'hidden',
defaultValue: 'button',
},
{
name: 'text',
text: '按钮文案',
@ -23,4 +30,4 @@ export default [
},
],
},
] as FormConfig;
]);

View 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,
};
};

View 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,
};
};

View File

@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"baseUrl": ".",
"declaration": true,
"declarationDir": "types",
"forceConsistentCasingInFileNames": true,
"paths": {},
},
"include": [
"src"
],
}

View File

@ -0,0 +1,6 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"baseUrl": "../..",
},
}

View File

@ -0,0 +1,45 @@
/*
* Tencent is pleased to support the open source community by making TMagicEditor available.
*
* Copyright (C) 2023 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import pkg from './package.json';
export default defineConfig({
plugins: [vue()],
build: {
cssCodeSplit: false,
sourcemap: true,
minify: false,
target: 'esnext',
lib: {
entry: 'src/index.ts',
name: 'TMagicFormRuntime',
fileName: 'tmagic-form-runtime',
},
rollupOptions: {
// 确保外部化处理那些你不想打包进库的依赖
external(id: string) {
return Object.keys(pkg.dependencies).some((k) => new RegExp(`^${k}`).test(id));
},
},
},
});

View File

@ -19,6 +19,7 @@
"paths": {
// src/index.ts, .
"@tmagic/*": ["packages/*/src"],
"@tmagic/tmagic-form-runtime": ["runtime/tmagic-form/src"],
"@editor/*": ["packages/editor/src/*"],
"@form/*": ["packages/form/src/*"],
"@data-source/*": ["packages/data-source/src/*"],