): (props: P) => ReactElement;
8 | }
9 |
10 | export interface ComponentEditor, T> extends Component
{
11 | }
12 |
13 | export interface ReactComponentProps {
14 | definition: ComponentDefinition;
15 |
16 | onRemove(): void;
17 | }
18 |
19 |
20 | export interface ReactComponentState {
21 | renderCounter: number;
22 | }
23 |
24 | export interface ReactComponentGroupState extends ReactComponentState {
25 | definition: ComponentDefinition | null;
26 | }
27 |
28 |
29 | export interface Activatable {
30 | onActive(): void;
31 |
32 | unActive(): void;
33 | }
34 |
--------------------------------------------------------------------------------
/src/factory/InputFactory.ts:
--------------------------------------------------------------------------------
1 | import {FactoryGroup, FieldFactory, FieldType} from "@@/types";
2 | import {InputProps} from "@@/props";
3 | import {makeComponentId, makeFieldId} from "@@/utils";
4 |
5 | class InputFactory implements FieldFactory {
6 | readonly type = "Input"
7 | readonly group = FactoryGroup.Component;
8 |
9 | title = "单行输入框"
10 |
11 | /**
12 | * 初始化一个组件定义
13 | * @returns {{type: string, title: string}}
14 | */
15 | createComponentDefinition() {
16 | return {
17 | id: makeComponentId(),
18 | type: this.type,
19 | title: this.title,
20 | fieldDef: {
21 | fieldId: makeFieldId(),
22 | fieldType: 'varchar' as FieldType,
23 | fieldName: ''
24 | },
25 | props: {
26 | placeholder: '请输入'
27 | },
28 | }
29 | }
30 | }
31 |
32 | export default new InputFactory();
33 |
--------------------------------------------------------------------------------
/antd-designer/src/util/MiscUtil.ts:
--------------------------------------------------------------------------------
1 | export function guaranteeNumber(value: number, min: number, max: number) {
2 | if (value === undefined || value == null) {
3 | return value;
4 | }
5 | if (value < min) { // 最小值
6 | return min;
7 | }
8 |
9 | if (value > max) {// 最大值
10 | return max;
11 | }
12 | return value;
13 | }
14 |
15 | export function isNull() {
16 | for (let i = 0; i < arguments.length; i++) {
17 | if (arguments[i] === undefined || arguments[i] == null) {
18 | return true;
19 | }
20 | }
21 | return false;
22 | }
23 |
24 | /**
25 | * 获取属性值并擦除属性
26 | * @param obj
27 | * @param prop
28 | * @returns {*}
29 | */
30 | export function getErasure(obj: any, prop: string) {
31 | if (typeof obj !== 'object') {
32 | throw new Error(`not expected type:${typeof obj}, must be object`);
33 | }
34 | const value = obj[prop];
35 | delete obj[prop];
36 | return value;
37 | }
38 |
--------------------------------------------------------------------------------
/src/factory/TextAreaFactory.ts:
--------------------------------------------------------------------------------
1 | import {FactoryGroup, FieldFactory, FieldType} from "@@/types";
2 | import {TextAreaProps} from "@@/props";
3 | import {makeComponentId, makeFieldId} from "@@/utils";
4 |
5 | class TextAreaFactory implements FieldFactory {
6 | readonly type = "TextArea"
7 | readonly group = FactoryGroup.Component;
8 | title = "多行输入框"
9 |
10 |
11 | /**
12 | * 初始化一个组件定义
13 | * @returns {{type: string, title: string}}
14 | */
15 | createComponentDefinition() {
16 | return {
17 | id: makeComponentId(),
18 | type: this.type,
19 | title: this.title,
20 | fieldDef: {
21 | fieldId: makeFieldId(),
22 | fieldType: 'varchar' as FieldType,
23 | fieldName: '',
24 | },
25 | props: {
26 | placeholder: '请输入'
27 | },
28 | }
29 | }
30 | }
31 |
32 | export default new TextAreaFactory();
33 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "ts-node": {
3 | "compilerOptions": {
4 | "module": "commonjs"
5 | }
6 | },
7 | "compilerOptions": {
8 | "sourceMap": true,
9 | // "outDir": "./dist",
10 | "target": "es6",
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "esModuleInterop": true,
14 | "allowSyntheticDefaultImports": true,
15 | "allowJs": true,
16 | "jsx": "react",
17 | "strict": true,
18 | "skipLibCheck": true,
19 | "stripInternal": true,
20 | "experimentalDecorators": true,
21 | "noImplicitAny": false,
22 | "lib": [
23 | "dom",
24 | "es2017"
25 | ],
26 | "baseUrl": ".",
27 | "paths": {
28 | "@@/*": [
29 | "./src/*"
30 | ]
31 | }
32 | },
33 | "path": [],
34 | "exclude": [
35 | "node_modules",
36 | "dist",
37 | "**/*.js"
38 | ],
39 | "include": [
40 | "./src/**/*.d.ts",
41 | "./src/**/*.ts",
42 | "./src/**/*.tsx",
43 | "./src/**/*.vue"
44 | ]
45 | }
46 |
--------------------------------------------------------------------------------
/antdv-designer/src/designer/components/input/PropEditor.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
39 |
40 |
43 |
--------------------------------------------------------------------------------
/antdv-designer/src/designer/types.ts:
--------------------------------------------------------------------------------
1 | import {ComponentDefinition} from "@@/types";
2 | import {ComponentInternalInstance} from "@vue/runtime-core";
3 |
4 | export interface ComponentFactoryRender {
5 | renderComponent(definition: ComponentDefinition): (props: P) => ComponentInternalInstance;
6 |
7 | renderEditor(definition: ComponentDefinition): (props: P) => ComponentInternalInstance;
8 | }
9 |
10 | export interface ComponentEditor, T> extends ComponentInternalInstance {
11 | }
12 |
13 | export interface VueComponentProps {
14 | definition: ComponentDefinition;
15 |
16 | onRemove?: () => void;
17 | }
18 |
19 |
20 | export interface ReactComponentState {
21 | renderCounter: number;
22 | }
23 |
24 | export interface ReactComponentGroupState extends ComponentInternalInstance {
25 | definition: ComponentDefinition | null;
26 | }
27 |
28 |
29 | export interface Activatable {
30 | onActive(): void;
31 |
32 | unActive(): void;
33 | }
34 |
--------------------------------------------------------------------------------
/antdv-designer/src/designer/components/textarea/PropEditor.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
39 |
40 |
43 |
--------------------------------------------------------------------------------
/antd-designer/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "declaration": false,
5 | "sourceMap": true,
6 | // "outDir": "./dist",
7 | "target": "es6",
8 | "module": "esnext",
9 | "moduleResolution": "node",
10 | "esModuleInterop": true,
11 | "noImplicitOverride": true,
12 | "allowSyntheticDefaultImports": true,
13 | "allowJs": true,
14 | "jsx": "react",
15 | "strict": true,
16 | "skipLibCheck": true,
17 | "stripInternal": true,
18 | "experimentalDecorators": true,
19 | "noImplicitAny": false,
20 | "lib": [
21 | "dom",
22 | "es2017"
23 | ],
24 | "baseUrl": ".",
25 | "paths": {
26 | "@@/*": [
27 | "../src/*"
28 | ],
29 | "@/*": [
30 | "./src/*"
31 | ]
32 | }
33 | },
34 | "exclude": [
35 | "./node_modules"
36 | ],
37 | "include": [
38 | "./**/*.d.ts",
39 | "./**/*.ts",
40 | "./**/*.tsx",
41 | "../src/**/*.ts",
42 | "../src/**/*.d.ts"
43 | ]
44 | }
45 |
--------------------------------------------------------------------------------
/antdv-designer/src/designer/components/radio/PropEditor.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
40 |
41 |
44 |
--------------------------------------------------------------------------------
/antdv-designer/src/designer/components/select/PropEditor.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
40 |
41 |
44 |
--------------------------------------------------------------------------------
/antdv-designer/src/designer/components/checkbox/PropEditor.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
40 |
41 |
44 |
--------------------------------------------------------------------------------
/antdv-designer/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "declaration": false,
5 | "sourceMap": true,
6 | // "outDir": "./dist",
7 | "target": "es6",
8 | "module": "esnext",
9 | "moduleResolution": "node",
10 | "esModuleInterop": true,
11 | "noImplicitOverride": true,
12 | "allowSyntheticDefaultImports": true,
13 | "allowJs": true,
14 | "jsx": "react",
15 | "strict": true,
16 | "skipLibCheck": true,
17 | "stripInternal": true,
18 | "experimentalDecorators": true,
19 | "noImplicitAny": false,
20 | "lib": [
21 | "dom",
22 | "es2017"
23 | ],
24 | "baseUrl": ".",
25 | "paths": {
26 | "@@/*": [
27 | "../src/*"
28 | ],
29 | "@/*": [
30 | "./src/*"
31 | ]
32 | }
33 | },
34 | "exclude": [
35 | "./node_modules"
36 | ],
37 | "include": [
38 | "./src/**/*.d.ts",
39 | "./src/**/*.ts",
40 | "./src/**/*.tsx",
41 | "./src/**/*.vue",
42 | "../src/**/*.ts",
43 | "../src/**/*.d.ts"
44 | ]
45 | }
46 |
--------------------------------------------------------------------------------
/antdv-designer/src/designer/components/rate/PropEditor.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
10 |
42 |
43 |
46 |
--------------------------------------------------------------------------------
/src/factory/SelectFactory.ts:
--------------------------------------------------------------------------------
1 | import {FactoryGroup, FieldFactory, FieldType} from "@@/types";
2 | import {SelectProps} from "@@/props";
3 | import {makeComponentId, makeFieldId} from "@@/utils";
4 |
5 | class SelectFactory implements FieldFactory {
6 | readonly type = "Select"
7 | readonly group = FactoryGroup.Component;
8 | title = "下拉选择"
9 |
10 | /**
11 | * 初始化一个组件定义
12 | * @returns {{type: string, title: string}}
13 | */
14 | createComponentDefinition() {
15 | return {
16 | id: makeComponentId(),
17 | type: this.type,
18 | title: this.title,
19 | fieldDef: {
20 | fieldId: makeFieldId(),
21 | fieldType: '' as FieldType,
22 | fieldName: '',
23 | },
24 | props: {
25 | placeholder: '请输入',
26 | options: [
27 | {label: '显示值', value: '真值', checked: false, disabled: false}
28 | ]
29 | },
30 | }
31 | }
32 | }
33 |
34 | export default new SelectFactory();
35 |
--------------------------------------------------------------------------------
/src/factory/RadioFactory.ts:
--------------------------------------------------------------------------------
1 | import {FactoryGroup, FieldFactory, FieldType} from "@@/types";
2 | import {RadioProps} from "@@/props";
3 | import {makeComponentId, makeFieldId} from "@@/utils";
4 |
5 | class RadioFactory implements FieldFactory {
6 | readonly type = "Radio"
7 | readonly group = FactoryGroup.Component;
8 |
9 | title = "单选框"
10 |
11 | /**
12 | * 初始化一个组件定义
13 | * @returns {{type: string, title: string}}
14 | */
15 | createComponentDefinition() {
16 | return {
17 | id: makeComponentId(),
18 | type: this.type,
19 | title: this.title,
20 | fieldDef: {
21 | fieldId: makeFieldId(),
22 | fieldType: 'varchar' as FieldType,
23 | fieldName: '',
24 | },
25 | props: {
26 | placeholder: '请输入',
27 | options: [
28 | {label: '显示值', value: '真值', checked: false, disabled: false}
29 | ]
30 | },
31 | }
32 | }
33 | }
34 |
35 | export default new RadioFactory();
36 |
--------------------------------------------------------------------------------
/antdv-designer/src/designer/layout/column/PropEditor.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
10 |
42 |
43 |
46 |
--------------------------------------------------------------------------------
/antd-designer/src/designer/components/InputFactory.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Input} from 'antd';
3 | import {ComponentWrapper, FactoryRegister} from '../wrapper';
4 | import {ReactComponent} from '../ReactComponent';
5 | import {PropsEditor} from '../widgets/PropsEditor';
6 | import {ReactComponentProps, ReactComponentState} from "../types";
7 | import {InputProps} from "@@/props";
8 | import InputFactory from "@@/factory/InputFactory";
9 |
10 | /**
11 | * 组件
12 | */
13 | @ComponentWrapper
14 | class InputComponent extends ReactComponent, InputProps, ReactComponentState> {
15 |
16 | override render() {
17 | const {definition: {props}} = this.props;
18 | return (
19 |
20 | )
21 | }
22 | }
23 |
24 | /**
25 | * 组件属性编辑器
26 | */
27 | class InputComponentEditor extends PropsEditor {
28 | doRender() {
29 | return (
30 | <>>
31 | );
32 | }
33 | }
34 |
35 | /**
36 | * 组件工厂
37 | */
38 | FactoryRegister(InputComponent, InputComponentEditor)(InputFactory)
39 |
40 |
--------------------------------------------------------------------------------
/src/factory/CheckboxFactory.ts:
--------------------------------------------------------------------------------
1 | import {FactoryGroup, FieldFactory, FieldType} from "@@/types";
2 | import {CheckboxProps} from "@@/props";
3 | import {makeComponentId, makeFieldId} from "@@/utils";
4 |
5 | class CheckboxFactory implements FieldFactory {
6 | readonly type = "Checkbox"
7 |
8 | readonly group = FactoryGroup.Component;
9 |
10 | title = "多选框"
11 |
12 | /**
13 | * 初始化一个组件定义
14 | * @returns {{type: string, title: string}}
15 | */
16 | createComponentDefinition() {
17 | return {
18 | id: makeComponentId(),
19 | type: this.type,
20 | title: this.title,
21 | fieldDef: {
22 | fieldId: makeFieldId(),
23 | fieldType: "array" as FieldType,
24 | fieldName: '',
25 | },
26 | props: {
27 | placeholder: '请输入',
28 | options: [
29 | {label: '显示值', value: '真值', checked: false, disabled: false}
30 | ]
31 | },
32 | }
33 | }
34 | }
35 |
36 | export default new CheckboxFactory();
37 |
--------------------------------------------------------------------------------
/antdv-designer/src/designer/components/radio/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
40 |
41 |
43 |
--------------------------------------------------------------------------------
/antdv-designer/src/designer/layout/column/Cell.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
11 |
42 |
43 |
46 |
--------------------------------------------------------------------------------
/antd-designer/src/designer/components/TextAreaFactory.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Input} from 'antd';
3 | import {ComponentWrapper, FactoryRegister} from '../wrapper';
4 | import {ReactComponent} from '../ReactComponent';
5 | import {PropsEditor} from '../widgets/PropsEditor';
6 | import {ReactComponentProps, ReactComponentState} from "../types";
7 | import {TextAreaProps} from "../../../../src/props";
8 | import TextAreaFactory from "@@/factory/TextAreaFactory";
9 |
10 | const TexArea = Input.TextArea;
11 |
12 | @ComponentWrapper
13 | class TextAreaComponent extends ReactComponent, TextAreaProps, ReactComponentState> {
14 |
15 | override render() {
16 | const {definition: {props}} = this.props;
17 | return (
18 |
19 | )
20 | }
21 | }
22 |
23 | class TextAreaComponentEditor extends PropsEditor {
24 | doRender() {
25 | return (
26 | <>
27 | >
28 | );
29 | }
30 | }
31 |
32 | /**
33 | * 注册
34 | */
35 | FactoryRegister(TextAreaComponent, TextAreaComponentEditor)(TextAreaFactory)
36 |
37 |
--------------------------------------------------------------------------------
/antd-designer/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React, {PureComponent} from 'react';
2 | import {Button, Layout} from 'antd';
3 | import {createRoot} from 'react-dom/client';
4 | import FormDesigner from './designer/FormDesigner';
5 | import FormStudio from "../../src/FormStudio";
6 |
7 | require('@@/style/formView.less')
8 | const {Header} = Layout;
9 |
10 | class App extends PureComponent {
11 | getJsonData() {
12 | alert(JSON.stringify(FormStudio.getJsonData()));
13 | }
14 |
15 | override render() {
16 | return (
17 |
18 |
26 |
27 |
28 |
29 |
30 | );
31 | }
32 | }
33 |
34 | const domNode = createRoot(document.getElementsByTagName('body')[0])
35 | domNode.render()
36 |
--------------------------------------------------------------------------------
/antdv-designer/src/designer/installModule.ts:
--------------------------------------------------------------------------------
1 | import {FormHelper} from '@/designer/helper';
2 | import FormStudio from "@@/FormStudio";
3 |
4 | function installFactory(modules: __WebpackModuleApi.RequireContext) {
5 | modules.keys().forEach((key) => {
6 | const mod = modules(key);
7 | FormStudio.registerFactory(mod.default);
8 | });
9 | }
10 |
11 | function installDef(modules: __WebpackModuleApi.RequireContext) {
12 | modules.keys().forEach((key) => {
13 | const mod = modules(key);
14 | FormHelper.registerComponent(mod.default);
15 | });
16 | }
17 |
18 | function installEditor(modules: __WebpackModuleApi.RequireContext) {
19 | modules.keys().forEach((key) => {
20 | const mod = modules(key);
21 | FormHelper.registerEditor(mod.default);
22 | });
23 | }
24 |
25 |
26 | installFactory(require.context('@@/factory', true, /Factory.ts/))
27 | installDef(require.context('./components', true, /index.vue/))
28 | installDef(require.context('./layout', true, /index.vue/))
29 | installEditor(require.context('./components', true, /PropEditor.vue/))
30 | installEditor(require.context('./layout', true, /PropEditor.vue/))
31 |
32 | export default function install(modules: string[]) {
33 | console.log(modules);
34 | }
35 |
--------------------------------------------------------------------------------
/antdv-designer/src/designer/components/select/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
44 |
45 |
47 |
--------------------------------------------------------------------------------
/antdv-designer/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
11 |
39 |
51 |
--------------------------------------------------------------------------------
/antdv-designer/src/designer/wrapper/ComponentWrapper.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | {{ title }}
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
52 |
53 |
56 |
--------------------------------------------------------------------------------
/antdv-designer/src/designer/components/checkbox/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
45 |
46 |
48 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "form-designer",
3 | "version": "1.0.0",
4 | "description": "",
5 | "scripts": {
6 | "start": "npx webpack serve"
7 | },
8 | "dependencies": {
9 | },
10 | "devDependencies": {
11 | "@babel/core": "^7.4.3",
12 | "@babel/plugin-proposal-class-properties": "^7.4.0",
13 | "@babel/plugin-proposal-decorators": "^7.4.0",
14 | "@babel/plugin-syntax-dynamic-import": "^7.2.0",
15 | "@babel/plugin-transform-react-jsx": "^7.3.0",
16 | "@babel/preset-env": "^7.4.3",
17 | "@types/jquery": "^3.3.6",
18 | "@types/jqueryui": "^1.12.3",
19 | "@types/node": "^18.15.3",
20 | "@types/webpack": "^5.28.0",
21 | "@types/webpack-env": "^1.18.0",
22 | "babel-loader": "^8.0.5",
23 | "css-loader": "^6.7.3",
24 | "html-webpack-plugin": "^5.5.0",
25 | "less": "^3.9.0",
26 | "less-loader": "^11.1.0",
27 | "mini-css-extract-plugin": "^2.7.3",
28 | "postcss-loader": "^3.0.0",
29 | "style-loader": "^0.23.1",
30 | "ts-node": "^10.8.2",
31 | "ts-loader": "^9.4.2",
32 | "typescript": "~4.9.3",
33 | "webpack": "^5.76.1",
34 | "@webpack-cli/serve": "^2.0.1",
35 | "webpack-dev-server": "^4.11.1",
36 | "webpack-cli": "^5.0.1",
37 | "eslint": "^8.37.0",
38 | "eslint-plugin-import": "^2.27.5",
39 | "webpack-merge": "^5.8.0",
40 | "dotenv": "^16.0.3",
41 | "cross-env": "^5.2.0"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/antd-designer/src/designer/components/CheckboxFactory.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Checkbox} from 'antd';
3 | import {ReactComponent} from '../ReactComponent';
4 | import {ComponentWrapper, FactoryRegister} from '../wrapper';
5 | import {CheckboxProps} from "../../../../src/props";
6 | import {ReactComponentProps, ReactComponentState} from "../types";
7 | import {OptionGroupEditor} from "../widgets/OptionGroupEditor";
8 | import CheckboxFactory from "@@/factory/CheckboxFactory";
9 |
10 | const CheckboxGroup = Checkbox.Group;
11 |
12 | @ComponentWrapper
13 | class CheckboxComponent extends ReactComponent, CheckboxProps, ReactComponentState> {
14 |
15 | override render() {
16 | const definition = this.props.definition;
17 | const props: CheckboxProps = definition.props!;
18 | const defaultValue = props.options.filter(item => {
19 | return item.checked;
20 | }).map(item => {
21 | return item.value;
22 | });
23 |
24 | return (
25 |
26 | )
27 | }
28 | }
29 |
30 | class CheckboxComponentEditor extends OptionGroupEditor {
31 |
32 | doRender() {
33 | return (
34 | <>
35 | {this.renderOptions()}
36 | >
37 | );
38 | }
39 | }
40 |
41 | FactoryRegister(CheckboxComponent, CheckboxComponentEditor)(CheckboxFactory)
42 |
--------------------------------------------------------------------------------
/antdv-designer/src/VueComponents.ts:
--------------------------------------------------------------------------------
1 | import type {ComponentDefinition} from "@@/types";
2 | import {ComponentInternalInstance, DefineComponent} from "@vue/runtime-core";
3 | import {ComponentPublicInstance} from "vue";
4 |
5 |
6 | export type VueComponents = {
7 |
8 | /**
9 | * shouldComponentUpdate 默认不是对象的深层比较,采用标志位的方式
10 | * 此方法代替foreUpdate
11 | */
12 | forceRender(): void;
13 | } & ComponentPublicInstance;
14 |
15 |
16 | export interface ComponentGroupMethods {
17 |
18 | /**
19 | * 添加子组件
20 | * @param index
21 | * @param componentDefinition
22 | */
23 | addChildBySlot(slot: number, componentDefinition: ComponentDefinition): void;
24 |
25 | /**
26 | * 移除子组件
27 | * @param index
28 | */
29 | removeChildBySlot(slot: number): void;
30 |
31 | /**
32 | * 移除子组件
33 | * @param index
34 | */
35 | removeChild(index: number): void;
36 |
37 | addChild(index: number, componentDefinition: ComponentDefinition): void;
38 |
39 | changeIndex(source: number, target: number): void;
40 |
41 | /**
42 | * 获取插槽上的组件
43 | * @param index
44 | * @param componentDefinition
45 | */
46 | getChildBySlot(slot: number): ComponentDefinition | null;
47 |
48 | forceRender(): void;
49 |
50 | getComponent(type: string): DefineComponent;
51 |
52 | }
53 |
54 | export type ComponentGroup = {} & VueComponents & ComponentGroupMethods;
55 |
56 | export type Layout = ComponentGroup;
57 |
--------------------------------------------------------------------------------
/antdv-designer/src/designer/FormView.vue:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
15 |
52 |
53 |
56 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | BSD 3-Clause License
2 |
3 | Copyright (c) 2019, lumengwei
4 | All rights reserved.
5 |
6 | Redistribution and use in source and binary forms, with or without
7 | modification, are permitted provided that the following conditions are met:
8 |
9 | 1. Redistributions of source code must retain the above copyright notice, this
10 | list of conditions and the following disclaimer.
11 |
12 | 2. Redistributions in binary form must reproduce the above copyright notice,
13 | this list of conditions and the following disclaimer in the documentation
14 | and/or other materials provided with the distribution.
15 |
16 | 3. Neither the name of the copyright holder nor the names of its
17 | contributors may be used to endorse or promote products derived from
18 | this software without specific prior written permission.
19 |
20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30 |
--------------------------------------------------------------------------------
/antdv-designer/webpack.config.ts:
--------------------------------------------------------------------------------
1 | import {Configuration, DefinePlugin} from "webpack";
2 | import defaultConfig from '../config/webpack.base.config'
3 |
4 | const {merge} = require('webpack-merge');
5 |
6 | const {VueLoaderPlugin} = require('vue-loader');
7 | const path = require('path');
8 |
9 | const config: Configuration = {
10 | entry: './src/main.ts',
11 | output: {
12 | path: path.resolve(__dirname, 'dist'),
13 | filename: 'foo.bundle.js',
14 | },
15 | // stats: 'verbose',
16 | stats: {
17 | // warnings: false,
18 | logging: 'error',
19 | errorDetails: true
20 | },
21 | resolve: {
22 | alias: {
23 | '@': path.resolve(__dirname, 'src')
24 | }
25 | },
26 | module: {
27 | parser: {
28 | javascript: {
29 | exportsPresence: 'auto',
30 | },
31 | },
32 | rules: [
33 | {
34 | test: /\.vue$/,
35 | use: [{
36 | loader: 'vue-loader',
37 | options: {
38 | reactivityTransform: false
39 | }
40 | }],
41 |
42 | }
43 | ]
44 | },
45 | plugins: [
46 | new VueLoaderPlugin(),
47 | new DefinePlugin({
48 | __VUE_PROD_DEVTOOLS__: process.env.NODE_ENV == 'development',
49 | __VUE_OPTIONS_API__: process.env.NODE_ENV == 'development',
50 | }),
51 | ]
52 | };
53 |
54 | const finalConfig = merge(defaultConfig, config);
55 |
56 | console.log(finalConfig)
57 | export default finalConfig;
58 |
--------------------------------------------------------------------------------
/antd-designer/src/designer/helper/FormHelper.ts:
--------------------------------------------------------------------------------
1 | import {Activatable, ReactComponentProps} from "../types";
2 | import ComponentPropsEditor from "../ComponentPropsEditor";
3 | import {ComponentFactory} from "../../../../src/types";
4 | import {ComponentSpec} from "react";
5 |
6 | class FormHelper {
7 | private _activeComponentIns: Activatable & ComponentSpec, any> | null = null;
8 | private _formEditorIns: ComponentPropsEditor | null = null;
9 | private _componentFactory: ComponentFactory | null = null;
10 | private _formView: ComponentSpec | null = null;
11 |
12 | get activeComponentIns() {
13 | return this._activeComponentIns;
14 | }
15 |
16 | set activeComponentIns(inst) {
17 | this._activeComponentIns?.unActive();
18 | this._activeComponentIns = inst;
19 | this._activeComponentIns?.onActive();
20 | if (inst?.props) {
21 | this._formEditorIns?.setDefinition(inst?.props.definition)
22 | } else {
23 | this._formEditorIns?.setDefinition(null)
24 | }
25 | }
26 |
27 | get formEditorIns() {
28 | return this._formEditorIns;
29 | }
30 |
31 | set formEditorIns(inst) {
32 | this._formEditorIns = inst;
33 | }
34 |
35 |
36 | get componentFactory() {
37 | return this._componentFactory;
38 | }
39 |
40 | set componentFactory(inst) {
41 | this._componentFactory = inst;
42 | }
43 |
44 | get formView() {
45 | return this._formView;
46 | }
47 |
48 | set formView(inst) {
49 | this._formView = inst;
50 | }
51 |
52 | }
53 |
54 | export default new FormHelper();
55 |
--------------------------------------------------------------------------------
/antd-designer/src/designer/components/RadioFactory.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Radio,} from 'antd';
3 | import {ComponentWrapper, FactoryRegister} from '../wrapper';
4 | import {ReactComponent} from '../ReactComponent';
5 | import {RadioProps} from "../../../../src/props";
6 | import {ReactComponentProps, ReactComponentState} from "../types";
7 | import {FactoryGroup, FieldFactory, FieldType} from "../../../../src/types";
8 | import {makeComponentId, makeFieldId} from "../../../../src/utils";
9 | import {OptionGroupEditor} from "../widgets/OptionGroupEditor";
10 | import RadioFactory from "@@/factory/RadioFactory";
11 |
12 |
13 | @ComponentWrapper
14 | class RadioComponent extends ReactComponent, RadioProps, ReactComponentState> {
15 |
16 | override render() {
17 | const definition = this.props.definition;
18 | const props: RadioProps = definition.props!;
19 | const defaultValue = props.options.filter(item => {
20 | return item.checked;
21 | }).map(item => {
22 | return item.value;
23 | })[0] || null;
24 |
25 | const opts = props.options.map(it => {
26 | return ({it.label})
27 | })
28 | return (
29 |
30 | {opts}
31 |
32 | )
33 | }
34 | }
35 |
36 | class RadioComponentEditor extends OptionGroupEditor {
37 |
38 | doRender() {
39 | return (
40 | <>
41 | {this.renderOptions()}
42 | >
43 | );
44 | }
45 | }
46 |
47 | FactoryRegister(RadioComponent, RadioComponentEditor)(RadioFactory)
48 |
49 |
--------------------------------------------------------------------------------
/antdv-designer/src/designer/layout/column/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | |
11 |
12 |
13 |
14 |
53 |
54 |
72 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: '@typescript-eslint/parser',
3 | extends: [
4 | 'airbnb',
5 | 'prettier',
6 | 'plugin:compat/recommended',
7 | 'plugin:vue/vue3-recommended',
8 | 'eslint:recommended'
9 | ],
10 | plugins: ['@typescript-eslint', 'import'],
11 | env: {
12 | browser: true,
13 | node: true,
14 | es6: true,
15 | mocha: true,
16 | jest: true,
17 | jasmine: true,
18 | },
19 | globals: {
20 | APP_TYPE: true,
21 | page: true,
22 | },
23 | rules: {
24 | 'react/jsx-filename-extension': [1, {extensions: ['.js']}],
25 | 'react/jsx-wrap-multilines': 0,
26 | 'react/prop-types': 0,
27 | 'react/forbid-prop-types': 0,
28 | 'react/jsx-one-expression-per-line': 0,
29 | 'import/no-unresolved': [2, {ignore: ['^@/', '^@@/']}],
30 | 'import/no-extraneous-dependencies': [
31 | 2,
32 | {
33 | optionalDependencies: true,
34 | devDependencies: ['**/tests/**.js', '/mock/**/**.js', '**/**.test.js'],
35 | },
36 | ],
37 | 'jsx-a11y/no-noninteractive-element-interactions': 0,
38 | 'jsx-a11y/click-events-have-key-events': 0,
39 | 'jsx-a11y/no-static-element-interactions': 0,
40 | 'jsx-a11y/anchor-is-valid': 0,
41 | 'linebreak-style': 0,
42 | },
43 | settings: {
44 | polyfills: ['fetch', 'promises', 'url', 'object-assign'],
45 | "import/resolver": {
46 | alias: {
47 | map: [
48 | ['@/', './src/']
49 | ],
50 | extensions: ['.ts', '.js', '.jsx', '.vue', '.tsx']
51 | },
52 | },
53 | },
54 | };
55 |
--------------------------------------------------------------------------------
/antd-designer/src/designer/ComponentPropsEditor.tsx:
--------------------------------------------------------------------------------
1 | import React, {PureComponent} from 'react'
2 | import {FactoryRenders, FormHelper} from "./helper";
3 | import {ReactComponentGroupState} from "./types";
4 | import {ComponentDefinition} from "../../../src/types";
5 |
6 | require('../../../src/style/formView.less');
7 |
8 | /**
9 | * 属性编辑器
10 | */
11 | class ComponentPropsEditor extends PureComponent<{}, ReactComponentGroupState> {
12 | constructor(props: {}, context: any) {
13 | super(props, context);
14 | this.state = {
15 | definition: null,
16 | renderCounter: 0
17 | }
18 | }
19 |
20 | override componentDidMount() {
21 | FormHelper.formEditorIns = this;
22 | }
23 |
24 |
25 | setDefinition(definition: ComponentDefinition | null) {
26 | this.setState({
27 | definition
28 | })
29 | }
30 |
31 | onValuesChange(props: any, values: any, _: any) {
32 | /* setTimeout(() => {// validateFields 的调用放在最后
33 | props.form.validateFields((err, allValues) => {
34 | if (err) return;
35 |
36 | const next = this.componentEditor.onChange(values, allValues);
37 |
38 | if (this.componentIns && next !== false) {
39 | this.componentIns.forceRender();
40 | }
41 | })
42 | })*/
43 | }
44 |
45 |
46 | renderProps() {
47 | const {definition} = this.state;
48 | if (definition) {
49 | return FactoryRenders.getRender(definition.type).renderEditor(definition)({
50 | onValuesChange: this.onValuesChange.bind(this),
51 | });
52 | }
53 | }
54 |
55 | override render() {
56 | return (
57 |
58 | {this.renderProps()}
59 |
60 | );
61 | }
62 | }
63 |
64 | export default ComponentPropsEditor;
65 |
--------------------------------------------------------------------------------
/antd-designer/src/designer/components/RateFactory.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Form, InputNumber, Rate} from 'antd';
3 | import {ComponentWrapper, FactoryRegister} from '../wrapper';
4 | import {ReactComponent} from '../ReactComponent';
5 | import {PropsEditor} from '../widgets/PropsEditor';
6 | import {RateProps} from "../../../../src/props";
7 | import {FactoryGroup, FieldFactory, FieldType} from "../../../../src/types";
8 | import {ReactComponentProps, ReactComponentState} from "../types";
9 | import {makeComponentId, makeFieldId} from "../../../../src/utils";
10 | import RateFactory from "@@/factory/RateFactory";
11 |
12 | @ComponentWrapper
13 | class RateComponent extends ReactComponent, RateProps, ReactComponentState> {
14 |
15 | override render() {
16 | const {definition: {props}} = this.props;
17 | return (
18 | <>
19 |
22 | >
23 |
24 | )
25 | }
26 | }
27 |
28 | class RateComponentEditor extends PropsEditor {
29 | doRender() {
30 | const definition = this.props.definition;
31 | const props: RateProps = definition.props!;
32 | return (
33 |
46 |
47 |
48 | );
49 | }
50 | }
51 |
52 | FactoryRegister(RateComponent, RateComponentEditor)(RateFactory)
53 |
54 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Form Designer
2 |
3 |
4 |
5 | 基于
Ant Design
6 | 和
jQuery UI 的表单设计器
7 |
8 | 
9 |
10 |
11 |
12 | ### 特性
13 |
14 | - [x] React
15 | - [x] Vue 3.x
16 | - [x] Typescript
17 | - [x] 统一的组件定义,对Vue 和React 的实现提供一个统一的组件定义描述
18 |
19 | ### 概念
20 |
21 | - Component 组件
22 | - Layout 布局,一种特殊的Component
23 | - Component Editor 组件属性编辑器
24 | - Component Factory 提供组件定义
25 |
26 | ### 扩展组件
27 |
28 | #### 定义组件属性类型
29 |
30 | 在src/props.ts 文件中定义组件属性
31 |
32 | ```
33 | export type RateProps = {
34 | count: number
35 | }
36 |
37 | ```
38 |
39 | #### 定义一个ComponentFactory
40 |
41 | 在src/factory目录下定义。 提供了makeFieldId()和makeComponentId()两个方法用于生成id
42 |
43 | ```
44 | class RateFactory implements FieldFactory {
45 | readonly type = "Rate"
46 | readonly group = FactoryGroup.Component;
47 | title = "评分"
48 |
49 | /**
50 | * 初始化一个组件定义
51 | * @returns {{type: string, title: string}}
52 | */
53 | createComponentDefinition() {
54 | return {
55 | id: makeComponentId(),
56 | type: this.type,
57 | title: this.title,
58 | fieldDef: {
59 | fieldId: makeFieldId(),
60 | fieldType: 'varchar' as FieldType,
61 | fieldName: '',
62 | },
63 | props: {
64 | count: 5
65 | },
66 | }
67 | }
68 | }
69 |
70 | export default new RateFactory();
71 | ```
72 | #### 创建一个vue组件或布局
73 |
74 | - 在src/antdv-designer/src/designer/components目录下添加vue组件
75 | - 在src/antdv-designer/src/designer/layout目录下添加vue布局
76 |
77 |
78 | #### 创建一个React组件或布局
79 |
80 | - 在src/antd-designer/src/designer/components目录下添加React组件
81 | - 在src/antd-designer/src/designer/layout目录下添加React布局
82 |
83 |
84 |
--------------------------------------------------------------------------------
/antd-designer/src/designer/wrapper/index.tsx:
--------------------------------------------------------------------------------
1 | import React, {ComponentClass} from 'react';
2 | import {ComponentDefinition, ComponentFactory, ComponentFactoryConstructor} from "../../../../src/types";
3 | import FormStudio from "../../../../src/FormStudio";
4 | import FactoryRenders from "../helper/FactoryRenders";
5 | import {ComponentFactoryRender, ReactComponentProps} from "../types";
6 |
7 |
8 | /**
9 | * 注册ComponentFactory
10 | * @param Factory
11 | * @constructor
12 | */
13 | export function FactoryRegister, T>(Component: ComponentClass
14 | , ComponentEditor?: ComponentClass>) {
15 |
16 | return function FactoryWrapper(factory: ComponentFactory) {
17 | const render: ComponentFactoryRender = {
18 | renderComponent(componentDefinition: ComponentDefinition) {
19 |
20 | return function (props: any) {
21 | return React.createElement(Component, {
22 | ...props,
23 | definition: componentDefinition
24 | })
25 | }
26 | },
27 | renderEditor(componentDefinition: ComponentDefinition) {
28 | return function (props: any) {
29 | if (ComponentEditor) {
30 | return React.createElement(ComponentEditor, {
31 | ...props,
32 | definition: componentDefinition
33 | });
34 | } else {
35 | return (<>>);
36 | }
37 | }
38 | }
39 | }
40 |
41 | //const factory: ComponentFactory = new Factory();
42 | FactoryRenders.register(render, factory.type)
43 | FormStudio.registerFactory(factory)
44 | }
45 | }
46 |
47 | export const LayoutWrapper = require('./LayoutWrapper').default;
48 | export const ComponentWrapper = require('./ComponentWrapper').default;
49 |
--------------------------------------------------------------------------------
/src/FormStudio.ts:
--------------------------------------------------------------------------------
1 | import {ComponentDefinition, ComponentFactory, FactoryGroup, FormDefinition} from './types';
2 |
3 |
4 | class FormStudio {
5 | private factoryList: ComponentFactory[] = [];
6 | private factoryStore = new Map>();
7 | private _definition: ComponentDefinition | null = null;
8 | private _formDef: FormDefinition = {
9 | title: '',
10 | description: '',
11 | width: ''
12 | };
13 |
14 |
15 | /**
16 | * 注册组件工厂
17 | * @param factory
18 | */
19 | registerFactory(factory: ComponentFactory) {
20 | if (this.factoryStore.has(factory.type)) {
21 | console.log(`[warn] factory ${factory.type} is exist, ignore`);
22 | return;
23 | }
24 |
25 | this.factoryStore.set(factory.type, factory);
26 | this.factoryList.push(factory);
27 |
28 | if (factory.type == 'LinearLayout') {
29 | this._definition = factory.createComponentDefinition() as ComponentDefinition;
30 | }
31 | }
32 |
33 | getFactoryList(group: FactoryGroup): ComponentFactory[] {
34 | return this.factoryList.filter(it => it.group == group)
35 | }
36 |
37 | /**
38 | * 根据类型获取工厂
39 | * @param factoryType
40 | * @returns {*}
41 | */
42 | getFactory(factoryType: string): ComponentFactory {
43 | return this.factoryStore.get(factoryType) as ComponentFactory;
44 | }
45 |
46 | factoryFilter(filter: (it: ComponentFactory) => boolean) {
47 | return this.factoryList.filter(filter);
48 | }
49 |
50 | get definition() {
51 | return this._definition;
52 | }
53 |
54 | get formDef() {
55 | return this._formDef;
56 | }
57 |
58 |
59 | getJsonData() {
60 | return {
61 | props: JSON.parse(JSON.stringify(this._definition)),
62 | form: JSON.parse(JSON.stringify(this._formDef)),
63 | };
64 | }
65 | }
66 |
67 | export default new FormStudio();
68 |
--------------------------------------------------------------------------------
/antdv-designer/src/designer/layout/linear/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
11 |
12 |
64 |
65 |
71 |
--------------------------------------------------------------------------------
/antd-designer/src/designer/FormView.tsx:
--------------------------------------------------------------------------------
1 | import React, {PureComponent, ReactElement} from 'react'
2 | import {FormDefinition} from "../../../src/types";
3 | import FormStudio from "../../../src/FormStudio";
4 | import {FactoryRenders, FormHelper} from "./helper";
5 | import {ReactComponentState} from "./types";
6 |
7 | require('../../../src/style/formView.less');
8 |
9 | type FormViewProps = {
10 | formDefinition: FormDefinition
11 | }
12 | export default class FormView extends PureComponent {
13 |
14 | private LinearLayout: ReactElement | null = null;
15 |
16 |
17 | constructor(props: FormViewProps, context: any) {
18 | super(props, context);
19 | this.state = {
20 | renderCounter: 0
21 | }
22 | }
23 |
24 | /**
25 | * shouldComponentUpdate 默认不是对象的深层比较,采用标志位的方式
26 | * 此方法代替foreUpdate
27 | */
28 | forceRender() {
29 | const {renderCounter} = this.state;
30 | this.setState({
31 | renderCounter: (renderCounter || 0) + 1
32 | });
33 | }
34 |
35 | override componentWillMount() {
36 | const componentRender = FactoryRenders.getRender("LinearLayout");
37 | this.LinearLayout = componentRender.renderComponent(FormStudio.definition!)({
38 | focusAble: false,
39 | toolbarAble: false
40 | });
41 |
42 | FormHelper.formView = this;
43 | }
44 |
45 | override render() {
46 | const {formDefinition} = this.props;
47 |
48 | return (
49 |
50 |
51 |
{formDefinition.title}
52 |
{formDefinition.description}
53 |
54 |
57 | {this.LinearLayout}
58 |
59 |
60 | );
61 | }
62 | }
63 |
64 |
--------------------------------------------------------------------------------
/antd-designer/src/designer/components/SelectFactory.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Select} from 'antd';
3 | import {ComponentWrapper, FactoryRegister} from '../wrapper';
4 | import {ReactComponent} from '../ReactComponent';
5 | import {SelectProps} from "../../../../src/props";
6 | import {ReactComponentProps, ReactComponentState} from "../types";
7 | import {FactoryGroup, FieldFactory, FieldType} from "../../../../src/types";
8 | import {makeComponentId, makeFieldId} from "../../../../src/utils";
9 | import {OptionGroupEditor} from "../widgets/OptionGroupEditor";
10 | import SelectFactory from "@@/factory/SelectFactory";
11 |
12 | @ComponentWrapper
13 | class SelectComponent extends ReactComponent, SelectProps, ReactComponentState> {
14 |
15 | renderOptions() {
16 | const {definition: {props}} = this.props;
17 | return props!.options.map(item => {
18 | return {item.label}
19 | })
20 | }
21 |
22 | override render() {
23 | const {definition: {props}} = this.props;
24 | const defaultValue = props!.options.filter(item => {
25 | return item.checked;
26 | }).map(item => {
27 | return item.value;
28 | });
29 |
30 | return (
31 |
41 | )
42 | }
43 | }
44 |
45 | class SelectComponentEditor extends OptionGroupEditor {
46 |
47 | doRender() {
48 | return (
49 | <>
50 | {this.renderOptions()}
51 | >
52 | );
53 | }
54 | }
55 |
56 | FactoryRegister(SelectComponent, SelectComponentEditor)(SelectFactory)
57 |
58 |
--------------------------------------------------------------------------------
/antdv-designer/src/designer/wrapper/ComponentRender.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
17 |
18 |
19 |
20 |
21 |
82 |
83 |
86 |
--------------------------------------------------------------------------------
/antd-designer/src/designer/layout/LinearLayoutFactory.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {sortable} from '../../lib/sortable';
3 | import {Layout} from '../ReactComponent';
4 | import {FactoryRegister, LayoutWrapper} from '../wrapper';
5 | import {ComponentDefinition, ComponentFactory, FactoryGroup} from "../../../../src/types";
6 | import {LinearLayoutProps} from "../../../../src/props";
7 | import {ReactComponentGroupState, ReactComponentProps} from "../types";
8 | import FactoryRenders from "../helper/FactoryRenders";
9 | import className from "classnames";
10 | import {makeComponentId} from "../../../../src/utils";
11 | import LinearLayoutFactory from "@@/factory/LinearLayoutFactory";
12 |
13 | /**
14 | * 这是一个特殊的布局
15 | */
16 | @LayoutWrapper({
17 | focusAble: true,
18 | toolbarAble: true,
19 | layoutStyle: {
20 | display: 'flex',
21 | position: 'relative',
22 | width: '100%',
23 | minHeight: '50px',
24 | }
25 | })
26 | class LinearLayout extends Layout,
27 | LinearLayoutProps
28 | , ReactComponentGroupState> {
29 |
30 | private _node: HTMLElement | null = null;
31 |
32 |
33 | override componentDidMount() {
34 | sortable(this._node, this);
35 | }
36 |
37 | ref = (node: HTMLElement | null) => {
38 | this._node = node;
39 | }
40 |
41 | renderChildren() {
42 | const definition: ComponentDefinition = this.props.definition;
43 | return definition.children!.map((item, index) => {
44 | const factory = FactoryRenders.getRender(item.type);
45 | return factory.renderComponent(item)({
46 | key: `children-${index}`,
47 | onRemove: () => {
48 | this.removeChild(index)
49 | }
50 | });
51 | });
52 | }
53 |
54 | override render() {
55 | const {definition} = this.props;
56 | return (
57 |
62 | {this.renderChildren()}
63 |
64 | )
65 | }
66 | }
67 |
68 | FactoryRegister(LinearLayout)(LinearLayoutFactory)
69 |
70 |
--------------------------------------------------------------------------------
/antdv-designer/src/designer/wrapper/LayoutWrapper.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
64 |
65 |
101 |
--------------------------------------------------------------------------------
/antdv-designer/src/designer/FormPropsEditor.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
14 |
18 |
19 |
20 |
24 |
28 |
29 |
30 |
31 |
32 |
87 |
88 |
91 |
--------------------------------------------------------------------------------
/antdv-designer/src/designer/FormDesigner.vue:
--------------------------------------------------------------------------------
1 |
2 |
31 |
32 |
33 |
75 |
76 |
80 |
--------------------------------------------------------------------------------
/antd-designer/src/designer/FormPropsEditor.tsx:
--------------------------------------------------------------------------------
1 | import React, {PureComponent} from 'react'
2 | import {FormHelper} from "./helper";
3 | import {ReactComponentGroupState} from "./types";
4 | import {FormDefinition} from "../../../src/types";
5 | import {Form, FormInstance, Input, InputNumber} from "antd";
6 | import {mergeObject} from "../../../src/utils";
7 | import TextArea from "antd/es/input/TextArea";
8 |
9 | require('../../../src/style/formView.less');
10 |
11 | type FormPropsEditorProps = {
12 | definition: FormDefinition;
13 | }
14 |
15 | /**
16 | * 属性编辑器
17 | */
18 | class FormPropsEditor extends PureComponent> {
19 |
20 | private readonly refForm = React.createRef();
21 |
22 |
23 | onValuesChange(changedValues: any) {
24 | console.log('onValuesChange', changedValues)
25 | const {definition} = this.props;
26 | for (const field of Object.keys(changedValues)) {
27 | mergeObject(field, changedValues[field], definition);
28 | }
29 |
30 | FormHelper.formView!.forceRender();
31 | }
32 |
33 | override render() {
34 | const {definition} = this.props;
35 | return (
36 |
37 |
51 |
52 |
53 |
58 |
59 |
60 |
65 |
66 |
67 |
68 |
69 | );
70 | }
71 | }
72 |
73 | export default FormPropsEditor;
74 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | export interface Component {
2 | title: string;
3 | readonly type: string;
4 | }
5 |
6 | export interface FormDefinition {
7 | title: string;
8 | description: string;
9 | width: string;
10 | }
11 |
12 |
13 | /**
14 | * 字段定义
15 | */
16 | export type FieldDefinition = {
17 | fieldId: string;
18 | fieldName: string;
19 | fieldType: FieldType;
20 | length?: number,
21 | scale?: number
22 | }
23 |
24 | /**
25 | * 组件定义
26 | */
27 | export interface ComponentDefinition extends Component {
28 | /**
29 | * 作为一个子组件时,所在父组件中的位置(这个数据不一定在父组件中起作用)
30 | */
31 | id: string;
32 | slot?: number;
33 | props?: T;
34 | children?: ComponentDefinition[];
35 | title: string;
36 | fieldDef?: FieldDefinition;
37 | rules?: FieldRule[];
38 | }
39 |
40 |
41 | export enum FactoryGroup {
42 | 'Layout' = '布局组件',
43 | 'Component' = '表单组件'
44 | }
45 |
46 | /**
47 | * 组件工厂
48 | * 负责创建组件的定义
49 | */
50 | export interface ComponentFactory extends Component {
51 | readonly group: FactoryGroup;
52 |
53 | createComponentDefinition: () => ComponentDefinition;
54 | }
55 |
56 |
57 | /**
58 | * 组件工厂
59 | * 负责创建组件的定义
60 | */
61 | export interface FieldFactory extends ComponentFactory {
62 | readonly group: FactoryGroup;
63 |
64 | createComponentDefinition: () => ComponentDefinition & { fieldDef: FieldDefinition };
65 | }
66 |
67 |
68 | export interface ComponentFactoryConstructor {
69 | new(): ComponentFactory;
70 | }
71 |
72 | export const FieldTypes: { [key: string]: string } = {
73 | 'string': 'STRING',
74 | 'timestamp': 'TIMESTAMP',
75 | 'date': 'DATE',
76 | 'varchar': 'VARCHAR',
77 | 'decimal': 'DECIMAL',
78 | 'int8': 'INT8',
79 | 'int16': 'INT16',
80 | 'int32': 'INT32',
81 | 'int64': 'INT64',
82 | 'boolean': 'BOOLEAN',
83 | 'array': 'ARRAY'
84 | };
85 |
86 | export type FieldType =
87 | 'string'
88 | | 'timestamp'
89 | | 'date'
90 | | 'varchar'
91 | | 'decimal'
92 | | 'int8'
93 | | 'int16'
94 | | 'int32'
95 | | 'int64'
96 | | 'boolean'
97 | | 'array';
98 |
99 | export type FieldRuleType =
100 | 'string'
101 | | 'number'
102 | | 'boolean'
103 | | 'method'
104 | | 'regexp'
105 | | 'integer'
106 | | 'float'
107 | | 'object'
108 | | 'enum'
109 | | 'date'
110 | | 'url'
111 | | 'hex'
112 | | 'email'
113 | ;
114 |
115 | export interface FieldRule {
116 | type?: FieldRuleType;
117 | len?: number;
118 | max?: number;
119 | min: number;
120 | message: string;
121 | }
122 |
--------------------------------------------------------------------------------
/antdv-designer/src/designer/helper/FormHelper.ts:
--------------------------------------------------------------------------------
1 | import {ComponentFactory} from "@@/types";
2 | import {DefineComponent} from "@vue/runtime-core";
3 |
4 | class FormHelper {
5 | private _activeComponentIns: any | null = null;
6 | private _formEditorIns: any | null = null;
7 | private _formView: any | null = null;
8 | private _componentFactory: ComponentFactory | null = null;
9 | private _componentDefine: Map = new Map();
10 | private _editorDefine: Map = new Map();
11 | private _activeComponentId: string | null = null;
12 |
13 |
14 | get activeComponentIns() {
15 | return this._activeComponentIns;
16 | }
17 |
18 | set activeComponentIns(inst) {
19 | // 取消原有选中状态
20 | this._activeComponentId = null;
21 | if (this._activeComponentIns) {
22 | this._activeComponentIns.$forceUpdate();
23 | }
24 | this._activeComponentIns = inst;
25 | if (inst?.$props) {
26 | this._activeComponentId = inst?.$props.definition.id;
27 | this._formEditorIns?.setDefinition(inst?.$props.definition)
28 | } else {
29 | this._formEditorIns?.setDefinition(null)
30 | }
31 |
32 | // 刷新现有的选中状态
33 | if (this._activeComponentIns) {
34 | this._activeComponentIns.$forceUpdate();
35 | }
36 | }
37 |
38 | get activeComponentId() {
39 | return this._activeComponentId;
40 | }
41 |
42 | set activeComponentId(id) {
43 | this._activeComponentId = id;
44 | }
45 |
46 | get formEditorIns() {
47 | return this._formEditorIns;
48 | }
49 |
50 | set formEditorIns(inst) {
51 | this._formEditorIns = inst;
52 | }
53 |
54 | get formView() {
55 | return this._formView;
56 | }
57 |
58 | set formView(inst) {
59 | this._formView = inst;
60 | }
61 |
62 | get componentFactory() {
63 | return this._componentFactory;
64 | }
65 |
66 | set componentFactory(inst) {
67 | this._componentFactory = inst;
68 | }
69 |
70 | registerComponent(defineComponent: DefineComponent) {
71 | this._componentDefine.set(defineComponent.name, defineComponent);
72 | }
73 |
74 | getComponent(type: string): DefineComponent {
75 | return this._componentDefine.get(type) as DefineComponent;
76 | }
77 |
78 | registerEditor(defineComponent: DefineComponent) {
79 | this._editorDefine.set(defineComponent.name, defineComponent);
80 | }
81 |
82 | getEditor(type: string): DefineComponent {
83 | return this._editorDefine.get(type) as DefineComponent;
84 | }
85 | }
86 |
87 | export default new FormHelper();
88 |
--------------------------------------------------------------------------------
/antd-designer/src/designer/FormDesigner.tsx:
--------------------------------------------------------------------------------
1 | import React, {PureComponent} from 'react'
2 | import {Tabs} from 'antd';
3 | import FormView from './FormView';
4 | import componentStyle from '@@/style/component.module.less';
5 | import ComponentPropsEditor from './ComponentPropsEditor';
6 | import FormStudio from "@@/FormStudio";
7 | import {Component, FactoryGroup} from "@@/types";
8 | import {draggable} from "@/lib/sortable";
9 | import FormPropsEditor from "./FormPropsEditor";
10 |
11 | require('./components');
12 |
13 | require('./layout');
14 |
15 | class FormComponent extends PureComponent {
16 |
17 | private _node: HTMLElement | null = null;
18 |
19 | ref = (node: HTMLElement | null) => {
20 | this._node = node;
21 | }
22 |
23 | override componentDidMount() {
24 | const {type} = this.props;
25 |
26 | draggable(this._node, type)
27 | }
28 |
29 | override render() {
30 | const {title} = this.props;
31 | return (
32 |
37 | {title}
38 |
39 | )
40 | }
41 | }
42 |
43 |
44 | class FormDesigner extends PureComponent {
45 |
46 | renderChild(group: FactoryGroup) {
47 | return FormStudio.getFactoryList(group).map(item => {
48 | return
49 | })
50 | }
51 |
52 | override render() {
53 | return (
54 |
55 |
56 |
57 |
58 |
59 | {this.renderChild(FactoryGroup.Component)}
60 |
61 |
62 |
63 |
64 | {this.renderChild(FactoryGroup.Layout)}
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 | );
85 | }
86 | }
87 |
88 | export default FormDesigner;
89 |
--------------------------------------------------------------------------------
/antd-designer/src/designer/ReactComponent.tsx:
--------------------------------------------------------------------------------
1 | import {PureComponent} from 'react';
2 | import {ReactComponentGroupState, ReactComponentProps, ReactComponentState} from "./types";
3 | import {ComponentDefinition} from "../../../src/types";
4 |
5 | export class ReactComponent, T, S extends ReactComponentState> extends PureComponent
{
6 | override componentWillMount() {
7 | const {definition: {type, title}} = this.props;
8 | if (!type || !title) {
9 | throw new Error(`type:${type}, title:${title} not defined in definition`);
10 | }
11 | }
12 |
13 | /**
14 | * shouldComponentUpdate 默认不是对象的深层比较,采用标志位的方式
15 | * 此方法代替foreUpdate
16 | */
17 | forceRender() {
18 | const {renderCounter} = this.state;
19 | this.setState({
20 | renderCounter: (renderCounter || 0) + 1
21 | })
22 | }
23 | }
24 |
25 | export class ComponentGroup
, T, S extends ReactComponentGroupState> extends ReactComponent {
26 |
27 | override componentWillMount() {
28 | super.componentWillMount();
29 | const {definition: {children}} = this.props;
30 | if (!children || !Array.isArray(children)) {
31 | throw new Error(`children:${children} not defined in definition`);
32 | }
33 | }
34 |
35 | /**
36 | * 添加子组件
37 | * @param index
38 | * @param componentDefinition
39 | */
40 | addChildBySlot(slot: number, componentDefinition: ComponentDefinition) {
41 | const {definition: {children}} = this.props;
42 | componentDefinition.slot = slot;
43 | children?.push(componentDefinition);
44 | this.forceUpdate()
45 | }
46 |
47 | /**
48 | * 移除子组件
49 | * @param index
50 | */
51 | removeChildBySlot(slot: number) {
52 | const {definition: {children}} = this.props;
53 | const child = children?.filter(it => it.slot == slot)[0]
54 | if (child) {
55 | children!.splice(children?.indexOf(child), 1)
56 | this.forceUpdate()
57 | }
58 | }
59 |
60 | /**
61 | * 移除子组件
62 | * @param index
63 | */
64 | removeChild(index: number) {
65 | const {definition: {children}} = this.props;
66 | children!.splice(index, 1)
67 | this.forceUpdate()
68 | }
69 |
70 | addChild(index: number, componentDefinition: ComponentDefinition) {
71 | const {definition: {children}} = this.props;
72 | children!.splice(index, 0, componentDefinition)
73 | this.forceUpdate();
74 | }
75 |
76 | changeIndex(source: number, target: number) {
77 | const {definition: {children}} = this.props;
78 | const s = children![source];
79 | children![source] = children![target];
80 | children![target] = s;
81 | this.forceUpdate();
82 | }
83 |
84 | /**
85 | * 获取插槽上的组件
86 | * @param index
87 | * @param componentDefinition
88 | */
89 | getChildBySlot(slot: number): ComponentDefinition | null {
90 | const {definition: {children}} = this.props;
91 | return children?.filter(it => it.slot == slot)[0] || null
92 | }
93 |
94 | }
95 |
96 | export class Layout, T, S extends ReactComponentGroupState> extends ComponentGroup {
97 |
98 | }
99 |
--------------------------------------------------------------------------------
/antdv-designer/src/lib/sortable.ts:
--------------------------------------------------------------------------------
1 | import $ from "./jquery";
2 | import {ReactComponentProps} from "../../../antd-designer/src/designer/types";
3 | import {FormHelper} from "@/designer/helper";
4 | import FormStudio from "@@/FormStudio";
5 | import {ComponentGroup} from "../VueComponents";
6 | import SortableUIParams = JQueryUI.SortableUIParams;
7 |
8 |
9 | /**
10 | *
11 | * @param $node
12 | * @param componentInst
13 | * @param slotIndex 插槽位置
14 | * @param limitSize 子元素最大个数
15 | * @param limitToDisable 如果子元素满额时,禁用sortable
16 | */
17 | export function sortable
, T>($node: HTMLElement | null
18 | , componentInst: ComponentGroup, slotIndex?: number, limitSize?: number, limitToDisable?: boolean
19 | ) {
20 | if ($node) {
21 | let sourceIndex = 0;
22 | $($node).sortable({
23 | connectWith: 'parent',
24 | placeholder: 'form-placeholder-filed ',
25 | cancel: '.j_cancel-drag',
26 | stop(e: JQueryEventObject, ui: SortableUIParams) {
27 | if ($($node).children().length > limitSize!) {
28 | $(ui.item).remove();
29 | return;
30 | } else {
31 | if (componentInst && FormHelper.componentFactory) {
32 | // 查润新的组件
33 | if (slotIndex === undefined) {
34 | componentInst.addChild($(ui.item).index(), FormHelper.componentFactory.createComponentDefinition())
35 | } else {
36 | componentInst.addChildBySlot(slotIndex, FormHelper.componentFactory.createComponentDefinition())
37 | }
38 | $(ui.item).remove();
39 |
40 | if (limitToDisable && $($node).children().length >= limitSize!) {
41 | $($node).sortable("disable")
42 | }
43 | } else {
44 | // 组件排序
45 | componentInst.changeIndex(sourceIndex, $(ui.item).index())
46 | }
47 |
48 | }
49 | },
50 | over(e: JQueryEventObject, ui: SortableUIParams) {
51 | if ($($node).children().length > limitSize!) {
52 | return;
53 | } else {
54 | $($node).find('.form-placeholder-filed').show();
55 | }
56 |
57 | },
58 | activate(e: JQueryEventObject, ui: SortableUIParams) {
59 | sourceIndex = $(ui.item).index();
60 | },
61 | out(e: JQueryEventObject, ui: SortableUIParams) {
62 | },
63 | receive(e: JQueryEventObject, ui: SortableUIParams) {
64 | },
65 | });
66 | } else {
67 | console.log($node)
68 | }
69 | }
70 |
71 |
72 | export function draggable(node: HTMLElement | null, factoryType: string) {
73 | if (node) {
74 | $(node).draggable({
75 | connectToSortable: ".ui-sortable",
76 | helper: "clone",
77 | opacity: .8,
78 | appendTo: "body",
79 | start() {
80 | FormHelper.componentFactory = FormStudio.getFactory(factoryType);
81 | },
82 | stop() {
83 | FormHelper.componentFactory = null;
84 | }
85 | }).disableSelection();
86 | } else {
87 | console.log(node)
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/antd-designer/src/lib/sortable.ts:
--------------------------------------------------------------------------------
1 | import $ from "./jquery";
2 | import {FormHelper} from "../designer/helper";
3 | import FormStudio from "../../../src/FormStudio";
4 | import {ComponentGroup} from "../designer/ReactComponent";
5 | import {ReactComponentGroupState, ReactComponentProps} from "../designer/types";
6 | import SortableUIParams = JQueryUI.SortableUIParams;
7 |
8 | /**
9 | *
10 | * @param $node
11 | * @param componentInst
12 | * @param slotIndex 插槽位置
13 | * @param limitSize 子元素最大个数
14 | * @param limitToDisable 如果子元素满额时,禁用sortable
15 | */
16 | export function sortable
, T, S extends ReactComponentGroupState>($node: HTMLElement | null
17 | , componentInst: ComponentGroup, slotIndex?: number, limitSize?: number, limitToDisable?: boolean
18 | ) {
19 | if ($node) {
20 | let sourceIndex = 0;
21 | $($node).sortable({
22 | connectWith: 'parent',
23 | placeholder: 'form-placeholder-filed ',
24 | cancel: '.j_cancel-drag',
25 | stop(e: JQueryEventObject, ui: SortableUIParams) {
26 | console.log('stop', e, ui)
27 | if ($($node).children().length > limitSize!) {
28 | $(ui.item).remove();
29 | return;
30 | } else {
31 | if (componentInst && FormHelper.componentFactory) {
32 | // 查润新的组件
33 | if (slotIndex === undefined) {
34 | componentInst.addChild($(ui.item).index(), FormHelper.componentFactory.createComponentDefinition())
35 | } else {
36 | componentInst.addChildBySlot(slotIndex, FormHelper.componentFactory.createComponentDefinition())
37 | }
38 | $(ui.item).remove();
39 |
40 | if (limitToDisable && $($node).children().length >= limitSize!) {
41 | $($node).sortable("disable")
42 | }
43 | } else {
44 | // 组件排序
45 | componentInst.changeIndex(sourceIndex, $(ui.item).index())
46 | }
47 |
48 | }
49 | },
50 | over(e: JQueryEventObject, ui: SortableUIParams) {
51 | if ($($node).children().length > limitSize!) {
52 | return;
53 | } else {
54 | $($node).find('.form-placeholder-filed').show();
55 | }
56 |
57 | },
58 | activate(e: JQueryEventObject, ui: SortableUIParams) {
59 | console.log('activate', e, ui)
60 | sourceIndex = $(ui.item).index();
61 | },
62 | out(e: JQueryEventObject, ui: SortableUIParams) {
63 | },
64 | receive(e: JQueryEventObject, ui: SortableUIParams) {
65 | },
66 | });
67 | } else {
68 | console.log($node)
69 | }
70 | }
71 |
72 |
73 | export function draggable(node: HTMLElement | null, factoryType: string) {
74 | if (node) {
75 | $(node).draggable({
76 | connectToSortable: ".ui-sortable",
77 | helper: "clone",
78 | opacity: .8,
79 | appendTo: "body",
80 | start() {
81 | FormHelper.componentFactory = FormStudio.getFactory(factoryType);
82 | },
83 | stop() {
84 | FormHelper.componentFactory = null;
85 | }
86 | }).disableSelection();
87 | } else {
88 | console.log(node)
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/antd-designer/src/designer/layout/ColumnLayoutFactory.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Form, InputNumber} from 'antd';
3 | import {Layout} from '../ReactComponent';
4 | import {FactoryRegister, LayoutWrapper} from '../wrapper';
5 | import FormStudio from "../../../../src/FormStudio";
6 | import {ColumnLayoutProps} from "../../../../src/props";
7 | import {ReactComponentGroupState, ReactComponentProps} from "../types";
8 | import FactoryRenders from "../helper/FactoryRenders";
9 | import {sortable} from "../../lib/sortable";
10 | import {PropsEditor} from "../widgets/PropsEditor";
11 | import ColumnLayoutFactory from "@@/factory/ColumnLayoutFactory";
12 |
13 |
14 | @LayoutWrapper()
15 | class ColumnLayout extends Layout, ColumnLayoutProps, ReactComponentGroupState> {
16 |
17 | renderColumns() {
18 |
19 | const {definition: {props, children}} = this.props;
20 |
21 | let es = props!.columnNum - children!.length;
22 | if (es !== 0) {
23 | if (es < 0) {
24 | es = Math.abs(es);
25 | children!.splice(children!.length - es, es);
26 | } else {
27 | while (es !== 0) {
28 | children!.splice(children!.length, 0);
29 | es -= 1;
30 | }
31 | }
32 | }
33 |
34 |
35 | const segments: any[] = [];
36 |
37 | for (let i = 0; i < props!.columnNum; i++) {
38 | const def = this.getChildBySlot(i);
39 | if (def) {
40 | const factory = FormStudio.getFactory(def.type)
41 | const render = FactoryRenders.getRender(def.type)
42 | segments.push((
43 | {render.renderComponent(factory.createComponentDefinition())({
44 | onRemove: () => {
45 | this.removeChildBySlot(i);
46 | }
47 | })}
48 |
))
49 | } else {
50 | const ref = (node: HTMLDivElement) => {
51 | if (node) {
52 | sortable(node, this, i, 1, true)
53 | }
54 | };
55 | segments.push(())
56 | }
57 |
58 | }
59 |
60 | return segments;
61 | }
62 |
63 | override render() {
64 | return (
65 | {this.renderColumns()}
66 | )
67 | }
68 | }
69 |
70 | class ColumnComponentEditor extends PropsEditor {
71 |
72 | doRender() {
73 | const {definition: {props}} = this.props;
74 | return (
75 | <>
76 |
91 |
92 |
93 | >);
94 | }
95 | }
96 |
97 | FactoryRegister(ColumnLayout, ColumnComponentEditor)(ColumnLayoutFactory)
98 |
99 |
100 |
--------------------------------------------------------------------------------
/antdv-designer/src/designer/helper/index.ts:
--------------------------------------------------------------------------------
1 | import formHelper from "./FormHelper";
2 | import {ComponentPublicInstance, getCurrentInstance} from "vue";
3 | import {ComponentGroup, ComponentGroupMethods} from "../../VueComponents";
4 | import {ComponentDefinition} from "@@/types";
5 | import {DefineComponent} from "@vue/runtime-core";
6 | import {VueComponentProps} from "@/designer/types";
7 |
8 | export const FormHelper = formHelper;
9 |
10 | export function useVueComponent() {
11 | const inst: ComponentPublicInstance | null = getCurrentInstance() as ComponentPublicInstance | null;
12 | return () => {
13 | if (inst) {
14 | inst.$forceUpdate();
15 | }
16 | }
17 | }
18 |
19 |
20 | export function useVueComponentGroup(): ComponentGroupMethods {
21 | const inst: ComponentGroup> | null = getCurrentInstance()?.proxy as ComponentGroup> | null;
22 |
23 | function addChildBySlot(slot: number, componentDefinition: ComponentDefinition) {
24 | const definition = inst?.$props.definition
25 | if (definition && definition.children) {
26 | componentDefinition.slot = slot;
27 | definition.children?.push(componentDefinition);
28 | inst.forceRender();
29 | } else {
30 | // TODO
31 | }
32 | }
33 |
34 | function removeChildBySlot(slot: number) {
35 | const definition = inst?.$props.definition;
36 | if (definition && definition.children) {
37 | const child = definition.children?.filter(it => it.slot == slot)[0]
38 | if (child) {
39 | definition.children!.splice(definition.children?.indexOf(child), 1)
40 | inst.forceRender();
41 | }
42 | } else {
43 | // TODO
44 | }
45 | }
46 |
47 | function removeChild(index: number) {
48 | const definition = inst?.$props.definition;
49 | if (definition && definition.children) {
50 | definition.children!.splice(index, 1);
51 | inst.forceRender();
52 | }
53 | }
54 |
55 | function addChild(index: number, componentDefinition: ComponentDefinition) {
56 | const definition = inst?.$props.definition;
57 | if (definition && definition.children) {
58 | definition.children!.splice(index, 0, componentDefinition)
59 | inst.forceRender();
60 | }
61 | }
62 |
63 | function changeIndex(source: number, target: number) {
64 | const definition = inst?.$props.definition;
65 | if (definition && definition.children) {
66 | const s = definition.children![source];
67 | definition.children![source] = definition.children![target];
68 | definition.children![target] = s;
69 | inst.forceRender();
70 | }
71 | }
72 |
73 | function getChildBySlot(slot: number): ComponentDefinition | null {
74 | const definition = inst?.$props.definition;
75 | if (definition && definition.children) {
76 | return definition.children?.filter(it => it.slot == slot)[0] || null
77 | }
78 |
79 | return null;
80 | }
81 |
82 | function forceRender() {
83 | if (inst) {
84 | inst.$forceUpdate();
85 | }
86 | }
87 |
88 | function getComponent(type: string): DefineComponent {
89 | return FormHelper.getComponent(type)
90 | }
91 |
92 | return {
93 | addChildBySlot,
94 | removeChildBySlot,
95 | removeChild,
96 | addChild,
97 | changeIndex,
98 | getChildBySlot,
99 | forceRender,
100 | getComponent
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | export function makeFieldId() {
2 | const date = new Date().getTime();
3 | if (date < 0) {
4 | throw Error("系统时间错误");
5 | }
6 | return 'FD' + date
7 | }
8 |
9 | export function makeComponentId() {
10 | const date = new Date().getTime();
11 | if (date < 0) {
12 | throw Error("系统时间错误");
13 | }
14 | return 'CP' + date
15 | }
16 |
17 |
18 | /**
19 | * 获取属性值并擦除属性
20 | * @param obj
21 | * @param fieldPath
22 | * @returns {*}
23 | */
24 | export function getFieldValue(obj: any, fieldPath: string): T | null {
25 | const fields: string[] = fieldPath.split('.');
26 |
27 | let co: any = obj;
28 | let i = 1;
29 | for (const field of fields) {
30 | if (field.endsWith(']')) {
31 | // 数组处理
32 | let index = field.indexOf('[');
33 | const fieldName = field.substring(0, index);
34 | const arrIndex = parseInt(field.substring(index + 1, field.length - 1))
35 |
36 | if (co.hasOwnProperty(fieldName)) {
37 | if (i == fields.length) {
38 | return co[fieldName][arrIndex];
39 | } else {
40 | co = co[fieldName][arrIndex] || {};
41 | }
42 | i++;
43 | } else {
44 | throw Error(`[${fieldPath}] unreachable`);
45 | }
46 | } else if (co.hasOwnProperty(field)) {
47 | // 普通字段处理
48 | if (i == fields.length) {
49 | return co[field];
50 | } else {
51 | co = co[field];
52 | }
53 | i++;
54 | } else {
55 | throw Error(`[${fieldPath}] unreachable`);
56 | }
57 | }
58 |
59 | return null;
60 | }
61 |
62 | /**
63 | * 合并两个对象某个字段值
64 | * @param fieldPath
65 | * @param source
66 | * @param target
67 | */
68 | export function mergeObj2Obj(fieldPath: string, source: any, target: any) {
69 | const value = getFieldValue(source, fieldPath);
70 | mergeObject(fieldPath, value, target);
71 | }
72 |
73 |
74 | function isRefObj(value: any) {
75 | switch (typeof value) {
76 | case "bigint":
77 | case "boolean":
78 | case "number":
79 | case "string":
80 | case "undefined":
81 | return false;
82 | default:
83 | return true;
84 | }
85 | }
86 |
87 | /**
88 | *
89 | * @param fieldPath
90 | * @param value
91 | * @param obj
92 | */
93 | export function mergeObject(fieldPath: string, value: any, obj: any) {
94 | const fields: string[] = fieldPath.split('.');
95 |
96 | let co: any = obj;
97 | let i = 1;
98 | for (const field of fields) {
99 | if (field.endsWith(']')) {
100 | // 数组处理
101 | let index = field.indexOf('[');
102 | const fieldName = field.substring(0, index);
103 | const arrIndex = parseInt(field.substring(index + 1, field.length - 1))
104 |
105 | if (co.hasOwnProperty(fieldName)) {
106 | if (i == fields.length) {
107 | co[fieldName][arrIndex] = isRefObj(value) ? JSON.parse(JSON.stringify(value)) : value;
108 | } else {
109 | co = co[fieldName][arrIndex] || {};
110 | }
111 | i++;
112 | } else {
113 | throw Error(`[${fieldPath}] unreachable`);
114 | }
115 | } else if (co.hasOwnProperty(field)) {
116 | // 普通字段处理
117 | if (i == fields.length) {
118 | co[field] = isRefObj(value) ? JSON.parse(JSON.stringify(value)) : value;
119 | } else {
120 | co = co[field];
121 | }
122 | i++;
123 | } else {
124 | throw Error(`[${fieldPath}] unreachable`);
125 | }
126 |
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/antd-designer/src/designer/wrapper/ComponentWrapper.tsx:
--------------------------------------------------------------------------------
1 | import React, {ComponentClass, ComponentSpec} from 'react';
2 | import {CloseCircleOutlined} from '@ant-design/icons';
3 | import className from 'classnames';
4 | import createReactClass from 'create-react-class';
5 | import {Activatable, ReactComponentProps} from "../types";
6 | import {FormHelper} from "../helper";
7 |
8 | const hoistNonReactStatics = require('hoist-non-react-statics');
9 |
10 | function ComponentWrapper(WrappedComponent: ComponentClass>) {
11 | const componentLayout = createReactClass({
12 | displayName: 'ComponentWrapper',
13 | getInitialState() {
14 | return {
15 | renderCounter: 0,
16 | active: false
17 | }
18 | },
19 |
20 | /**
21 | * shouldComponentUpdate 默认不是对象的深层比较,采用标志位的方式
22 | * 此方法代替foreUpdate
23 | */
24 | forceRender() {
25 | const {renderCounter} = this.state;
26 | this.setState({
27 | renderCounter: (renderCounter || 0) + 1
28 | });
29 | },
30 |
31 | onActive() {
32 | this.setState({
33 | active: true
34 | });
35 | },
36 | unActive() {
37 | this.setState({
38 | active: false
39 | })
40 | },
41 |
42 | getWrappedInstance() {
43 | return this.wrappedInstance;
44 | },
45 | setWrappedInstance(ref: any) {
46 | this.wrappedInstance = ref;
47 | },
48 |
49 | onDelete(e: React.MouseEvent) {
50 | e.stopPropagation();
51 |
52 | console.log(this, 'onDelete')
53 | const {onRemove} = this.props;
54 | if (onRemove) {
55 | onRemove()
56 | }
57 |
58 | // 如果删除的是当前激活状态的实例,设置null
59 | if (FormHelper.activeComponentIns == this) {
60 | FormHelper.activeComponentIns = null;
61 | }
62 | },
63 | renderDel() {
64 | const {active} = this.state;
65 |
66 | if (active) {
67 | return (
68 | <>
69 | this.onDelete(e)}
71 | >
72 |
73 |
74 | >
75 | )
76 | } else {
77 | return;
78 | }
79 | },
80 | activeClick(e: Event) {
81 | if (e) {
82 | e.stopPropagation();
83 | }
84 | FormHelper.activeComponentIns = this;
85 | },
86 | render() {
87 | const {active, renderCounter} = this.state;
88 | const {definition} = this.props;
89 | return (
90 | <>
91 |
93 | {this.renderDel()}
94 |
95 |
96 | {definition.title}
97 |
98 |
99 |
104 |
105 |
106 |
107 | >
108 | );
109 | }
110 | } as Activatable & ComponentSpec);
111 |
112 | componentLayout.displayName = `ComponentWrapper(${WrappedComponent.displayName || WrappedComponent.name || 'WrappedComponent'})`;
113 |
114 | return hoistNonReactStatics(componentLayout, WrappedComponent);
115 | }
116 |
117 | export default ComponentWrapper;
118 |
--------------------------------------------------------------------------------
/antdv-designer/src/designer/widget/OptionsEditor.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | |
22 | 显示值
23 | |
24 |
25 | 真值
26 | |
27 |
28 | 默认
29 | |
30 |
31 | 禁用
32 | |
33 |
34 | |
35 |
36 |
37 |
38 |
39 | |
40 |
46 |
49 |
50 | |
51 |
52 |
58 |
60 |
61 | |
62 |
63 |
69 |
71 |
72 |
73 |
74 | |
75 |
76 |
77 |
83 |
85 |
86 | |
87 |
88 |
89 |
90 |
91 |
92 |
93 | |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
160 |
161 |
164 |
--------------------------------------------------------------------------------
/antd-designer/src/designer/wrapper/LayoutWrapper.tsx:
--------------------------------------------------------------------------------
1 | import React, {ComponentClass, ComponentSpec, PureComponent} from 'react';
2 | import className from 'classnames';
3 | import createReactClass from 'create-react-class';
4 | import {Activatable, ReactComponentProps} from "../types";
5 | import {DeleteOutlined, DragOutlined} from "@ant-design/icons";
6 | import {FormHelper} from "../helper";
7 |
8 | const hoistNonReactStatics = require('hoist-non-react-statics');
9 |
10 | interface LayoutToolbarProps {
11 | disable?: boolean;
12 |
13 | onRemove?(): void;
14 | }
15 |
16 | class LayoutToolbar extends PureComponent {
17 | onDelete(e: React.MouseEvent) {
18 | e.stopPropagation();
19 | const {onRemove} = this.props;
20 | if (onRemove) {
21 | onRemove()
22 | }
23 | }
24 |
25 | override render() {
26 | const {disable} = this.props;
27 |
28 | if (disable) {
29 | return;
30 | }
31 |
32 | return (
33 | <>
34 |
35 |
36 |
37 |
38 | this.onDelete(e)} title="删除">
39 |
40 |
41 |
42 | >
43 | )
44 | }
45 | }
46 |
47 | export interface FactoryWrapperOptions {
48 | focusAble?: boolean;
49 | toolbarAble?: boolean;
50 | layoutStyle?: any;
51 | }
52 |
53 | function LayoutWrapperFactory(opt: FactoryWrapperOptions = {}) {
54 | const options = opt || {};
55 |
56 | return function LayoutWrapper(WrappedComponent: ComponentClass>) {
57 | const componentLayout = createReactClass({
58 | displayName: 'LayoutWrapper',
59 | getInitialState() {
60 | return {
61 | renderCounter: 0,
62 | active: false,
63 | focusAble: options.focusAble !== false, // 是否支持active
64 | toolbarAble: options.toolbarAble !== false, // 是否显示toolbar
65 | layoutStyle: options.layoutStyle || {}
66 | }
67 | },
68 |
69 | /**
70 | * shouldComponentUpdate 默认不是对象的深层比较,采用标志位的方式
71 | * 此方法代替foreUpdate
72 | */
73 | forceRender() {
74 | const {renderCounter} = this.state;
75 | this.setState({
76 | renderCounter: (renderCounter || 0) + 1
77 | })
78 | },
79 |
80 | onActive() {
81 | this.setState({
82 | active: true
83 | })
84 | },
85 |
86 | unActive() {
87 | this.setState({
88 | active: false
89 | })
90 | },
91 | activeClick(e: Event) {
92 | if (e) {
93 | e.stopPropagation();
94 | }
95 | const focusAble = this.props.focusAble == undefined ? this.state.focusAble : this.props.focusAble;
96 | if (focusAble) {
97 | FormHelper.activeComponentIns = this;
98 | }
99 | },
100 | getWrappedInstance() {
101 | return this.wrappedInstance;
102 | },
103 | setWrappedInstance(ref: any) {
104 | this.wrappedInstance = ref;
105 | },
106 |
107 | onDelete() {
108 | const {onRemove} = this.props;
109 | console.log(this, 'onDelete')
110 | if (onRemove) {
111 | onRemove()
112 | }
113 |
114 | // 如果删除的是当前激活状态的实例,设置null
115 | if (FormHelper.activeComponentIns == this) {
116 | FormHelper.activeComponentIns = null;
117 | }
118 | },
119 |
120 | render() {
121 | const {active, renderCounter, layoutStyle} = this.state;
122 | const toolbarAble = this.props.toolbarAble == undefined ? this.state.toolbarAble : this.props.toolbarAble
123 | const {definition} = this.props;
124 | return (
125 |
128 |
130 | {toolbarAble ? this.onDelete()}/> : null}
131 |
132 | );
133 | }
134 | } as Activatable & ComponentSpec);
135 |
136 | componentLayout.displayName = `LayoutWrapper(${WrappedComponent.displayName || WrappedComponent.name || 'WrappedComponent'})`;
137 |
138 | return hoistNonReactStatics(componentLayout, WrappedComponent);
139 |
140 | }
141 | }
142 |
143 |
144 | export default LayoutWrapperFactory;
145 |
--------------------------------------------------------------------------------
/src/style/formView.less:
--------------------------------------------------------------------------------
1 | @warn-color: #DB4040;
2 | @primary-color: #1890ff;
3 | @border-color: #ddd;
4 |
5 | body {
6 | margin: 0;
7 | height: 100vh;
8 | }
9 |
10 | .form-view {
11 | min-height: 100%;
12 | min-width: 778px;
13 | margin: 0 auto;
14 | background: #fff;
15 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.4);
16 |
17 | & .scroll-wrapper {
18 | height: 100%;
19 | }
20 |
21 | & .form-head {
22 | position: relative;
23 | -webkit-box-sizing: content-box;
24 | -moz-box-sizing: content-box;
25 | box-sizing: content-box;
26 | padding: 20px 10px 12px;
27 | word-wrap: break-word;
28 | word-break: break-all;
29 |
30 | & .form-name {
31 | text-align: center;
32 | font-weight: bold;
33 | font-size: 18px;
34 | }
35 |
36 | & .form-description {
37 | text-align: center;
38 | font-weight: bold;
39 | font-size: 12px;
40 | color: #999999;
41 | }
42 | }
43 |
44 | & .form-content {
45 | display: flex;
46 | min-height: 700px;
47 | padding: 10px;
48 | }
49 |
50 | & .columnLayout {
51 | display: table;
52 | width: 100%;
53 | table-layout: fixed;
54 | border: 1px solid @primary-color;
55 | }
56 |
57 | & .layout-tool-bar {
58 | position: absolute;
59 | right: 0;
60 | bottom: 0;
61 | z-index: 100;
62 | line-height: 20px;
63 | -webkit-transition: opacity linear 0.15s;
64 | -moz-transition: opacity linear 0.15s;
65 | transition: opacity linear 0.15s;
66 |
67 | & .fm-btn {
68 | margin-left: 1px;
69 | padding: 0 5px;
70 | color: #fff;
71 | font-size: 12px;
72 | text-align: center;
73 | background: @primary-color;
74 | cursor: pointer;
75 | }
76 | }
77 |
78 | & .fm-btn-del {
79 | font-size: 18px;
80 | background: @warn-color !important;
81 | cursor: pointer;
82 | }
83 |
84 | & .formPlaceholder,
85 | & .form-placeholder-filed {
86 | height: 80px;
87 | border: 2px dashed @warn-color;
88 | }
89 |
90 | & .ui-sortable {
91 | width: 100%;
92 | min-height: 50px;
93 | }
94 |
95 | & .component {
96 | position: relative;
97 | cursor: pointer;
98 |
99 | &.active {
100 | background: #fff8dc;
101 | border: 1px dashed #000 !important;
102 |
103 | & .fm-btn-remove {
104 | display: block !important;
105 | }
106 | }
107 | }
108 |
109 | & .component-layout {
110 | position: relative;
111 | display: flex;
112 | margin-bottom: -1px;
113 | border-right: 1px dashed rgba(0, 0, 0, 0);
114 | border-top: 1px dashed rgba(0, 0, 0, 0);
115 | border-left: 1px dashed rgba(0, 0, 0, 0);
116 | border-bottom: 1px dashed @border-color;
117 | cursor: pointer;
118 | min-height: 60px;
119 |
120 | &:first-child {
121 | border: 1px dashed @border-color;
122 | }
123 |
124 | & .component-layout:first-child {
125 | border-top: none;
126 | }
127 |
128 |
129 | & .flex-column {
130 | display: flex;
131 | flex-direction: column;
132 | }
133 |
134 | & .flex-row {
135 | display: flex;
136 | flex-direction: row;
137 | }
138 | }
139 |
140 | & .column-layout {
141 | display: table;
142 | width: 100%;
143 | table-layout: fixed;
144 |
145 |
146 | & .cell {
147 | display: table-cell;
148 | height: 50px;
149 | vertical-align: top;
150 | border-right: 1px dashed rgba(0, 0, 0, 0);
151 | border-top: 1px dashed rgba(0, 0, 0, 0);
152 | border-bottom: 1px dashed rgba(0, 0, 0, 0);
153 | border-left: 1px dashed @border-color;
154 | }
155 |
156 | & .cell:first-child {
157 | border-left: none !important;
158 | }
159 | }
160 |
161 | & .component-field {
162 | position: relative;
163 | background: #fff;
164 | padding: 14px;
165 |
166 | border-right: 1px dashed rgba(0, 0, 0, 0);
167 | border-top: 1px dashed rgba(0, 0, 0, 0);
168 | border-left: 1px dashed rgba(0, 0, 0, 0);
169 | border-bottom: 1px dashed @border-color;;
170 |
171 | & .fm-btn-remove {
172 | position: absolute;
173 | top: 0;
174 | right: 0;
175 | z-index: 1;
176 | display: none;
177 | color: #d14836;
178 | font-size: 14px;
179 | cursor: pointer;
180 | }
181 |
182 | & .field-title {
183 | float: left;
184 | width: 80px;
185 | margin-top: 5px;
186 | text-align: right;
187 | }
188 |
189 | & .field-content {
190 | position: relative;
191 | min-height: 30px;
192 | margin-left: 90px;
193 | }
194 | }
195 |
196 | & .component-field,
197 | & .component-layout {
198 | &.active {
199 | background: #fff8dc;
200 | border-color: #ddd;
201 | }
202 | }
203 | }
204 |
205 | .form-designer {
206 | display: flex;
207 | height: 100%;
208 | background: #f0f2f5;
209 | justify-content: space-between;
210 |
211 | .form-view-wrapper {
212 | flex: 1;
213 | height: 100%;
214 | overflow-y: auto;
215 | }
216 |
217 | .form-editor {
218 | padding-left: 10px;
219 | padding-right: 10px;
220 | border-left: 1px solid #efefef;
221 | background: white;
222 | width: 450px;
223 |
224 | .ant-tabs-nav-wrap {
225 | justify-content: center;
226 | }
227 | }
228 |
229 | .form-widget {
230 | border-right: 1px solid #efefef;
231 | background: white;
232 |
233 | .ant-tabs-nav-wrap {
234 | justify-content: center;
235 | }
236 | }
237 | }
238 |
239 |
--------------------------------------------------------------------------------
/antdv-designer/src/designer/ComponentPropsEditor.vue:
--------------------------------------------------------------------------------
1 |
2 |
66 |
67 |
68 |
170 |
171 |
174 |
--------------------------------------------------------------------------------
/config/webpack.base.config.ts:
--------------------------------------------------------------------------------
1 | import type {Configuration as DevServerConfiguration} from "webpack-dev-server";
2 | import type {Configuration} from "webpack";
3 | import envs from "./envs";
4 |
5 | const path = require('path')
6 | const dotenv = require('dotenv')
7 | const webpack = require('webpack')
8 |
9 | const env = dotenv.config({path: path.resolve(__dirname, '../.env.' + process.env.ENV_MODE)}).parsed
10 | const envKeys = Object.keys(env).reduce((prev: any, next) => {
11 | prev[`process.env.${next}`] = JSON.stringify(env[next])
12 | return prev
13 | }, {
14 | 'process.env.ENV_MODE': `'${process.env.ENV_MODE}'`
15 | })
16 |
17 |
18 | const HtmlWebpackPlugin = require('html-webpack-plugin');
19 | const ExtractTextPlugin = require('mini-css-extract-plugin');
20 |
21 | const cssRegex = /\.css$/;
22 | const cssModuleRegex = /\.module\.css$/;
23 | const lessRegex = /\.less$/;
24 | const lessModuleRegex = /\.module\.less$/;
25 |
26 | export const isEnvDevelopment = process.env.ENV_MODE === envs.DEV;
27 | export const isEnvProduction = process.env.ENV_MODE === envs.PROD;
28 |
29 | const devServer: DevServerConfiguration = {
30 | static: {
31 | directory: path.join(__dirname, 'public'),
32 | },
33 | compress: true,
34 | hot: true,
35 | port: 9001
36 | };
37 |
38 | function getWebpackMode() {
39 | switch (process.env.ENV_MODE) {
40 | case envs.PROD:
41 | return 'production';
42 | case envs.DEV:
43 | default:
44 | return 'development';
45 | }
46 | }
47 |
48 | function getStyleLoaders(cssOptions: any, preProcessor?: any): any[] {
49 | console.log(isEnvDevelopment)
50 | const loaders = [
51 | isEnvDevelopment && {
52 | loader: require.resolve('style-loader')
53 | },
54 | isEnvProduction && {
55 | loader: ExtractTextPlugin.loader,
56 | },
57 | {
58 | loader: 'css-loader',
59 | options: Object.assign({}, cssOptions, {
60 | importLoaders: 1 + preProcessor ? 1 : 0,
61 | esModule: false,
62 | }),
63 | },
64 | {
65 | loader: 'postcss-loader',
66 | options: {
67 | sourceMap: isEnvDevelopment,
68 | },
69 | },
70 | ].filter(Boolean);
71 | if (preProcessor) {
72 | loaders.push(preProcessor);
73 | }
74 |
75 | console.log('getStyleLoaders', loaders)
76 | return loaders;
77 | };
78 |
79 | const config: Configuration = {
80 | mode: getWebpackMode(),
81 | devtool: isEnvDevelopment ? 'source-map' : false,
82 | infrastructureLogging: {
83 | appendOnly: true,
84 | level: 'verbose',
85 | },
86 | stats: {
87 | errorDetails: true
88 | },
89 | module: {
90 | rules: [
91 | {
92 | test: /\.([cm]?ts|tsx)$/,
93 | use: {
94 | loader: 'ts-loader',
95 | options: {
96 | logLevel: 'info',
97 | logInfoToStdOut: true,
98 | appendTsSuffixTo: [/\.vue$/]
99 | }
100 | },
101 | exclude: /node_modules/,
102 | },
103 | {
104 | test: /\.js/,
105 | use: {
106 | loader: 'babel-loader',
107 | },
108 | exclude: /node_modules/
109 | },
110 | {
111 | test: cssRegex,
112 | exclude: cssModuleRegex,
113 | use: getStyleLoaders({
114 | modules: false,
115 | sourceMap: isEnvDevelopment
116 | })
117 | },
118 | {
119 | test: cssModuleRegex,
120 | use: getStyleLoaders({
121 | modules: true,
122 | sourceMap: isEnvDevelopment
123 | })
124 | },
125 | {
126 | test: lessRegex,
127 | exclude: lessModuleRegex,
128 | use: getStyleLoaders({
129 | modules: false,
130 | sourceMap: isEnvDevelopment
131 | }, {
132 | loader: 'less-loader',
133 | options: {
134 | lessOptions: {
135 | javascriptEnabled: true
136 | }
137 | }
138 | })
139 | },
140 | {
141 | test: lessModuleRegex,
142 | use: getStyleLoaders({
143 | modules: true,
144 | sourceMap: isEnvDevelopment
145 | }, {
146 | loader: 'less-loader',
147 | options: {
148 | lessOptions: {
149 | javascriptEnabled: true
150 | }
151 | }
152 | })
153 | },
154 | ]
155 | },
156 | resolve: {
157 | extensions: ['.tsx', '.ts', '.js', '.jsx'],
158 | alias: {
159 | '@@': path.resolve(__dirname, '../src')
160 | }
161 | },
162 | devServer,
163 | plugins: [
164 | new webpack.DefinePlugin(envKeys),
165 | new ExtractTextPlugin({filename: "[name].css",}),
166 | new HtmlWebpackPlugin({
167 | title: 'Form Designer',
168 | template: path.resolve(__dirname, 'index.ejs')
169 | })
170 | ]
171 | };
172 |
173 |
174 | export default config;
175 |
--------------------------------------------------------------------------------
/antd-designer/src/designer/widgets/OptionGroupEditor.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {Button, Checkbox, Form, Input, Radio} from "antd";
3 | import {PropsEditor} from "./PropsEditor";
4 | import {OptionType} from "../../../../src/props";
5 | import {DeleteOutlined, PlusOutlined} from "@ant-design/icons";
6 |
7 | const styles = require('@@/style/component.module.less')
8 |
9 | type OptionGroupProps = {
10 | options: OptionType[]
11 | }
12 |
13 | export abstract class OptionGroupEditor extends PropsEditor {
14 |
15 | protected removeOption(index: number) {
16 | const definition = this.props.definition;
17 | const props: OptionGroupProps = definition.props!;
18 | props.options.splice(index, 1);
19 | this.forceUpdate();
20 | this.refreshFormView();
21 | }
22 |
23 | protected addOption(index: number) {
24 | const definition = this.props.definition;
25 | const props: OptionGroupProps = definition.props!;
26 | props.options.splice(index + 1, 0, {
27 | label: `显示值${index + 1}`,
28 | value: `真值${index + 1}`,
29 | checked: false,
30 | disabled: false
31 | });
32 | this.forceUpdate();
33 | this.refreshFormView();
34 | }
35 |
36 | protected renderOptions() {
37 | const definition = this.props.definition;
38 | const props: OptionGroupProps = definition.props!;
39 | const opts = props.options.map((item, index) => {
40 | return (
41 |
42 | |
43 |
51 |
52 |
53 | |
54 |
55 |
63 |
64 |
65 | |
66 |
67 |
73 |
74 |
75 |
76 |
77 | |
78 |
79 |
85 |
86 |
87 | |
88 |
89 |
90 | {/*
96 | |
97 |
98 | );
99 | })
100 |
101 | return (
102 |
103 |
104 | }
105 | onClick={() => this.addOption(props.options.length - 1)}>
106 | 添加
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 | |
120 | 显示值
121 | |
122 |
123 | 真值
124 | |
125 |
126 | 默认
127 | |
128 |
129 | 禁用
130 | |
131 |
132 | |
133 |
134 |
135 |
136 | {opts}
137 |
138 |
139 |
140 | )
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/antd-designer/src/designer/widgets/PropsEditor.tsx:
--------------------------------------------------------------------------------
1 | import React, {PropsWithChildren, PureComponent, ReactElement} from "react";
2 | import {Form, FormInstance, Input, InputNumber, Select} from "antd";
3 | import {ComponentEditor, ReactComponentProps} from "../types";
4 | import {FormHelper} from "../helper";
5 | import {mergeObject} from "../../../../src/utils";
6 | import {FieldType, FieldTypes} from "../../../../src/types";
7 |
8 | const lengthFields: FieldType[] = ['varchar', 'string'];
9 | const scaleFields: FieldType[] = ['decimal'];
10 |
11 | export type PropsEditorState = {
12 | scaleDisabled: boolean,
13 | lengthDisabled: boolean
14 | }
15 |
16 | export abstract class PropsEditor extends PureComponent>, PropsEditorState>
17 | implements ComponentEditor, T> {
18 |
19 |
20 | private writeFields: string[] = ["props", "title", "fieldDef.fieldName", "fieldDef.fieldType"];
21 |
22 | private fieldTypeList: { value: string, label: string }[] = Object.keys(FieldTypes).map(field => {
23 | return {
24 | value: field,
25 | label: FieldTypes[field]
26 | }
27 | })
28 |
29 | private readonly refForm = React.createRef();
30 |
31 | constructor(props: React.PropsWithChildren>, context: any) {
32 | super(props, context);
33 | this.state = {
34 | scaleDisabled: false,
35 | lengthDisabled: false,
36 | }
37 | }
38 |
39 | onValuesChange(changedValues: any, allValues: any) {
40 | console.log('onValuesChange', changedValues)
41 | const {definition} = this.props;
42 | for (const field of Object.keys(changedValues)) {
43 | const canMerge = this.writeFields.filter(it => field.startsWith(it)).length;
44 | if (canMerge) {
45 | mergeObject(field, changedValues[field], definition);
46 | } else {
47 | throw new Error(`the field [${field}] is readonly`)
48 | }
49 |
50 | }
51 |
52 | this.refreshFormView();
53 | }
54 |
55 | /**
56 | * 强制刷新FormView
57 | * @protected
58 | */
59 | protected refreshFormView() {
60 | FormHelper.activeComponentIns!.forceRender();
61 | }
62 |
63 |
64 | onSelectType(val: string) {
65 | this.setState({
66 | scaleDisabled: scaleFields.indexOf(val as FieldType) < 0,
67 | lengthDisabled: lengthFields.indexOf(val as FieldType) < 0
68 | })
69 | this.refForm.current!.setFieldsValue({
70 | 'fieldDef.length': null,
71 | 'fieldDef.scale': null
72 | })
73 | }
74 |
75 | renderFieldDef() {
76 | const {definition} = this.props;
77 | const {lengthDisabled, scaleDisabled} = this.state
78 | if (definition.fieldDef) {
79 | return (
80 |
144 | )
145 | } else {
146 | return null;
147 | }
148 | }
149 |
150 | override render() {
151 | return (
152 | <>
153 |
162 | >
163 | )
164 | }
165 |
166 | abstract doRender(): ReactElement;
167 | }
168 |
--------------------------------------------------------------------------------