}
34 | renderItem={(item) => (
35 | { onDelete(item); }}>删除]}
37 | >
38 |
64 |
65 | )}
66 | />
67 | );
68 | };
69 |
70 | export default EnumList;
71 |
--------------------------------------------------------------------------------
/src/components/descList.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const getEnumLabel = (value, enums) => {
4 | if (Array.isArray(enums)) {
5 | if (typeof value === 'string' || typeof value === 'number') {
6 | const findIndex = enums.findIndex(x => x.value === value);
7 | if (findIndex > -1) {
8 | return enums[findIndex].label;
9 | }
10 | return value;
11 | } else if (Array.isArray(value)) {
12 | const result = value.map(v => getEnumLabel(value, enums));
13 | return String(result);
14 | }
15 | return value;
16 | }
17 | return value;
18 | };
19 |
20 | const DescriptionList = ({ schema = {}, value = [], index }) => {
21 | const list = getDescription({ schema, value, index })
22 | .filter(item => item.title)
23 | .slice(0, 3);
24 | return (
25 |
26 | {list.map((item, i) => {
27 | return item.title ? (
28 | -
29 | {item.title}:
30 | {item.text}
31 |
32 | ) : null;
33 | })}
34 |
35 | );
36 | };
37 |
38 | export default DescriptionList;
39 |
40 | // 获得title,value值list
41 | export const getDescription = ({ schema = {}, value = [], index }) => {
42 | const { items = {} } = schema;
43 | // 只有当items为object时才做收起(fold)处理
44 | if (items.type !== 'object') {
45 | return [];
46 | }
47 | let titles = (items && items.properties) || {};
48 | titles = Object.values(titles);
49 | let description = (value && value.length && value[index]) || {};
50 | const valueList = Object.values(description);
51 | const descList = titles.map((t, idx) => {
52 | let hidden = t && t['ui:hidden'];
53 | // ui:hidden为判断式时解析 TODO: 解析在外部集中做
54 | if (hidden) return;
55 | const title = t.title;
56 | let text = valueList[idx];
57 | if (text === null && text === undefined) {
58 | text = '';
59 | } else if (typeof text === 'boolean') {
60 | text = text ? '是' : '否';
61 | } else if (typeof text !== 'string' && typeof text !== 'number' && text) {
62 | text = '{复杂结构}';
63 | } else if (t.enum) {
64 | text = getEnumLabel(text, t.enum);
65 | }
66 | return {
67 | title,
68 | text,
69 | };
70 | });
71 | // 去空
72 | return descList.filter(d => !!d);
73 | };
74 |
--------------------------------------------------------------------------------
/src/Right/ItemSettings.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import FRWrapper from '../FRWrapper';
3 | import { widgets } from '../widgets';
4 | import { useGlobalProps, useStore } from '../hooks';
5 | import ALL, { commonSettings } from '../Left/elementList';
6 | import { getWidgetName } from '../mapping';
7 |
8 | const elements = [...ALL[0], ...ALL[1], ...ALL[2]]; // 前三项是所有的组件
9 |
10 | export default function ItemSettings() {
11 | const { flatten, onItemChange } = useStore();
12 | const { selected, Widgets, isPc, customElements, advancedElements } = useGlobalProps();
13 |
14 | let settingSchema = {};
15 | let settingData = {};
16 |
17 | const onDataChange = newSchema => {
18 | if (selected) {
19 | try {
20 | const item = flatten[selected];
21 | if (item && item.schema) {
22 | onItemChange(selected, { ...item, schema: newSchema });
23 | }
24 | } catch (error) {
25 | console.log(error, 'catch');
26 | }
27 | }
28 | };
29 |
30 | // setting该显示什么的计算,要把选中组件的schema和它对应的widgets的整体schema进行拼接
31 | let itemSelected;
32 | let widgetName;
33 | try {
34 | itemSelected = flatten[selected];
35 | if (itemSelected) {
36 | widgetName = getWidgetName(itemSelected.schema);
37 | }
38 | if (widgetName) {
39 | let schemaNow;
40 | const element = elements.find(e => e.widget === widgetName);
41 | if (element) { // 内置组件
42 | schemaNow = isPc ? element.setting : element.mobileSetting;
43 | } else { // 自定义组件
44 | const customSetting = [...advancedElements, ...customElements].find(x => x.schema['x-component'] === widgetName).settings;
45 | schemaNow = { ...commonSettings, ...customSetting }
46 | }
47 | // 包装一层schema,符合FRWrapper格式
48 | settingSchema = {
49 | type: 'object',
50 | properties: {
51 | ...schemaNow,
52 | },
53 | };
54 | // 选中的控件的schema,即是配置组件的formdata
55 | settingData = itemSelected.schema;
56 | }
57 | } catch (error) {
58 | console.log(error);
59 | }
60 |
61 | return (
62 |
63 |
73 |
74 | );
75 | }
76 |
--------------------------------------------------------------------------------
/docs/demo/Demo2.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useRef } from 'react';
2 | import Generator from '@coolvision/schema-generator';
3 | import { Button, Modal, Input } from 'antd';
4 | const { TextArea } = Input;
5 |
6 | const defaultValue = {
7 | type: 'object',
8 | properties: {
9 | array: {
10 | key: 'array',
11 | type: 'array',
12 | name: 'array',
13 | title: 'Name',
14 | 'x-component': 'arraytable',
15 | items: {
16 | type: 'object',
17 | properties: {
18 | aa: {
19 | key: 'aa',
20 | type: 'string',
21 | name: 'aa',
22 | title: '控制相邻字段显示隐藏',
23 | enum: [
24 | {
25 | label: '显示',
26 | value: true,
27 | },
28 | {
29 | label: '隐藏',
30 | value: false,
31 | },
32 | ],
33 | 'x-component': 'input',
34 | },
35 | bb: {
36 | key: 'bb',
37 | type: 'string',
38 | name: 'bb',
39 | title: 'BB',
40 | 'x-component': 'input',
41 | },
42 | },
43 | },
44 | },
45 | cc: {
46 | key: 'cc',
47 | type: 'string',
48 | name: 'cc',
49 | title: 'CC',
50 | 'x-component': 'input',
51 | 'x-component-props': { min: 1 },
52 | },
53 | },
54 | };
55 |
56 | const Demo = () => {
57 | const [show, setShow] = useState(false);
58 | const [schema, setSchema] = useState(() => defaultValue);
59 | const genRef = useRef(); // class组件用 React.createRef()
60 |
61 | const toggle = () => setShow(o => !o);
62 |
63 | const handleOk = () => {
64 | const value = genRef.current && genRef.current.getValue();
65 | setSchema(value);
66 | toggle();
67 | };
68 |
69 | return (
70 |
71 |
74 |
83 |
84 |
85 |
91 | );
92 | };
93 |
94 | export default Demo;
95 |
--------------------------------------------------------------------------------
/src/Main.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, forwardRef } from 'react';
2 | import { useSet } from './hooks';
3 | import FRWrapper from './FRWrapper';
4 | import { mapping } from './mapping';
5 | import { isDeepEqual } from './utils/common';
6 | import 'antd/dist/antd.css';
7 | import 'antd-mobile/dist/antd-mobile.css';
8 | import 'tachyons';
9 | import './Main.css';
10 |
11 | const SCHEMA = {
12 | type: 'object',
13 | properties: {},
14 | };
15 |
16 | // TODO: formData 不存在的时候会报错:can't find # of undefined
17 |
18 | function App({
19 | defaultValue,
20 | value,
21 | onChange = () => {},
22 | customElements,
23 | advancedElements,
24 | className,
25 | Widgets,
26 | title,
27 | actions,
28 | onSave,
29 | beforeDragComplete = () => true
30 | }, ref) {
31 | const initGlobal = {
32 | displayType: 'row',
33 | };
34 |
35 | const [state, setState] = useSet({
36 | formData: {},
37 | schema: SCHEMA,
38 | selected: undefined, // 被选中的$id, 如果object/array的内部,以首字母0标识
39 | hovering: undefined, // 目前没有用到
40 | preview: false, // preview = false 是编辑模式
41 | isPc: true,
42 | ...initGlobal, // form-render 的全局props等
43 | });
44 |
45 | const { schema, formData, preview, selected, hovering, isPc, ...rest } = state;
46 |
47 | const { displayType } = rest;
48 | const showDescIcon = displayType === 'row' ? true : false;
49 |
50 | const onDataChange = data => {
51 | setState({ formData: data });
52 | };
53 |
54 | const onSchemaChange = newSchema => {
55 | setState({ schema: newSchema });
56 |
57 | onChange(newSchema);
58 | };
59 |
60 | useEffect(() => {
61 | if (typeof value === 'undefined') {
62 | setState({
63 | schema: defaultValue || SCHEMA
64 | });
65 | }
66 | }, []);
67 |
68 | useEffect(() => {
69 | if (typeof value !== 'undefined' && !isDeepEqual(schema, value)) {
70 | setState({
71 | schema: value
72 | });
73 | }
74 | }, [value]);
75 |
76 | const _mapping = { ...mapping };
77 |
78 | const globalProps = {
79 | preview,
80 | isPc,
81 | setState,
82 | simple: false,
83 | mapping: _mapping,
84 | beforeDragComplete,
85 | Widgets, // 外部引入组件
86 | advancedElements: advancedElements || [], // 高级组件
87 | customElements: customElements || [], // 自定义组件
88 | selected,
89 | hovering,
90 | ...rest,
91 | showDescIcon,
92 | };
93 |
94 | const FRProps = {
95 | schema,
96 | formData,
97 | onChange: onDataChange,
98 | onSchemaChange,
99 | className,
100 | title,
101 | actions,
102 | onSave,
103 | ...globalProps,
104 | };
105 |
106 | return ;
107 | }
108 |
109 | export default forwardRef(App);
110 |
--------------------------------------------------------------------------------
/src/utils/common.js:
--------------------------------------------------------------------------------
1 |
2 | function stringContains(str, text) {
3 | return str.indexOf(text) > -1;
4 | }
5 |
6 | export const isObject = a =>
7 | stringContains(Object.prototype.toString.call(a), 'Object');
8 |
9 | // '3' => true, 3 => true, undefined => false
10 | export function isLooselyNumber(num) {
11 | if (typeof num === 'number') return true;
12 | if (typeof num === 'string') {
13 | return !Number.isNaN(Number(num));
14 | }
15 | return false;
16 | }
17 |
18 | export function isCssLength(str) {
19 | if (typeof str !== 'string') return false;
20 | return str.match(/^([0-9])*(%|px|rem|em)$/i);
21 | }
22 |
23 | // 深度对比
24 | export function isDeepEqual(param1, param2) {
25 | if (param1 === undefined && param2 === undefined) return true;
26 | else if (param1 === undefined || param2 === undefined) return false;
27 | else if (param1.constructor !== param2.constructor) return false;
28 |
29 | if (param1.constructor === Array) {
30 | if (param1.length !== param2.length) return false;
31 | for (let i = 0; i < param1.length; i++) {
32 | if (param1[i].constructor === Array || param1[i].constructor === Object) {
33 | if (!isDeepEqual(param1[i], param2[i])) return false;
34 | } else if (param1[i] !== param2[i]) return false;
35 | }
36 | } else if (param1.constructor === Object) {
37 | if (Object.keys(param1).length !== Object.keys(param2).length) return false;
38 | for (let i = 0; i < Object.keys(param1).length; i++) {
39 | const key = Object.keys(param1)[i];
40 | if (
41 | param1[key] &&
42 | typeof param1[key] !== 'number' &&
43 | (param1[key].constructor === Array ||
44 | param1[key].constructor === Object)
45 | ) {
46 | if (!isDeepEqual(param1[key], param2[key])) return false;
47 | } else if (param1[key] !== param2[key]) return false;
48 | }
49 | } else if (param1.constructor === String || param1.constructor === Number) {
50 | return param1 === param2;
51 | }
52 | return true;
53 | }
54 |
55 | export const getSaveNumber = () => {
56 | const searchStr = localStorage.getItem('SAVES');
57 | if (searchStr) {
58 | try {
59 | const saves = JSON.parse(searchStr);
60 | const length = saves.length;
61 | if (length) return length + 1;
62 | } catch (error) {
63 | return 1;
64 | }
65 | } else {
66 | return 1;
67 | }
68 | };
69 |
70 | export function looseJsonParse(obj) {
71 | return Function('"use strict";return (' + obj + ')')();
72 | }
73 |
74 | export function getFormat(format) {
75 | let dateFormat;
76 | switch (format) {
77 | case 'date':
78 | dateFormat = 'YYYY-MM-DD';
79 | break;
80 | case 'time':
81 | dateFormat = 'HH:mm:ss';
82 | break;
83 | default:
84 | // dateTime
85 | dateFormat = 'YYYY-MM-DD HH:mm:ss';
86 | }
87 | return dateFormat;
88 | }
89 |
90 | export function hasRepeat(list) {
91 | return list.find(
92 | (x, i, self) =>
93 | i !== self.findIndex(y => JSON.stringify(x) === JSON.stringify(y)),
94 | );
95 | }
96 |
--------------------------------------------------------------------------------
/src/FR/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useGlobalProps, useStore } from '../hooks';
3 | import RenderChildren from './RenderChildren';
4 | import RenderField from './RenderField';
5 | import Wrapper from './Wrapper';
6 |
7 | const FR = ({ id = '#', preview = false, isPc = true }) => {
8 | const { flatten } = useStore();
9 | const { displayType, column } = useGlobalProps();
10 | const item = flatten[id];
11 | if (!item) return null;
12 |
13 | const { schema } = item;
14 | const isObj = schema.type === 'object';
15 | const isList = schema.type === 'array' && schema.enum === undefined;
16 | const isComplex = isObj || isList;
17 | let containerClass = `fr-field w-100 ${isComplex ? 'fr-field-complex' : ''}`;
18 | let labelClass = 'fr-label mb2';
19 | let contentClass = 'fr-content';
20 |
21 | let columnStyle = { paddingRight: '12px' };
22 |
23 | if (!isComplex && column > 1) {
24 | columnStyle = {
25 | width: `calc(100% /${column})`,
26 | paddingRight: '12px',
27 | };
28 | }
29 |
30 | switch (schema.type) {
31 | case 'object':
32 | // if (schema.title) {
33 | // containerClass += ' ba b--black-20 pt4 pr3 pb2 relative mt3 mb4'; // object的margin bottom由内部元素撑起
34 | // labelClass += ' fr-label-object bg-white absolute ph2 top-upper left-1'; // fr-label-object 无默认style,只是占位用于使用者样式覆盖
35 | // }
36 | // containerClass += ' fr-field-object'; // object的margin bottom由内部元素撑起
37 | // if (schema.title) {
38 | // contentClass += ' ml3'; // 缩进
39 | // }
40 | if (schema.title && !schema.enum) {
41 | labelClass += ' mt2 mb3';
42 | }
43 | break;
44 | case 'array':
45 | if (schema.title && !schema.enum) {
46 | labelClass += ' mt2 mb3';
47 | }
48 | break;
49 | default:
50 | if (displayType === 'row') {
51 | labelClass = labelClass.replace('mb2', 'mb0');
52 | }
53 | }
54 | // 横排时
55 | if (displayType === 'row' && !isComplex) {
56 | containerClass += ' flex items-center';
57 | labelClass += ' flex-shrink-0 fr-label-row';
58 | labelClass = labelClass.replace('mb2', 'mb0');
59 | contentClass += ' flex-grow-1 relative';
60 | }
61 |
62 | // 横排的checkbox
63 | // if (displayType === 'row') {
64 | // contentClass += ' flex justify-end pr2';
65 | // }
66 |
67 | const fieldProps = {
68 | $id: id,
69 | item,
70 | labelClass,
71 | contentClass,
72 | isComplex,
73 | };
74 | const childrenProps = {
75 | children: item.children,
76 | preview,
77 | };
78 |
79 | const childrenElement =
80 | item.children && item.children.length > 0 ? (
81 |
84 | ) : null;
85 |
86 | // TODO: list 也要算进去
87 | if (preview) {
88 | return (
89 |
90 |
91 | {(isObj || isList) && childrenElement}
92 |
93 |
94 | );
95 | }
96 |
97 | const isEmpty = Object.keys(flatten).length < 2; // 只有一个根元素 # 的情况
98 | if (isEmpty) {
99 | return (
100 |
101 |
104 | 点击/拖拽左侧栏的组件进行添加
105 |
106 |
107 | );
108 | }
109 |
110 | return (
111 |
112 |
113 |
114 | {(isObj || isList) && (
115 |
116 | {childrenElement || }
117 |
118 | )}
119 |
120 |
121 |
122 | );
123 | };
124 |
125 | export default FR;
126 |
--------------------------------------------------------------------------------
/src/FR/RenderField.js:
--------------------------------------------------------------------------------
1 | import React, {
2 | useCallback,
3 | useMemo
4 | } from 'react';
5 | import { widgets } from '../widgets';
6 | import { useGlobalProps, useStore } from '../hooks';
7 | import { getParentProps } from '../utils/schema';
8 | import { isLooselyNumber, isCssLength } from '../utils/common';
9 | import { getWidgetName } from '../mapping';
10 | import 'antd-mobile/dist/antd-mobile.css';
11 |
12 | const RenderField = ({
13 | $id,
14 | item,
15 | labelClass,
16 | contentClass,
17 | isComplex,
18 | children,
19 | }) => {
20 | const { onItemChange } = useStore();
21 | const { schema, data } = item;
22 | const {
23 | displayType,
24 | showDescIcon,
25 | showValidate,
26 | Widgets,
27 | isPc,
28 | mapping,
29 | disabled,
30 | readOnly
31 | } = useGlobalProps();
32 | const { type, title, description, required } = schema;
33 | const _widgets = useMemo(() => {
34 | const widgetType = isPc ? 'pc' : 'mobile';
35 |
36 | return {
37 | ...widgets[widgetType],
38 | ...Widgets
39 | };
40 | }, [isPc]);
41 |
42 | let labelStyle = { width: 120 };
43 | if (isComplex || displayType === 'column') {
44 | labelStyle = { flexGrow: 1 };
45 | }
46 |
47 | const onChange = useCallback(value => {
48 | onItemChange($id, {
49 | ...item,
50 | data: value
51 | });
52 | }, [item, onItemChange]);
53 |
54 | const Widget = useMemo(() => {
55 | // TODO 是否要强制加上x-component
56 | let widgetName = getWidgetName(schema, mapping);
57 | // 如果不存在,比如有外部的自定义组件名称,使用默认展示组件
58 | if (!(widgetName && _widgets[widgetName])) {
59 | widgetName = getWidgetName(schema, mapping, false);
60 | }
61 |
62 | return _widgets[widgetName];
63 | }, [schema['x-component'], _widgets]);
64 |
65 | // TODO: useMemo
66 | const usefulWidgetProps = {
67 | disabled: schema['disabled'] || disabled,
68 | readOnly: schema['readOnly'] || readOnly,
69 | visible: schema['visible'],
70 | options: schema['x-component-props'],
71 | };
72 |
73 | if (!Widget) {
74 | return children;
75 | }
76 |
77 | if (!isPc) {
78 | return (
79 |
86 | );
87 | }
88 |
89 | return (
90 | <>
91 | {schema.title ? (
92 |
93 |
123 |
124 | ) : null}
125 |
126 |
133 |
134 | >
135 | );
136 | };
137 |
138 | export default RenderField;
139 |
--------------------------------------------------------------------------------
/src/hooks.js:
--------------------------------------------------------------------------------
1 | import { useReducer, useContext, useRef, useEffect, useState } from 'react';
2 | import { Ctx, InnerCtx, PropsCtx } from './context';
3 |
4 | // 使用最顶层组件的 setState
5 | export const useGlobal = () => {
6 | return useContext(Ctx);
7 | };
8 |
9 | export const useGlobalProps = () => {
10 | return useContext(PropsCtx);
11 | };
12 |
13 | export const useStore = () => {
14 | return useContext(InnerCtx);
15 | };
16 |
17 | // const logger = reducer => {
18 | // const reducerWithLogger = (state, action, actionName = 'Action') => {
19 | // console.group(actionName);
20 | // console.log('%cState:', 'color: #9E9E9E; font-weight: 500;', state);
21 | // console.log('%cAction:', 'color: #00A7F7; font-weight: 500;', action);
22 | // console.log('%cNext:', 'color: #47B04B; font-weight: 500;', {
23 | // ...state,
24 | // ...action,
25 | // });
26 | // console.groupEnd();
27 | // return reducer(state, action);
28 | // };
29 | // return reducerWithLogger;
30 | // };
31 |
32 | // export default logger;
33 |
34 | // 类似于class component的setState
35 | export const useSet = initState => {
36 | const [state, setState] = useReducer((state, newState) => {
37 | let action = newState;
38 | if (typeof newState === 'function') {
39 | action = action(state);
40 | }
41 | if (newState.action && newState.payload) {
42 | action = newState.payload;
43 | if (typeof action === 'function') {
44 | action = action(state);
45 | }
46 | }
47 | const result = { ...state, ...action };
48 | // if (newState.action !== 'no-log') {
49 | // console.group(newState.action || 'action'); // TODO: give it a name
50 | // console.log('%cState:', 'color: #9E9E9E; font-weight: 700;', state);
51 | // console.log('%cAction:', 'color: #00A7F7; font-weight: 700;', action);
52 | // console.log('%cNext:', 'color: #47B04B; font-weight: 700;', result);
53 | // console.groupEnd();
54 | // } else {
55 | // }
56 | return result;
57 | }, initState);
58 | const setStateWithActionName = (state, actionName) => {
59 | setState(state);
60 | };
61 | return [state, setStateWithActionName];
62 | };
63 |
64 | // start: true 开始、false 暂停
65 | export function useInterval(callback, delay, start) {
66 | const savedCallback = useRef();
67 | // Remember the latest callback.
68 | useEffect(() => {
69 | savedCallback.current = callback;
70 | }, [callback]);
71 |
72 | // Set up the interval.
73 | const id = useRef();
74 | useEffect(() => {
75 | if (!start) {
76 | return;
77 | }
78 | function tick() {
79 | savedCallback && savedCallback.current && savedCallback.current();
80 | }
81 | tick();
82 | if (delay !== null) {
83 | id.current = setInterval(tick, delay);
84 | return () => clearInterval(id.current);
85 | }
86 | }, [delay, start]);
87 | return () => clearInterval(id.current);
88 | }
89 |
90 | export function usePrevious(value) {
91 | // The ref object is a generic container whose current property is mutable ...
92 | // ... and can hold any value, similar to an instance property on a class
93 | const ref = useRef();
94 |
95 | // Store current value in ref
96 | useEffect(() => {
97 | ref.current = value;
98 | }, [value]); // Only re-run if value changes
99 |
100 | // Return previous value (happens before update in useEffect above)
101 | return ref.current;
102 | }
103 |
104 | export const useShowOnce = localKey => {
105 | // 从 localStorage 读取 key 值
106 | const [show, setShow] = useState(false);
107 | let localStr;
108 | try {
109 | localStr = localStorage.getItem(localKey);
110 | } catch (error) {}
111 | if (!localStr) {
112 | setShow(true);
113 | localStorage.setItem(localKey, JSON.stringify(true));
114 | }
115 | return show;
116 | };
117 |
118 | export const useModal = () => {
119 | const [show, setShow] = useState(false);
120 | const toggle = () => setShow(!show);
121 | return [show, toggle];
122 | };
123 |
124 | export const useWindowState = initState => {
125 | const [state, setState] = useState(initState);
126 | return [state, setState];
127 | };
128 |
129 | export const useStorageState = (initState = {}, searchKey = 'SAVES') => {
130 | // 从 localStorage 读取 search 值
131 | const readSearchFromStorage = () => {
132 | const searchStr = localStorage.getItem(searchKey);
133 | if (searchStr) {
134 | try {
135 | return JSON.parse(searchStr);
136 | } catch (error) {
137 | return initState;
138 | }
139 | }
140 | return initState;
141 | };
142 | const [data, setData] = useState(readSearchFromStorage());
143 | // 存储搜索值到 localStorage
144 | const setSearchWithStorage = search => {
145 | setData(search);
146 | localStorage.setItem(searchKey, JSON.stringify(search));
147 | };
148 | return [data, setSearchWithStorage];
149 | };
150 |
--------------------------------------------------------------------------------
/src/FRWrapper.js:
--------------------------------------------------------------------------------
1 | import React, {
2 | useRef,
3 | useMemo,
4 | useCallback,
5 | forwardRef,
6 | useImperativeHandle,
7 | } from 'react';
8 | import cn from 'classnames';
9 | import { useSet } from './hooks';
10 | import Frame from 'react-frame-component'
11 | import { iframeSrcDoc } from './utils/iframe';
12 |
13 | import IconFont from './components/IconFont';
14 | import Left from './Left';
15 | import Right from './Right';
16 | import {
17 | flattenSchema,
18 | idToSchema,
19 | dataToFlatten,
20 | flattenToData,
21 | } from './utils/schema';
22 | import { getSaveNumber, looseJsonParse } from './utils/common';
23 | import { Ctx, PropsCtx, InnerCtx } from './context';
24 | import FR from './FR';
25 | import FrameBindingContext from './FR/FrameBindingContext';
26 | import { Modal, Input, message, Tooltip } from 'antd';
27 | import { Button } from 'antd';
28 |
29 | // import 'tachyons';
30 | import './FRWrapper.css';
31 |
32 | const { TextArea } = Input;
33 |
34 | function Wrapper(
35 | {
36 | simple = true,
37 | schema,
38 | formData,
39 | onChange,
40 | onSchemaChange,
41 | className = '',
42 | title = 表单设计器
,
43 | actions = [],
44 | onSave = () => {},
45 | ...globalProps
46 | },
47 | ref,
48 | ) {
49 | const {
50 | preview,
51 | isPc,
52 | setState,
53 | mapping,
54 | selected,
55 | hovering,
56 | ...rest
57 | } = globalProps;
58 | const flatten = useMemo(() => flattenSchema(schema || {}), [schema]);
59 | const flattenWithData = useMemo(() => dataToFlatten(flatten, formData), [flatten, formData]);
60 |
61 | const onFlattenChange = useCallback(newFlatten => {
62 | const newSchema = idToSchema(newFlatten);
63 | const newData = flattenToData(newFlatten);
64 | // 判断只有schema变化时才调用,一般需求的用户不需要
65 | if (onSchemaChange) {
66 | onSchemaChange(newSchema);
67 | }
68 | onChange(newData);
69 | }, [onSchemaChange, onChange]);
70 |
71 | const onItemChange = useCallback((key, value) => {
72 | flattenWithData[key] = value;
73 | onFlattenChange(flattenWithData);
74 | }, [onFlattenChange, flattenWithData]);
75 |
76 | const onEmpty = () => {
77 | setState({
78 | schema: {
79 | type: 'object',
80 | properties: {},
81 | },
82 | formData: {},
83 | selected: undefined,
84 | });
85 | };
86 |
87 | useImperativeHandle(ref, () => ({
88 | getValue: () => {
89 | return idToSchema(flattenWithData, '#', true);
90 | },
91 | getSelectedItem: () => {
92 | return flatten[selected];
93 | }
94 | }));
95 |
96 | // TODO: flatten是频繁在变的,应该和其他两个函数分开
97 | const store = {
98 | flatten: flattenWithData,
99 | onFlattenChange,
100 | onItemChange,
101 | ...globalProps,
102 | };
103 |
104 | if (simple) {
105 | return (
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 | );
114 | }
115 |
116 | const renderIframe = () => {
117 | return (
118 |
119 |
{
123 | const iframe = document.querySelector('#dnd-iframe');
124 | const head = document.head.cloneNode(true);
125 | const contentDocument = iframe.contentDocument;
126 | contentDocument.head.remove();
127 | contentDocument.documentElement.insertBefore(head, contentDocument.body);
128 | // TODO 需要处理样式延迟加载问题
129 | // iframe.style.display = 'block';
130 | }}
131 | >
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 | {
147 | setState({ isPc: !isPc });
148 | }}
149 | />
150 |
151 |
152 | );
153 | }
154 |
155 | const renderTitle = () => {
156 | return (
157 | {title}
158 | );
159 | }
160 |
161 | const renderButtonGroup = () => {
162 | return (
163 |
164 | {
165 | actions.map((item, index) => {
166 | const { onClick = () => {}, label, ...otherProps } = item;
167 | return (
168 |
176 | );
177 | })
178 | }
179 |
188 |
195 |
204 |
205 | );
206 | }
207 |
208 | return (
209 |
210 |
211 |
212 |
213 |
214 | {renderTitle()}
215 | {renderButtonGroup()}
216 |
217 |
218 |
219 |
220 | {renderIframe()}
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 | );
229 | }
230 |
231 | const FRWrapper = forwardRef(Wrapper);
232 |
233 | FRWrapper.defaultProps = {
234 | };
235 |
236 | export default FRWrapper;
237 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | /*
2 | 用于原有样式的覆盖
3 | */
4 | .top-upper {
5 | top: -12px;
6 | }
7 |
8 | .fr-set {
9 | padding: 12px 14px 12px 12px;
10 | margin-bottom: 12px;
11 | border-radius: 4px;
12 | }
13 |
14 | .fr-field {
15 | margin-bottom: 24px;
16 | }
17 |
18 | .fr-field-object {
19 | /* margin-bottom: 0; */
20 | }
21 |
22 | .fr-label {
23 | display: block;
24 | }
25 |
26 | .fr-label-title {
27 | display: inline-flex;
28 | color: #333;
29 | font-size: 14px;
30 | min-height: 22px; /* ""的标签页占位 */
31 | line-height: 22px;
32 | }
33 |
34 | .fr-label-required {
35 | margin: 1px 4px 0 0;
36 | color: #f5222d;
37 | font-size: 14px;
38 | font-family: SimSun, sans-serif;
39 | }
40 |
41 | .fr-label-title::after {
42 | content: ':';
43 | position: relative;
44 | top: -0.5px;
45 | margin: 0 10px 0 2px;
46 | }
47 |
48 | .fr-label-title.no-colon::after {
49 | content: '';
50 | margin: 0;
51 | }
52 |
53 | .fr-label-object .fr-label-title {
54 | font-size: 16px;
55 | color: #222;
56 | }
57 |
58 | .fr-desc {
59 | margin-top: 3px;
60 | font-size: 12px;
61 | word-break: break-all;
62 | color: #888;
63 | }
64 |
65 | .fr-validate {
66 | margin-left: 12px;
67 | font-size: 12px;
68 | word-break: break-all;
69 | color: #f5222d;
70 | }
71 |
72 | /* Row */
73 |
74 | .fr-validate-row {
75 | margin: 3px 0 0 0;
76 | }
77 |
78 | .fr-label-row {
79 | text-align: right;
80 | flex-shrink: 0;
81 | }
82 |
83 | .fr-field-row .fr-content {
84 | flex: 1;
85 | position: relative;
86 | }
87 |
88 | .fr-field-row .fr-tooltip-icon {
89 | margin: 3px 2px 0 0;
90 | }
91 |
92 | /* 自定义类 */
93 | .hover-b--black-20:hover {
94 | border-color: rgba(0, 0, 0, 0.3);
95 | }
96 |
97 | .pt44 {
98 | padding-top: 46px;
99 | }
100 |
101 | .pv12 {
102 | padding-top: 12px;
103 | padding-bottom: 12px;
104 | }
105 |
106 | .fr-move-icon {
107 | position: absolute;
108 | top: 0;
109 | right: 0;
110 | padding-top: 2px;
111 | padding-right: 10px;
112 | font-size: 24px;
113 | font-weight: 300;
114 | }
115 |
116 | .fr-move-icon:hover {
117 | cursor: move;
118 | }
119 |
120 | /* 组件内部样式*/
121 |
122 | .fr-color-picker {
123 | width: 100%;
124 | display: flex;
125 | flex-direction: row;
126 | align-items: center;
127 | color: #666;
128 | }
129 |
130 | .fr-color-picker .rc-color-picker-trigger {
131 | margin-right: 12px;
132 | height: 30px;
133 | width: 60px;
134 | border: 1px solid #e5e5e5;
135 | }
136 |
137 | .fr-color-picker > p {
138 | margin: 0;
139 | font-size: 14px;
140 | line-height: 28px;
141 | }
142 |
143 | .fr-color-picker .rc-color-picker-wrap {
144 | display: flex;
145 | }
146 |
147 | .next-input,
148 | .next-number-picker {
149 | width: 100%;
150 | }
151 |
152 | .upload-img {
153 | max-width: 200px;
154 | max-height: 200px;
155 | margin-right: 24px;
156 | }
157 |
158 | .fr-preview-image {
159 | width: 160px;
160 | }
161 |
162 | .fr-preview {
163 | position: relative;
164 | cursor: pointer;
165 | }
166 |
167 | .fr-upload-mod,
168 | .fr-upload-file {
169 | display: flex;
170 | }
171 | .fr-upload-mod {
172 | align-items: center;
173 | }
174 | .fr-upload-mod .fr-upload-preview {
175 | margin: 0 12px;
176 | }
177 | .fr-upload-file .ant-upload-list-item {
178 | margin: 5px 0 0 8px;
179 | }
180 | .fr-upload-file .ant-upload-list-item-name {
181 | margin-right: 6px;
182 | }
183 | .fr-upload-file .ant-upload-list-item-info {
184 | cursor: pointer;
185 | }
186 | .fr-upload-file .next-upload-list-text .next-upload-list-item-done,
187 | .fr-upload-file .next-upload-list-text .next-upload-list-item .next-icon {
188 | height: 28px;
189 | line-height: 28px;
190 | margin-left: 12px;
191 | }
192 |
193 | .fr-upload-file .next-upload-list-item-name-wrap {
194 | margin-top: -4px;
195 | }
196 |
197 | .fr-sort-help-class {
198 | background: #fff;
199 | }
200 |
201 | /* 其他样式 */
202 |
203 | .fold-icon.fold-icon-active {
204 | transform: rotate(0deg);
205 | }
206 |
207 | .fold-icon {
208 | transform: rotate(-90deg);
209 | transition: transform 0.24s;
210 | cursor: pointer;
211 | position: relative;
212 | }
213 |
214 | .fold-icon::after {
215 | content: '';
216 | position: absolute;
217 | top: -20px;
218 | right: -10px;
219 | bottom: -5px;
220 | left: -20px;
221 | }
222 |
223 | .fr-tooltip-toggle {
224 | cursor: pointer;
225 | position: relative;
226 | }
227 |
228 | .fr-tooltip-toggle:hover .fr-tooltip-container {
229 | opacity: 1;
230 | visibility: visible;
231 | }
232 |
233 | .fr-tooltip-icon {
234 | height: 14px;
235 | width: 14px;
236 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAEnUlEQVR42u1bS0iUURT+zd4Y9KDosSihNhYUaBE9YKpFDKKOOmOLahtJ2SoxA2fUdUoQJK5qU0ib6LlxUYJpkY0LE3tQZkU7y7A0e1jnA4Of8pz//5373/+BF44Oc+/c853v3nOf5xpup0QiMZ9kD8mxeDzeTHKXPveSvKXP3+j/GP1/h++QhzIoW1ZWthu/NYKYysvLN5EhKZJ7ZMQEye8ZyjjJfZIk6vS10YWFhcvI4FoC2wfwLkkf6aiBLt8YHovFNhCoFnRlgNQkY9BJbrLeS9uzqFueYAzXJV9ITgKLVsvBPCnuAAifSIe23kBd7zApHHUI8D3JBfptFUlialbYSJIDweep7+Iog7L4jUMdn4HNzeksmxRcdgColyRZWlq6NQOd21AH6rKrFxiBVanxxcXFS6jSdpsAHtLYUODC1FqAum0S0Q7Mqlp+ESlO2zD8DUmJBhcsgS4bJDwBdhUKb1ko+kll6qLR6AJDU4IuuAZ0W2C7kWnrJy1a/QONvjsNjxLp32VjsEzO1OcOkoJJoeJuGuBWGB4nYCCcD4RGmiSJOm35PIup7kokEplr+CQBCzAJeEeJqC22V3jE2IBQWVd+fv48w2cJmAhbp4C7H7bZaf1KoZJBbET8vCEDRgF/pdXGZim1/ifGl344WtTIJJ//B+ggts+K6t4BrAwBH2GjNKo2CuxVqzJeGLDqFemoFnQ0sq0vDHyP4D+KwI1ILqZqpyqsGmFjznStX8tNIzQlbjYUJasVnCo9wAzsjE1npyPgFQPqJvKDRYC8ioWt/200BFD7FRMwIu0nFBNwgNMFm80Fz0mAFIOqVz4IyoS/ZvQ1mQv1MYBqkO8GCSDXTDRjvAoCznAHrH/X0qu4FqE99Voj2AkErOPsg+3w/wqmwDMjHAkkPGdsPITMBqb7t4SFANjCjTkgoI0ZJY+Hwnp5f9MGdtIMOxF35n1ZXDrG38fYmDa48zWcs4eFANxesUtv+jPMZC4PCwE4NWL0DRvcehkHjyEhACdGCxl9vwBwggEyxw0wXhCAOANG34TsAi4k37kA/RliMnND1ANyGX1DGAMe89Og+uSjaRDSAwKucgeIYSEAcQzc8T4ISDGZrSFygVaml6dAQILJfBEiAl4yy/0KZK6WtsMBJwD+v0bcDk/tlp4yveB0cAmQj8hhs7lQE1NoIAQEPGdsazZ3k+3SoahOAvQfisoDBeR2QAmArjvcsbi2i5FMQ+Ay0Jvn6GIE10VWV2MBIiALmKWrMfFylJsRgkIAsAp1NojX47hCZn74HdfjficAAzp3PY6rf9g4GyBhw3/6Axoi0yW48IDtcQwBRVIUOHaQfguSAiYpvB42Od1Cxiz8s5tkpdfGAwOwSFhhy0xXUnV+DpSEbmCwwFiXKcPXbYTKpnSHykIndFsYf01JsDRJTxCDpUGU7nB5SKcfwuWLiooWK38wQXLJwcOFtIoHE/Bh5v6SE2DMdvNN4BE3nszQ93szfTJDclTbU7lAP5pSGIxY5eWzOdL/lXrkKW6Fp7M3XPTi4SR0zz6dnX087fOEq2k8hTc/nydJm57Pj3v5fP4PSqRR6oYkTaUAAAAASUVORK5CYII=');
237 | background-size: cover;
238 | display: block;
239 | margin: 4px 0 0 4px;
240 | }
241 |
242 | .fr-tooltip-container {
243 | position: absolute;
244 | width: 160px;
245 | left: 50%;
246 | white-space: initial !important;
247 | top: -34px;
248 | text-align: center;
249 | background: #2b222a;
250 | padding: 4px;
251 | margin-left: -77px;
252 | border-radius: 4px;
253 | color: #efefef;
254 | font-size: 13px;
255 | cursor: auto;
256 | z-index: 99999;
257 | transition: all 0.5s ease;
258 | opacity: 0;
259 | visibility: hidden;
260 | }
261 |
262 | .fr-tooltip-triangle {
263 | position: absolute;
264 | left: 50%;
265 | border-left: 5px solid transparent;
266 | border-right: 5px solid transparent;
267 | border-top: 5px solid #2b222a;
268 | transition: all 0.5s ease;
269 | content: ' ';
270 | font-size: 0;
271 | line-height: 0;
272 | margin-left: -5px;
273 | width: 0;
274 | bottom: -5px;
275 | }
276 |
277 | .fr-tooltip-toggle::before,
278 | .fr-tooltip-toggle::after {
279 | color: #efefef;
280 | font-size: 13px;
281 | opacity: 0;
282 | pointer-events: none;
283 | text-align: center;
284 | }
285 |
286 | .fr-tooltip-toggle:focus::before,
287 | .fr-tooltip-toggle:focus::after,
288 | .fr-tooltip-toggle:hover::before,
289 | .fr-tooltip-toggle:hover::after {
290 | opacity: 1;
291 | transition: all 0.75s ease;
292 | }
293 |
294 | .fr-slider {
295 | display: flex;
296 | width: 100%;
297 | align-items: center;
298 | }
299 |
300 | .fr-map {
301 | display: flex;
302 | flex-wrap: wrap;
303 | }
304 |
--------------------------------------------------------------------------------
/src/widgets/pc/list.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import * as Icons from '@ant-design/icons';
4 | import { Button } from 'antd';
5 | import {
6 | arrayMove,
7 | } from 'react-sortable-hoc';
8 | import FoldIcon from '../../components/foldIcon';
9 | import DescriptionList, { getDescription } from '../../components/descList';
10 |
11 | class ListItem extends React.Component {
12 | componentDidMount() {
13 | const { p = {}, name, fold } = this.props;
14 | const description = getDescription({
15 | schema: p.schema,
16 | value: p.value,
17 | index: name,
18 | });
19 | // 如果第一个值不为空,则收起
20 | // 新增的值为0,不折叠
21 | const hasValue = description && description[0] && description[0].text;
22 | if (hasValue && fold !== 0) {
23 | this.props.toggleFoldItem(name);
24 | }
25 | }
26 |
27 | toggleFold = () => {
28 | this.props.toggleFoldItem(this.props.name);
29 | };
30 |
31 | render() {
32 | const { item, p = {}, name, fold } = this.props;
33 | const descProps = { ...p, index: name };
34 | const { options = {}, readOnly, formData, value: rootValue } = p;
35 | const { foldable: canFold } = options;
36 | let { itemButtons } = options;
37 |
38 | // 只有当items为object时才做收起(fold)处理
39 | const isObj = p.schema.items && p.schema.items.type == 'object';
40 | let setClass =
41 | 'fr-set ba b--black-10 hover-b--black-20 relative flex flex-column';
42 | if (canFold && fold) {
43 | setClass += ' pv12';
44 | } else if (p.displayType === 'row') {
45 | setClass += ' pt44';
46 | }
47 | return (
48 |
49 | {canFold && fold && isObj ? : item}
50 | {canFold && (
51 |
56 | )}
57 | {!((canFold && fold) || readOnly) && (
58 |
59 | {
63 | const value = [...p.value];
64 | value.splice(name, 1);
65 | p.onChange(value);
66 | }}
67 | >
68 | 删除
69 |
70 | {itemButtons &&
71 | itemButtons.length > 0 &&
72 | itemButtons.map((btn, idx) => {
73 | return (
74 | {
80 | const value = [...p.value];
81 | if (typeof window[btn.callback] === 'function') {
82 | const result = window[btn.callback](value, name); // eslint-disable-line
83 | p.onChange(result);
84 | }
85 | }}
86 | >
87 | {btn.text || ''}
88 |
89 | );
90 | })}
91 |
92 | )}
93 |
94 | );
95 | }
96 | }
97 |
98 | class FieldList extends React.Component {
99 | handleAddClick = () => {
100 | const { p, addUnfoldItem } = this.props;
101 | const value = [...p.value];
102 | value.push(p.newItem);
103 | p.onChange(value);
104 | addUnfoldItem();
105 | };
106 |
107 | render() {
108 | const { p, foldList = [], toggleFoldItem } = this.props;
109 | const { options = {}, extraButtons = {} } = p || {};
110 | // prefer x-component-props/buttons to ui:extraButtons, but keep both for backwards compatibility
111 | const buttons = options.buttons || extraButtons || [];
112 | const { readOnly, schema = {} } = p;
113 | const { maxItems } = schema;
114 | const list = p.value || [];
115 | const canAdd = maxItems ? maxItems > list.length : true; // 当到达最大个数,新增按钮消失
116 |
117 | return (
118 |
119 | {list.map((_, name) => (
120 |
129 | ))}
130 | {!readOnly && (
131 |
132 |
133 | 新增
134 |
135 | {buttons &&
136 | buttons.length > 0 &&
137 | buttons.map((item, i) => (
138 | {
143 | if (item.callback === 'clearAll') {
144 | p.onChange([]);
145 | return;
146 | }
147 | if (item.callback === 'copyLast') {
148 | const value = [...p.value];
149 | const lastIndex = value.length - 1;
150 | value.push(lastIndex > -1 ? value[lastIndex] : p.newItem);
151 | p.onChange(value);
152 | return;
153 | }
154 | if (typeof window[item.callback] === 'function') {
155 | const value = [...p.value];
156 | const onChange = value => p.onChange(value);
157 | window[item.callback](value, onChange, p.newItem); // eslint-disable-line
158 | }
159 | }}
160 | >
161 | {item.text}
162 |
163 | ))}
164 |
165 | )}
166 |
167 | );
168 | }
169 | }
170 |
171 | class list extends React.Component {
172 | static propTypes = {
173 | value: PropTypes.array,
174 | };
175 |
176 | static defaultProps = {
177 | value: [null], // list需要默认加一项
178 | };
179 |
180 | constructor(props) {
181 | super(props);
182 | const len = this.props.value.length || 0;
183 | this.state = {
184 | foldList: new Array(len).fill(false) || [],
185 | };
186 | }
187 |
188 | // 新添加的item默认是展开的
189 | addUnfoldItem = () =>
190 | this.setState({
191 | foldList: [...this.state.foldList, 0],
192 | });
193 |
194 | toggleFoldItem = index => {
195 | const { foldList = [] } = this.state;
196 | foldList[index] = !foldList[index]; // TODO: need better solution for the weird behavior caused by setState being async
197 | this.setState({
198 | foldList,
199 | });
200 | };
201 |
202 | handleSort = ({ oldIndex, newIndex }) => {
203 | const { onChange, value } = this.props;
204 | onChange(arrayMove(value, oldIndex, newIndex));
205 | this.setState({
206 | foldList: arrayMove(this.state.foldList, oldIndex, newIndex),
207 | });
208 | };
209 |
210 | render() {
211 | const { foldList } = this.state;
212 | return (
213 |
219 | );
220 | }
221 | }
222 |
223 | function FrButton({ icon, children, ...rest }) {
224 | let iconName;
225 | switch (icon) {
226 | case 'add':
227 | iconName = 'PlusCircleOutlined';
228 | break;
229 | case 'delete':
230 | iconName = 'DeleteOutlined';
231 | break;
232 | default:
233 | iconName = icon;
234 | break;
235 | }
236 | const IconComponent = Icons[iconName];
237 | if (IconComponent) {
238 | return (
239 | }>
240 | {children}
241 |
242 | );
243 | }
244 | return ;
245 | }
246 |
247 | export default list;
248 |
--------------------------------------------------------------------------------
/src/FR/Wrapper.js:
--------------------------------------------------------------------------------
1 | import React, { useRef, useState } from 'react';
2 | import { DeleteOutlined, CopyOutlined, DragOutlined } from '@ant-design/icons';
3 | import { useGlobal, useGlobalProps, useStore } from '../hooks';
4 | import { copyItem, getKeyFromUniqueId, dropItem } from '../utils/schema';
5 | import { useDrag, useDrop } from 'react-dnd';
6 |
7 | import './Wrapper.css';
8 |
9 | export default function Wrapper({
10 | $id,
11 | item,
12 | inside = false,
13 | children,
14 | style,
15 | }) {
16 | const [position, setPosition] = useState();
17 | const { flatten, onFlattenChange } = useStore();
18 | const setGlobal = useGlobal();
19 | const { selected, hovering, beforeDragComplete } = useGlobalProps();
20 | const { schema } = item;
21 | const { type } = schema;
22 | const boxRef = useRef(null);
23 |
24 | const [{ isDragging }, dragRef, dragPreview] = useDrag({
25 | item: { type: 'box', $id: inside ? 0 + $id : $id },
26 | end: (item, monitor) => {
27 | const dropResult = monitor.getDropResult();
28 | if (item && dropResult) {
29 | // alert(`You dropped into ${dropResult.name}!`);
30 | }
31 | },
32 | collect: monitor => ({
33 | isDragging: monitor.isDragging(),
34 | }),
35 | });
36 |
37 | const [{ canDrop, isOver }, dropRef] = useDrop({
38 | accept: 'box',
39 | drop: (item, monitor) => {
40 | // 如果children已经作为了drop target,不处理
41 | // console.log('monitor', monitor);
42 |
43 | const didDrop = monitor.didDrop();
44 | if (didDrop) {
45 | return;
46 | }
47 |
48 | if (!beforeDragComplete(schema)) {
49 | return;
50 | }
51 |
52 | const [newFlatten, newId] = dropItem({
53 | dragId: item.$id, // 内部拖拽用dragId
54 | dragItem: item.dragItem, // 从左边栏过来的,用dragItem
55 | dropId: $id,
56 | position,
57 | flatten,
58 | });
59 | onFlattenChange(newFlatten);
60 | setGlobal({ selected: newId });
61 | return;
62 | },
63 | hover: (item, monitor) => {
64 | // 只检查被hover的最小元素
65 | const didHover = monitor.isOver({ shallow: true });
66 | if (didHover) {
67 | // Determine rectangle on screen
68 | const hoverBoundingRect =
69 | boxRef.current && boxRef.current.getBoundingClientRect();
70 | // Get vertical middle
71 | const hoverMiddleY =
72 | (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
73 | // Determine mouse position
74 | // const clientOffset = monitor.getClientOffset();
75 | const dragOffset = monitor.getSourceClientOffset();
76 | // Get pixels to the top
77 | const hoverClientY = dragOffset.y - hoverBoundingRect.top;
78 | // Only perform the move when the mouse has crossed half of the items height
79 | // When dragging downwards, only move when the cursor is below 50%
80 | // When dragging upwards, only move when the cursor is above 50%
81 | // Dragging downwards
82 | if (inside) {
83 | setPosition('inside');
84 | } else {
85 | if (hoverClientY <= hoverMiddleY) {
86 | setPosition('up');
87 | }
88 | // Dragging upwards
89 | if (hoverClientY > hoverMiddleY) {
90 | setPosition('down');
91 | }
92 | }
93 | }
94 | },
95 | collect: monitor => ({
96 | isOver: monitor.isOver({ shallow: true }),
97 | canDrop: monitor.canDrop(),
98 | }),
99 | });
100 |
101 | const isActive = canDrop && isOver;
102 | dragPreview(dropRef(boxRef));
103 |
104 | const handleClick = e => {
105 | e.stopPropagation();
106 |
107 | const _id = inside ? '0' + $id : $id;
108 | if (_id === selected) {
109 | return;
110 | }
111 | setGlobal({ selected: _id });
112 | };
113 |
114 | const deleteItem = e => {
115 | e.stopPropagation();
116 | const newFlatten = { ...flatten };
117 | let newSelect = '#';
118 | // 计算删除后新被选中的元素:
119 | // 1. 如果是第一个,选第二个
120 | // 2. 如果不是第一,选它前一个
121 | // 3. 如果同级元素没了,选parent
122 | try {
123 | const parent = newFlatten[$id].parent;
124 | const siblings = newFlatten[parent].children;
125 | const idx = siblings.indexOf($id);
126 | if (idx > 0) {
127 | newSelect = siblings[idx - 1];
128 | } else {
129 | newSelect = siblings[1] || parent;
130 | }
131 | } catch (error) {
132 | console.log('catch', error);
133 | }
134 | delete newFlatten[$id];
135 | onFlattenChange(newFlatten);
136 | setGlobal({ selected: newSelect });
137 | };
138 |
139 | const handleItemCopy = e => {
140 | e.stopPropagation();
141 | const [newFlatten, newId] = copyItem(flatten, $id);
142 | onFlattenChange(newFlatten);
143 | setGlobal({ selected: newId });
144 | };
145 |
146 | const handleMouseEnter = () => {
147 | // console.log('进来了');
148 | // setGlobal({ hovering: inside ? '0' + $id : $id });
149 | };
150 |
151 | const handleMouseLeave = () => {
152 | // TODO: 如何写hoverLeave,延迟问题,导致无法得到最新的
153 | // let hoverItem = '';
154 | // if (hovering && hovering[0] === '0') {
155 | // hoverItem = $id;
156 | // } else {
157 | // hoverItem = $id.split;
158 | // }
159 | // console.log('出去了e');
160 | // setGlobal({ hovering: null });
161 | };
162 |
163 | // 一些computed
164 | let isSelected = selected === $id && !inside;
165 | if (selected && selected[0] === '0') {
166 | isSelected = selected.substring(1) === $id && inside;
167 | }
168 |
169 | const hoverId = inside ? '0' + $id : $id;
170 |
171 | let overwriteStyle = {
172 | backgroundColor: hovering === hoverId ? '#ecf5ff' : '#fff',
173 | opacity: isDragging ? 0 : 1,
174 | };
175 | if (inside) {
176 | overwriteStyle = {
177 | ...overwriteStyle,
178 | borderColor: '#777',
179 | // marginLeft: 12,
180 | padding: '12px 12px 0',
181 | backgroundColor: '#f6f5f6',
182 | };
183 | } else if ($id === '#') {
184 | overwriteStyle = {
185 | ...overwriteStyle,
186 | borderColor: '#777',
187 | padding: 12,
188 | height: '100%',
189 | overflow: 'auto',
190 | backgroundColor: '#f6f5f6',
191 | };
192 | } else if (type === 'object') {
193 | overwriteStyle = { ...overwriteStyle, paddingTop: 12 };
194 | }
195 | if (isActive) {
196 | if (inside) {
197 | overwriteStyle = {
198 | ...overwriteStyle,
199 | boxShadow: '0 -3px 0 red',
200 | };
201 | } else if (position === 'up') {
202 | overwriteStyle = {
203 | ...overwriteStyle,
204 | boxShadow: '0 -3px 0 red',
205 | };
206 | } else if (position === 'down') {
207 | overwriteStyle = {
208 | ...overwriteStyle,
209 | boxShadow: '0 3px 0 red',
210 | };
211 | }
212 | }
213 | if (isSelected) {
214 | overwriteStyle = {
215 | ...overwriteStyle,
216 | outline: '2px solid #409eff',
217 | borderColor: '#fff',
218 | };
219 | }
220 | if (style && typeof style === 'object') {
221 | overwriteStyle = {
222 | ...overwriteStyle,
223 | ...style,
224 | };
225 | }
226 |
227 | if ($id === '#' && inside) return children;
228 |
229 | // 展示的id
230 | let shownId = schema && schema.$id && getKeyFromUniqueId(schema.$id);
231 | if (shownId === '#') shownId = ''; // 根元素不展示了
232 |
233 | return (
234 |
242 | {!inside && isSelected && $id !== '#' && (
243 |
256 |
257 |
258 | )}
259 | {!inside && (
260 |
{shownId}
261 | )}
262 | {children}
263 |
264 | {isSelected && !inside && $id !== '#' && (
265 |
280 |
281 |
289 |
290 |
291 |
299 |
300 |
301 | )}
302 |
303 | );
304 | }
305 |
--------------------------------------------------------------------------------
/src/utils/schema.js:
--------------------------------------------------------------------------------
1 | import nanoid from 'nanoid';
2 | import deepClone from 'clone';
3 |
4 | // 后面三个参数都是内部递归使用的,将schema的树形结构扁平化成一层, 每个item的结构
5 | // {
6 | // parent: '#',
7 | // schema: ...,
8 | // children: []
9 | // }
10 | export function flattenSchema(schema, name = '#', parent, result = {}) {
11 | const _schema = deepClone(schema);
12 | if (!_schema.$id) {
13 | _schema.$id = name; // 给生成的schema添加一个唯一标识,方便从schema中直接读取
14 | }
15 | const children = [];
16 | const isObj = _schema.type === 'object' && _schema.properties;
17 | const isList =
18 | _schema.type === 'array' && _schema.items && _schema.items.properties;
19 | if (isObj) {
20 | Object.entries(_schema.properties).forEach(([key, value]) => {
21 | const uniqueName = name + '/' + key;
22 | children.push(uniqueName);
23 | flattenSchema(value, uniqueName, name, result);
24 | });
25 | delete _schema.properties;
26 | }
27 | if (isList) {
28 | Object.entries(_schema.items.properties).forEach(([key, value]) => {
29 | const uniqueName = name + '/' + key;
30 | children.push(uniqueName);
31 | flattenSchema(value, uniqueName, name, result);
32 | });
33 | delete _schema.items.properties;
34 | }
35 | // if (_schema.type) {
36 | result[name] = { parent, schema: _schema, children };
37 | // }
38 | return result;
39 | }
40 |
41 | export const getKeyFromUniqueId = (uniqueId = '#') => {
42 | const arr = uniqueId.split('/');
43 | return arr[arr.length - 1];
44 | };
45 |
46 | export const changeKeyFromUniqueId = (uniqueId = '#', key = 'something') => {
47 | const arr = uniqueId.split('/');
48 | if (typeof key === 'string' || typeof key === 'number') {
49 | arr[arr.length - 1] = key;
50 | }
51 | return arr.join('/');
52 | };
53 |
54 | // final = true 用于最终的导出的输出
55 | // 几种特例:
56 | // 1. 删除时值删除了item,没有删除和parent的关联,也没有删除children,所以要在解析这步来兜住 (所有的解析都是)
57 | // 2. 修改$id的情况, 修改的是schema内的$id, 解析的时候要把schema.$id 作为真正的id (final = true的解析)
58 | export function idToSchema(flatten, id = '#', final = false) {
59 | let schema = {};
60 | const _item = flatten[id];
61 | const item = deepClone(_item);
62 | if (item) {
63 | schema = { ...item.schema };
64 | // 最终输出去掉 $id
65 | if (final) {
66 | schema.$id && delete schema.$id;
67 | }
68 | if (item.children.length > 0) {
69 | item.children.forEach(child => {
70 | let childId = child;
71 | // TODO: 这个情况会出现吗?return会有问题吗?
72 | if (!flatten[child]) {
73 | return;
74 | }
75 | // 最终输出将所有的 key 值改了
76 | try {
77 | if (final) {
78 | childId = flatten[child].schema.$id;
79 | }
80 | } catch (error) {
81 | console.log('catch', error);
82 | }
83 | const key = getKeyFromUniqueId(childId);
84 | if (schema.type === 'object') {
85 | if (!schema.properties) {
86 | schema.properties = {};
87 | }
88 | schema.properties[key] = idToSchema(flatten, child, final);
89 | }
90 | if (
91 | schema.type === 'array' &&
92 | schema.items &&
93 | schema.items.type === 'object'
94 | ) {
95 | if (!schema.items.properties) {
96 | schema.items.properties = {};
97 | }
98 | schema.items.properties[key] = idToSchema(flatten, child, final);
99 | }
100 | });
101 | }
102 | }
103 | return schema;
104 | }
105 |
106 | // 删除对应id的schema(以及所有它的子schema)
107 | export const deleteSchema = (id, schema) => {
108 | const flatten = flattenSchema(schema);
109 | if (id in flatten) {
110 | delete flatten[id];
111 | }
112 | return idToSchema(flatten);
113 | };
114 |
115 | export const copyItem = (flatten, $id) => {
116 | let newFlatten = { ...flatten };
117 | try {
118 | const item = flatten[$id];
119 | const newId = $id + nanoid(6);
120 | const siblings = newFlatten[item.parent].children;
121 | const idx = siblings.findIndex(x => x === $id);
122 | siblings.splice(idx + 1, 0, newId);
123 | newFlatten[newId] = deepClone(newFlatten[$id]);
124 | newFlatten[newId].schema.$id = newId;
125 | return [newFlatten, newId];
126 | } catch (error) {
127 | console.error(error, 'catcherror');
128 | return [flatten, $id];
129 | }
130 | };
131 |
132 | // schema的某个id位置后面添加一个名字是key的subSchema,生成新的schema
133 | // TODO: 如果没有任何选中,或者选中的是object,逻辑要变
134 |
135 | export const addSchema = ({ id, key, schema, subSchema }) => {
136 | const flatten = flattenSchema(schema);
137 | let newId = changeKeyFromUniqueId(id, key) + '$$' + nanoid(10);
138 | if (id in flatten) {
139 | // 生成新id,并将其放置于parent节点的children属性中
140 | const parent = flatten[id].parent;
141 | if (parent && parent in flatten) {
142 | const children = flatten[parent].children;
143 | try {
144 | const idx = children.findIndex(x => x === id);
145 | children.splice(idx + 1, 0, newId);
146 | } catch (error) {
147 | console.error(error.message);
148 | }
149 | }
150 | // 生成新节点
151 | try {
152 | flatten[newId] = {
153 | parent: flatten[id].parent,
154 | schema: subSchema,
155 | children: [],
156 | };
157 | flatten[newId].schema.$id = newId;
158 | } catch (error) {
159 | console.error(error.message);
160 | }
161 | }
162 | // 将id也返回,用于ui展示显示
163 | return [idToSchema(flatten), newId];
164 | };
165 |
166 | // Left 点击添加 item
167 | export const addItem = ({ selected, name, schema, flatten }) => {
168 | let _selected = selected || '#';
169 | let newId;
170 | // string第一个是0,说明点击了object、list的里侧
171 | if ((_selected && _selected[0] === '0') || _selected === '#') {
172 | const newFlatten = { ...flatten };
173 | try {
174 | let oldId = _selected.substring(1);
175 | newId = oldId + '/' + name + '_' + nanoid(6);
176 | if (_selected === '#') {
177 | newId = '#/' + name + '_' + nanoid(6);
178 | oldId = '#';
179 | }
180 | const siblings = newFlatten[oldId].children;
181 | siblings.push(newId);
182 | const newItem = {
183 | parent: oldId,
184 | schema: { ...schema, $id: newId },
185 | data: undefined,
186 | children: [],
187 | };
188 | newFlatten[newId] = newItem;
189 | } catch (error) {
190 | console.error(error, 'catch');
191 | }
192 | return { newId, newFlatten };
193 | }
194 | let _name = name + '_' + nanoid(6);
195 | const idArr = selected.split('/');
196 | idArr.pop();
197 | idArr.push(_name);
198 | newId = idArr.join('/');
199 | const newFlatten = { ...flatten };
200 | try {
201 | const item = newFlatten[selected];
202 | const siblings = newFlatten[item.parent].children;
203 | const idx = siblings.findIndex(x => x === selected);
204 | siblings.splice(idx + 1, 0, newId);
205 | const newItem = {
206 | parent: item.parent,
207 | schema: { ...schema, $id: newId },
208 | data: undefined,
209 | children: [],
210 | };
211 | newFlatten[newId] = newItem;
212 | } catch (error) {
213 | console.error(error);
214 | }
215 | return { newId, newFlatten };
216 | };
217 |
218 | // position 代表 drop 在元素的哪里: 'up' 上 'down' 下 'inside' 内部
219 | export const dropItem = ({ dragId, dragItem, dropId, position, flatten }) => {
220 | const _position = dropId === '#' ? 'inside' : position;
221 | let newFlatten = { ...flatten };
222 | // 会动到三块数据,dragItem, dragParent, dropParent. 其中dropParent可能就是dropItem(inside的情况)
223 | if (dragItem) {
224 | newFlatten[dragId] = dragItem;
225 | }
226 | const _dragItem = dragItem || newFlatten[dragId];
227 |
228 | const dropItem = newFlatten[dropId];
229 | let dropParent = dropItem;
230 | if (_position !== 'inside') {
231 | const parentId = dropItem.parent;
232 | dropParent = newFlatten[parentId];
233 | }
234 | // TODO: 这块的体验,现在这样兜底了,但是drag起一个元素了,应该让原本变空
235 | if (dropId.indexOf(dragId) > -1) {
236 | return newFlatten;
237 | }
238 |
239 | let newId = dragId;
240 | try {
241 | const newParentId = dropParent.schema.$id;
242 | newId = newId.replace(_dragItem.parent, newParentId);
243 | } catch (error) {}
244 |
245 | // dragParent 的 children 删除 dragId
246 | try {
247 | const dragParent = newFlatten[_dragItem.parent];
248 | const idx = dragParent.children.indexOf(dragId);
249 | if (idx > -1) {
250 | dragParent.children.splice(idx, 1);
251 | }
252 | } catch (error) {
253 | console.error(error);
254 | }
255 | try {
256 | // dropParent 的 children 添加 dragId
257 | const newChildren = dropParent.children || []; // 要考虑children为空,inside的情况
258 | const idx = newChildren.indexOf(dropId);
259 | switch (_position) {
260 | case 'up':
261 | newChildren.splice(idx, 0, dragId);
262 | break;
263 | case 'down':
264 | newChildren.splice(idx + 1, 0, dragId);
265 | break;
266 | default:
267 | // inside 作为 default 情况
268 | newChildren.push(dragId);
269 | break;
270 | }
271 | dropParent.children = newChildren;
272 | } catch (error) {
273 | console.error(error);
274 | }
275 |
276 | _dragItem.parent = dropParent.$id;
277 | return [newFlatten, newId];
278 | };
279 | // TODO: 是不是要考虑如果drag前,已经有id和schema.id不一致的情况,会不会有问题?
280 |
281 | // export const changeSubSchema = ({ id, schema, subSchema }) => {
282 | // const flatten = flattenSchema(schema);
283 | // if (id in flatten) {
284 | // const oldSchema = flatten[id];
285 | // const newId = subSchema.$id;
286 | // if (oldSchema.$id !== subSchema.$id) {
287 | // }
288 | // }
289 | // };
290 |
291 | // 解析函数字符串值
292 | // TODO: 没有考虑list的情况
293 | export const getDataById = (data, idString) => {
294 | if (idString === '#') return data;
295 | try {
296 | const idConnectedByDots = idString
297 | .split('/')
298 | .filter(id => id !== '#')
299 | .map(id => `["${id}"]`)
300 | .join('');
301 | const string = `data${idConnectedByDots}`;
302 | const a = `"use strict";
303 | const data = ${JSON.stringify(data)};
304 | return ${string}`;
305 | return Function(a)();
306 | // TODO: can be better
307 | // let result = { ...data };
308 | // idConnectedByDots.forEach((item) => {
309 | // result = result[item];
310 | // });
311 | // return result;
312 | } catch (error) {
313 | return undefined;
314 | }
315 | };
316 |
317 | // TODO: 没有考虑list的情况
318 | export const dataToFlatten = (flatten, data) => {
319 | if (!flatten || !data) return;
320 | Object.entries(flatten).forEach(([id, item]) => {
321 | const branchData = getDataById(data, id);
322 | flatten[id].data = branchData;
323 | });
324 | return flatten;
325 | };
326 |
327 | export const onChangeById = onChange => (id, value) => {};
328 |
329 | // TODO: 没有考虑list的情况
330 | export const flattenToData = (flatten, id = '#') => {
331 | try {
332 | let result = flatten[id].data;
333 | const ids = Object.keys(flatten);
334 | const childrenIds = ids.filter(item => {
335 | const lengthOfId = id.split('/').length;
336 | const lengthOfChild = item.split('/').length;
337 | return item.indexOf(id) > -1 && lengthOfChild > lengthOfId;
338 | });
339 | if (childrenIds && childrenIds.length > 0) {
340 | if (result === undefined) {
341 | // TODO: 这个是简化的逻辑,在编辑器模型下,list和object都是object结构???
342 | if (flatten[id].schema.type === 'object') {
343 | result = {};
344 | }
345 | if (flatten[id].schema.type === 'array') {
346 | result = [{}];
347 | }
348 | }
349 | childrenIds.forEach(c => {
350 | const lengthOfId = id.split('/').length;
351 | const lengthOfChild = c.split('/').length;
352 | // 只比他长1,是直属的child
353 | if (lengthOfChild === lengthOfId + 1) {
354 | const cData = flattenToData(flatten, c);
355 | const cKey = getKeyFromUniqueId(c);
356 |
357 | // if (flatten[id].schema.type === 'array') { // TODO 数组怎么确定是第几项??
358 | // result[0][cKey] = cData;
359 | // }
360 | result[cKey] = cData;
361 | }
362 | });
363 | }
364 | return result;
365 | } catch (error) {
366 | return undefined;
367 | }
368 | };
369 |
370 | // 例如当前item的id = '#/obj/input' propName: '' 往上一直找,直到找到第一个不是undefined的值
371 | export const getParentProps = (propName, id, flatten) => {
372 | try {
373 | const item = flatten[id];
374 | if (item.schema[propName] !== undefined) return item.schema[propName];
375 | if (item && item.parent) {
376 | const parentSchema = flatten[item.parent].schema;
377 | if (parentSchema[propName] !== undefined) {
378 | return parentSchema[propName];
379 | } else {
380 | return getParentProps(propName, item.parent, flatten);
381 | }
382 | }
383 | } catch (error) {
384 | return undefined;
385 | }
386 | };
387 |
--------------------------------------------------------------------------------
/docs/Playground.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Input } from 'antd';
3 | import Generator from '@coolvision/schema-generator';
4 | import './index.css';
5 |
6 | const defaultValue = {
7 | "type": "object",
8 | "properties": {
9 | "object_HHLdi8": {
10 | "title": "基础技能",
11 | "type": "object",
12 | "properties": {
13 | "play_game": {
14 | "key": "play_game",
15 | "type": "string",
16 | "name": "play_game",
17 | "title": "打游戏",
18 | "x-component": "Input",
19 | "x-component-props": {
20 | "placeholder": "请输入"
21 | },
22 | "required": true
23 | }
24 | }
25 | }
26 | }
27 | };
28 |
29 | const advancedElements = [
30 | {
31 | text: '工作经历',
32 | name: 'work_experience',
33 | schema: {
34 | "title": "工作经历",
35 | "type": "array",
36 | "items": {
37 | "type": "object",
38 | "properties": {
39 | "company_GYjdmL": {
40 | "title": "工作单位",
41 | "type": "string",
42 | "x-component": "Input",
43 | "x-component-props": {
44 | "placeholder": "请输入工作单位"
45 | },
46 | "required": true
47 | },
48 | "position_MYOc0t": {
49 | "title": "职位",
50 | "type": "string",
51 | "x-component": "Input",
52 | "x-component-props": {
53 | "placeholder": "请输入职位"
54 | },
55 | "required": true
56 | },
57 | "dateRange_YUb4kj": {
58 | "title": "开始结束时间",
59 | "type": "range",
60 | "format": "dateTime",
61 | "x-component": "RangePicker",
62 | "x-component-props": {
63 | "placeholder": [
64 | "开始时间",
65 | "结束时间"
66 | ],
67 | "picker": "date"
68 | },
69 | "required": true
70 | },
71 | "input_JDWEkz": {
72 | "title": "证明人",
73 | "type": "string",
74 | "x-component": "Input",
75 | "x-component-props": {
76 | "placeholder": "请输入"
77 | }
78 | },
79 | "telephone_Ln_F2i": {
80 | "title": "证明人手机号",
81 | "type": "string",
82 | "x-component": "Input",
83 | "x-component-props": {
84 | "placeholder": "请输入手机号"
85 | },
86 | "x-rules": [
87 | {
88 | "validator": "(value: string) => {\n return !(/^[1]([3-9])[0-9]{9}$/.test(value));\n }",
89 | "message": "手机号格式不正确"
90 | }
91 | ],
92 | "required": false
93 | },
94 | "textarea_6RDJDA": {
95 | "title": "离职原因",
96 | "type": "string",
97 | "x-component": "TextArea",
98 | "x-component-props": {
99 | "placeholder": "请输入离职原因"
100 | }
101 | }
102 | }
103 | },
104 | "required": true
105 | },
106 | },
107 | {
108 | text: '教育经历',
109 | name: 'educate_experience',
110 | schema: {
111 | "title": "教育经历",
112 | "type": "array",
113 | "items": {
114 | "type": "object",
115 | "properties": {
116 | "school__ZvyAv": {
117 | "title": "学校",
118 | "type": "string",
119 | "x-component": "Input",
120 | "x-component-props": {
121 | "placeholder": "请输入学校"
122 | },
123 | "required": true
124 | },
125 | "major_zkzF80": {
126 | "title": "专业",
127 | "type": "string",
128 | "x-component": "Input",
129 | "x-component-props": {
130 | "placeholder": "请输入专业"
131 | },
132 | "required": true
133 | },
134 | "dateRange_OfS_p8": {
135 | "title": "开始结束时间",
136 | "type": "range",
137 | "format": "dateTime",
138 | "x-component": "RangePicker",
139 | "x-component-props": {
140 | "placeholder": [
141 | "开始时间",
142 | "结束时间"
143 | ],
144 | "picker": "date"
145 | },
146 | "required": true
147 | }
148 | }
149 | }
150 | }
151 | },
152 | {
153 | text: '家庭成员',
154 | name: 'family_member',
155 | schema: {
156 | "title": "家庭成员",
157 | "type": "array",
158 | "items": {
159 | "type": "object",
160 | "properties": {
161 | "input_2CSFYn": {
162 | "title": "姓名",
163 | "type": "string",
164 | "x-component": "Input",
165 | "x-component-props": {
166 | "placeholder": "请输入姓名"
167 | },
168 | "required": true
169 | },
170 | "input_7WLMnR": {
171 | "title": "关系",
172 | "type": "string",
173 | "x-component": "Input",
174 | "x-component-props": {
175 | "placeholder": "请输入关系"
176 | },
177 | "required": true
178 | },
179 | "input_mkFCKW": {
180 | "title": "工作或学习单位",
181 | "type": "string",
182 | "x-component": "Input",
183 | "x-component-props": {
184 | "placeholder": "请输入工作或学习单位"
185 | },
186 | "required": true
187 | },
188 | "input_psn_Dw": {
189 | "title": "职务",
190 | "type": "string",
191 | "x-component": "Input",
192 | "x-component-props": {
193 | "placeholder": "请输入职务"
194 | },
195 | "required": true
196 | }
197 | }
198 | },
199 | "required": true
200 | }
201 | },
202 | {
203 | text: '专业证书',
204 | name: 'professional_certificate',
205 | schema: {
206 | "title": "专业证书",
207 | "type": "array",
208 | "items": {
209 | "type": "object",
210 | "properties": {
211 | "certificate_type_C2F0Cy": {
212 | "title": "证书类型",
213 | "type": "string",
214 | "x-component": "Input",
215 | "x-component-props": {
216 | "placeholder": "请输入证书类型"
217 | },
218 | "required": true
219 | },
220 | "certificate_name_6odVio": {
221 | "title": "证书名称",
222 | "type": "string",
223 | "x-component": "Input",
224 | "x-component-props": {
225 | "placeholder": "请输入证书名称"
226 | },
227 | "required": true
228 | },
229 | "input_u4EP8Z": {
230 | "title": "发证机构",
231 | "type": "string",
232 | "x-component": "Input",
233 | "x-component-props": {
234 | "placeholder": "请输入发证机构"
235 | }
236 | },
237 | "date_OGHtXa": {
238 | "title": "取得日期",
239 | "type": "string",
240 | "x-component": "DatePicker",
241 | "x-component-props": {
242 | "placeholder": "请输入"
243 | }
244 | }
245 | }
246 | }
247 | }
248 | }
249 | ];
250 |
251 | const customElements = [
252 | {
253 | text: '手机号',
254 | name: 'telephone',
255 | schema: {
256 | title: '手机号',
257 | type: 'string',
258 | "x-component": 'Input',
259 | "x-component-props": {
260 | "placeholder": "请输入手机号"
261 | },
262 | "x-rules": [
263 | {
264 | validator: `(value: string) => {
265 | return !(/^[1]([3-9])[0-9]{9}$/.test(value));
266 | }`,
267 | message: "手机号格式不正确"
268 | }
269 | ],
270 | required: true
271 | },
272 | },
273 | {
274 | text: '邮箱',
275 | name: 'email',
276 | schema: {
277 | title: '邮箱',
278 | type: 'string',
279 | "x-component": 'Input',
280 | "x-component-props": {
281 | "placeholder": "请输入邮箱"
282 | },
283 | "x-rules": [
284 | {
285 | validator: `(value: string) => {
286 | return !(/^([A-Za-z0-9_\-\.])+\@([A-Za-z0-9_\-\.])+\.([A-Za-z]{2,4})$/.test(value));
287 | }`,
288 | message: "邮箱格式不正确"
289 | }
290 | ],
291 | required: true
292 | },
293 | },
294 | {
295 | text: '应聘职位',
296 | name: 'position',
297 | schema: {
298 | title: '应聘职位',
299 | type: 'string',
300 | "x-component": 'Input',
301 | "x-component-props": {
302 | "placeholder": "请输入应聘职位"
303 | },
304 | required: true
305 | },
306 | },
307 | {
308 | text: '期望月薪',
309 | name: 'expected_salary',
310 | schema: {
311 | title: '期望月薪',
312 | type: 'string',
313 | "x-component": 'Select',
314 | "x-component-props": {
315 | placeholder: '请选择期望月薪'
316 | },
317 | enum: [
318 | {
319 | label: '5000元以下',
320 | value: 'a',
321 | },
322 | {
323 | label: '5000元 - 10000元',
324 | value: 'b',
325 | },
326 | {
327 | label: '10000元 - 15000元',
328 | value: 'c',
329 | },
330 | {
331 | label: '15000元 - 20000元',
332 | value: 'd',
333 | },
334 | {
335 | label: '20000元 - 30000元',
336 | value: 'e',
337 | },
338 | {
339 | label: '30000元以上',
340 | value: 'f',
341 | },
342 | ],
343 | required: true
344 | },
345 | },
346 | {
347 | text: '当前月薪',
348 | name: 'current_salary',
349 | schema: {
350 | title: '当前月薪',
351 | type: 'string',
352 | "x-component": 'Select',
353 | "x-component-props": {
354 | placeholder: '请选择当前月薪'
355 | },
356 | enum: [
357 | {
358 | label: '5000元以下',
359 | value: 'a',
360 | },
361 | {
362 | label: '5000元 - 10000元',
363 | value: 'b',
364 | },
365 | {
366 | label: '10000元 - 15000元',
367 | value: 'c',
368 | },
369 | {
370 | label: '15000元 - 20000元',
371 | value: 'd',
372 | },
373 | {
374 | label: '20000元 - 30000元',
375 | value: 'e',
376 | },
377 | {
378 | label: '30000元以上',
379 | value: 'f',
380 | },
381 | ],
382 | required: true
383 | },
384 | },
385 | {
386 | text: '婚姻状况',
387 | name: 'maritlal_status',
388 | widget: 'Radio',
389 | schema: {
390 | title: '婚姻状况',
391 | type: 'string',
392 | 'x-component': 'Radio',
393 | enum: [
394 | {
395 | label: '已婚',
396 | value: 'a',
397 | },
398 | {
399 | label: '未婚',
400 | value: 'b',
401 | },
402 | {
403 | label: '其他',
404 | value: 'c',
405 | },
406 | ],
407 | required: true
408 | }
409 | },
410 | {
411 | text: '性别',
412 | name: 'gender',
413 | widget: 'Radio',
414 | schema: {
415 | title: '点击单选',
416 | type: 'string',
417 | 'x-component': 'Radio',
418 | enum: [
419 | {
420 | label: '男',
421 | value: 'a',
422 | },
423 | {
424 | label: '女',
425 | value: 'b',
426 | },
427 | {
428 | label: '其他',
429 | value: 'c',
430 | },
431 | ],
432 | required: true
433 | }
434 | },
435 | {
436 | text: '政治面貌',
437 | name: 'politics',
438 | schema: {
439 | title: '政治面貌',
440 | type: 'string',
441 | "x-component": 'Input',
442 | "x-component-props": {
443 | "placeholder": "请输入政治面貌"
444 | },
445 | required: true
446 | },
447 | },
448 | {
449 | text: '籍贯',
450 | name: 'native_place',
451 | schema: {
452 | title: '籍贯',
453 | type: 'string',
454 | "x-component": 'Input',
455 | "x-component-props": {
456 | "placeholder": "请输入籍贯"
457 | },
458 | required: true
459 | },
460 | },
461 | {
462 | text: '民族',
463 | name: 'nation',
464 | schema: {
465 | title: '民族',
466 | type: 'string',
467 | "x-component": 'Input',
468 | "x-component-props": {
469 | "placeholder": "请输入民族"
470 | },
471 | required: true
472 | },
473 | },
474 | {
475 | text: '工作单位',
476 | name: 'company',
477 | schema: {
478 | title: '工作单位',
479 | type: 'string',
480 | "x-component": 'Input',
481 | "x-component-props": {
482 | "placeholder": "请输入工作单位"
483 | },
484 | required: true
485 | },
486 | },
487 | {
488 | text: '学校',
489 | name: 'school',
490 | schema: {
491 | title: '学校',
492 | type: 'string',
493 | "x-component": 'Input',
494 | "x-component-props": {
495 | "placeholder": "请输入学校"
496 | },
497 | required: true
498 | },
499 | },
500 | {
501 | text: '专业',
502 | name: 'major',
503 | schema: {
504 | title: '专业',
505 | type: 'string',
506 | "x-component": 'Input',
507 | "x-component-props": {
508 | "placeholder": "请输入专业"
509 | },
510 | required: true
511 | },
512 | },
513 | {
514 | text: '学历',
515 | name: 'education',
516 | schema: {
517 | title: '学历',
518 | type: 'string',
519 | "x-component": 'Select',
520 | "x-component-props": {
521 | "placeholder": "请选择"
522 | },
523 | enum: [
524 | {
525 | label: '小学',
526 | value: 'a',
527 | },
528 | {
529 | label: '初中',
530 | value: 'b',
531 | },
532 | {
533 | label: '高中',
534 | value: 'c',
535 | },
536 | {
537 | label: '中专',
538 | value: 'd',
539 | },
540 | {
541 | label: '大专',
542 | value: 'e',
543 | },
544 | {
545 | label: '本科',
546 | value: 'f',
547 | },
548 | {
549 | label: '硕士',
550 | value: 'g',
551 | },
552 | {
553 | label: '博士',
554 | value: 'h',
555 | },
556 | ],
557 | required: true
558 | },
559 | },
560 | {
561 | text: '证书类型',
562 | name: 'certificate_type',
563 | schema: {
564 | title: '证书类型',
565 | type: 'string',
566 | "x-component": 'Input',
567 | "x-component-props": {
568 | "placeholder": "请输入证书类型"
569 | },
570 | required: true
571 | },
572 | },
573 | {
574 | text: '证书名称',
575 | name: 'certificate_name',
576 | schema: {
577 | title: '证书名称',
578 | type: 'string',
579 | "x-component": 'Input',
580 | "x-component-props": {
581 | "placeholder": "请输入证书名称"
582 | },
583 | required: true
584 | },
585 | },
586 | // {
587 | // text: '自定义组件2',
588 | // name: 'something2',
589 | // schema: {
590 | // title: '自定义的widget',
591 | // description: '这是一个自定义的widget',
592 | // type: 'string',
593 | // "x-component": 'ABCD',
594 | // 'x-component-props': {
595 | // maxLength: 20
596 | // }
597 | // },
598 | // settings: {
599 | // 'x-component-props': {
600 | // title: '选项',
601 | // type: 'object',
602 | // properties: {
603 | // allowClear: {
604 | // title: '是否带清除按钮',
605 | // description: '填写内容后才会出现x哦',
606 | // type: 'boolean',
607 | // },
608 | // addonBefore: {
609 | // title: '前置标签',
610 | // type: 'string',
611 | // },
612 | // addonAfter: {
613 | // title: '后置标签',
614 | // type: 'string',
615 | // },
616 | // prefix: {
617 | // title: '前缀',
618 | // type: 'string',
619 | // },
620 | // suffix: {
621 | // title: '后缀',
622 | // type: 'string',
623 | // },
624 | // maxLength: {
625 | // title: '最长字数',
626 | // type: 'number',
627 | // }
628 | // },
629 | // },
630 | // }
631 | // }
632 | ];
633 |
634 | const ABCD = ({ disabled, readOnly, value, onChange, options }) => {
635 | return (
636 | {
641 | onChange(e.target.value)
642 | }}
643 | maxLength={options.maxLength}
644 | />
645 | );
646 | }
647 |
648 | const Demo = () => {
649 | const [value, setValue] = useState(defaultValue);
650 |
651 | return (
652 |
653 | {
656 | setValue(newValue);
657 | }}
658 | customElements={customElements}
659 | advancedElements={advancedElements}
660 | // Widgets={{
661 | // ABCD
662 | // }}
663 | actions={[
664 | { label: '切换模板', onClick: () => {} }
665 | ]}
666 | onSave={(schema) => {
667 | console.log(JSON.stringify(schema, null, 2))
668 | }}
669 | beforeDragComplete={(current) => {
670 | console.log('current', current);
671 | return true;
672 | }}
673 | />
674 |
675 | );
676 | };
677 |
678 | export default Demo;
679 |
--------------------------------------------------------------------------------
/src/Left/elementList.js:
--------------------------------------------------------------------------------
1 | // 只需写配置,方便可扩展
2 | export const commonSettings = {
3 | $id: {
4 | title: 'ID',
5 | description: '数据存储的名称,请写英文,不能为空',
6 | type: 'string',
7 | 'x-component': 'IdInput',
8 | },
9 | title: {
10 | title: '标题',
11 | type: 'string',
12 | },
13 | disabled: {
14 | title: '置灰',
15 | type: 'boolean',
16 | },
17 | required: {
18 | title: '必填',
19 | type: 'boolean',
20 | },
21 | };
22 |
23 | // widget 用于schema中每个元素对应的右侧配置知道用哪个setting
24 |
25 | const elements = [
26 | {
27 | text: '输入框',
28 | name: 'input',
29 | widget: 'Input',
30 | schema: {
31 | title: '输入框',
32 | type: 'string',
33 | 'x-component': 'Input',
34 | 'x-component-props': {
35 | placeholder: '请输入'
36 | }
37 | },
38 | setting: {
39 | 'x-component-props': {
40 | title: '属性',
41 | type: 'object',
42 | properties: {
43 | placeholder: {
44 | title: 'placeholder',
45 | type: 'string',
46 | },
47 | allowClear: {
48 | title: '是否带清除按钮',
49 | description: '填写内容后才会出现x哦',
50 | type: 'boolean',
51 | 'x-component': 'Switch',
52 | },
53 | addonBefore: {
54 | title: '前置标签',
55 | type: 'string',
56 | },
57 | addonAfter: {
58 | title: '后置标签',
59 | type: 'string',
60 | },
61 | prefix: {
62 | title: '前缀',
63 | type: 'string',
64 | },
65 | suffix: {
66 | title: '后缀',
67 | type: 'string',
68 | },
69 | maxLength: {
70 | title: '最长字数',
71 | type: 'number',
72 | 'x-component': 'Number',
73 | 'x-component-props': {
74 | min: 1,
75 | precision: 0
76 | },
77 | },
78 | size: {
79 | title: '尺寸',
80 | type: 'string',
81 | 'x-component': 'Select',
82 | 'x-component-props': {
83 | allowClear: true
84 | },
85 | enum: [
86 | {
87 | label: '大',
88 | value: 'large',
89 | },
90 | {
91 | label: '中',
92 | value: 'middle',
93 | },
94 | {
95 | label: '小',
96 | value: 'small',
97 | },
98 | ],
99 | },
100 | },
101 | },
102 | },
103 | mobileSetting: {
104 | 'x-component-props': {
105 | title: '属性',
106 | type: 'object',
107 | properties: {
108 | placeholder: {
109 | title: 'placeholder',
110 | type: 'string',
111 | },
112 | clear: {
113 | title: '是否带清除功能',
114 | type: 'boolean',
115 | 'x-component': 'Switch',
116 | },
117 | maxLength: {
118 | title: '最长字数',
119 | type: 'number',
120 | 'x-component': 'Number',
121 | 'x-component-props': {
122 | precision: 0
123 | },
124 | },
125 | extra: {
126 | title: '右边注释',
127 | type: 'string',
128 | }
129 | },
130 | },
131 | }
132 | },
133 | {
134 | text: '大输入框',
135 | name: 'textarea',
136 | widget: 'TextArea',
137 | schema: {
138 | title: '编辑框',
139 | type: 'string',
140 | 'x-component': 'TextArea',
141 | 'x-component-props': {
142 | "placeholder": "请输入"
143 | }
144 | },
145 | setting: {
146 | 'x-component-props': {
147 | title: '属性',
148 | type: 'object',
149 | properties: {
150 | placeholder: {
151 | title: 'placeholder',
152 | type: 'string',
153 | },
154 | allowClear: {
155 | title: '是否带清除按钮',
156 | description: '填写内容后才会出现x哦',
157 | type: 'boolean',
158 | 'x-component': 'Switch',
159 | },
160 | autoSize: {
161 | title: '高度自动',
162 | type: 'boolean',
163 | 'x-component': 'Switch'
164 | },
165 | rows: {
166 | title: '指定高度',
167 | type: 'number',
168 | 'x-component': 'Number',
169 | 'x-component-props': {
170 | min: 1
171 | },
172 | },
173 | maxLength: {
174 | title: '最长字数',
175 | type: 'number',
176 | 'x-component': 'Number',
177 | 'x-component-props': {
178 | min: 1,
179 | precision: 0
180 | },
181 | },
182 | },
183 | },
184 | },
185 | },
186 | {
187 | text: '数字输入框',
188 | name: 'number',
189 | widget: 'Number',
190 | schema: {
191 | title: '数字输入框',
192 | type: 'number',
193 | 'x-component': 'Number',
194 | 'x-component-props': {
195 | "placeholder": "请输入"
196 | }
197 | },
198 | setting: {
199 | 'x-component-props': {
200 | title: '属性',
201 | type: 'object',
202 | properties: {
203 | placeholder: {
204 | title: 'placeholder',
205 | type: 'string',
206 | },
207 | allowClear: {
208 | title: '是否带清除按钮',
209 | description: '填写内容后才会出现x哦',
210 | type: 'boolean',
211 | 'x-component': 'Switch',
212 | },
213 | min: {
214 | title: '最小值',
215 | type: 'number',
216 | 'x-component': 'Number'
217 | },
218 | max: {
219 | title: '最大值',
220 | type: 'number',
221 | 'x-component': 'Number'
222 | },
223 | precision: {
224 | title: '精度',
225 | type: 'number',
226 | 'x-component': 'Number',
227 | 'x-component-props': {
228 | min: 0,
229 | precision: 0
230 | },
231 | },
232 | step: {
233 | title: '步数',
234 | type: 'number',
235 | 'x-component': 'Number',
236 | 'x-component-props': {
237 | min: 0,
238 | },
239 | },
240 | }
241 | }
242 | },
243 | },
244 | {
245 | text: '是否选择',
246 | name: 'switch',
247 | widget: 'Switch',
248 | schema: {
249 | title: '是否选择',
250 | type: 'boolean',
251 | 'x-component': 'Switch',
252 | },
253 | setting: {
254 | 'x-component-props': {
255 | title: '属性',
256 | type: 'object',
257 | properties: {
258 | checkedChildren: {
259 | title: '选中时的内容',
260 | type: 'string',
261 | 'x-component-props': {
262 | allowClear: true
263 | }
264 | },
265 | unCheckedChildren: {
266 | title: '非选中时的内容',
267 | type: 'string',
268 | 'x-component-props': {
269 | allowClear: true
270 | }
271 | },
272 | },
273 | },
274 | },
275 | },
276 | {
277 | text: '点击单选',
278 | name: 'radio',
279 | widget: 'Radio',
280 | schema: {
281 | title: '点击单选',
282 | type: 'string',
283 | 'x-component': 'Radio',
284 | enum: [
285 | {
286 | label: '早',
287 | value: 'a',
288 | },
289 | {
290 | label: '中',
291 | value: 'b',
292 | },
293 | {
294 | label: '晚',
295 | value: 'c',
296 | },
297 | ],
298 | },
299 | setting: {
300 | enum: {
301 | title: '数据源',
302 | type: 'array',
303 | 'x-component': 'EnumList',
304 | },
305 | },
306 | },
307 | {
308 | text: '点击多选',
309 | name: 'checkbox',
310 | widget: 'Checkbox',
311 | schema: {
312 | title: '点击多选',
313 | type: 'array',
314 | items: {
315 | type: 'string',
316 | },
317 | enum: [
318 | {
319 | label: '杭州',
320 | value: 'A',
321 | },
322 | {
323 | label: '武汉',
324 | value: 'B',
325 | },
326 | {
327 | label: '湖州',
328 | value: 'C',
329 | },
330 | {
331 | label: '贵阳',
332 | value: 'D',
333 | },
334 | ],
335 | 'x-component': 'Checkbox',
336 | },
337 | setting: {
338 | enum: {
339 | title: '数据源',
340 | type: 'array',
341 | 'x-component': 'EnumList',
342 | },
343 | },
344 | },
345 | {
346 | text: '下拉单选',
347 | name: 'select',
348 | widget: 'Select',
349 | schema: {
350 | title: '下拉单选',
351 | type: 'string',
352 | "x-component": 'Select',
353 | "x-component-props": {
354 | placeholder: '请输入'
355 | },
356 | enum: [
357 | {
358 | label: '早',
359 | value: 'a',
360 | },
361 | {
362 | label: '中',
363 | value: 'b',
364 | },
365 | {
366 | label: '晚',
367 | value: 'c',
368 | },
369 | ],
370 | },
371 | setting: {
372 | enum: {
373 | title: '数据源',
374 | type: 'array',
375 | 'x-component': 'EnumList',
376 | },
377 | "x-component-props": {
378 | title: '属性',
379 | type: 'object',
380 | properties: {
381 | placeholder: {
382 | title: 'placeholder',
383 | type: 'string',
384 | },
385 | allowClear: {
386 | title: '是否带清除按钮',
387 | description: '填写内容后才会出现x哦',
388 | type: 'boolean',
389 | 'x-component': 'Switch',
390 | },
391 | size: {
392 | title: '尺寸',
393 | type: 'string',
394 | 'x-component': 'Select',
395 | 'x-component-props': {
396 | allowClear: true
397 | },
398 | enum: [
399 | {
400 | label: '大',
401 | value: 'large',
402 | },
403 | {
404 | label: '中',
405 | value: 'middle',
406 | },
407 | {
408 | label: '小',
409 | value: 'small',
410 | },
411 | ],
412 | },
413 | virtual: {
414 | title: '是否开启虚拟滚动',
415 | type: 'boolean',
416 | 'x-component': 'Switch',
417 | }
418 | }
419 | }
420 | },
421 | },
422 | {
423 | text: '下拉多选',
424 | name: 'multiSelect',
425 | widget: 'MultiSelect',
426 | schema: {
427 | title: '下拉多选',
428 | type: 'array',
429 | items: {
430 | type: 'string',
431 | },
432 | enum: [
433 | {
434 | label: '杭州',
435 | value: 'A',
436 | },
437 | {
438 | label: '武汉',
439 | value: 'B',
440 | },
441 | {
442 | label: '湖州',
443 | value: 'C',
444 | },
445 | {
446 | label: '贵阳',
447 | value: 'D',
448 | },
449 | ],
450 | 'x-component': 'MultiSelect',
451 | 'x-component-props': {
452 | placeholder: '请输入'
453 | }
454 | },
455 | setting: {
456 | enum: {
457 | title: '数据源',
458 | type: 'array',
459 | 'x-component': 'EnumList',
460 | },
461 | 'x-component-props': {
462 | title: '属性',
463 | type: 'object',
464 | properties: {
465 | placeholder: {
466 | title: 'placeholder',
467 | type: 'string',
468 | },
469 | allowClear: {
470 | title: '是否带清除按钮',
471 | description: '填写内容后才会出现x哦',
472 | type: 'boolean',
473 | 'x-component': 'Switch',
474 | },
475 | size: {
476 | title: '尺寸',
477 | type: 'string',
478 | 'x-component': 'Select',
479 | 'x-component-props': {
480 | allowClear: true
481 | },
482 | enum: [
483 | {
484 | label: '大',
485 | value: 'large',
486 | },
487 | {
488 | label: '中',
489 | value: 'middle',
490 | },
491 | {
492 | label: '小',
493 | value: 'small',
494 | },
495 | ],
496 | },
497 | virtual: {
498 | title: '是否开启虚拟滚动',
499 | type: 'boolean',
500 | 'x-component': 'Switch',
501 | }
502 | }
503 | }
504 | },
505 | },
506 | {
507 | text: '日期选择',
508 | name: 'date',
509 | widget: 'DatePicker',
510 | schema: {
511 | title: '日期选择',
512 | type: 'string',
513 | 'x-component': 'DatePicker',
514 | 'x-component-props': {
515 | placeholder: '请选择'
516 | }
517 | },
518 | setting: {
519 | 'x-component-props': {
520 | title: '属性',
521 | type: 'object',
522 | properties: {
523 | placeholder: {
524 | title: 'placeholder',
525 | type: 'string',
526 | },
527 | allowClear: {
528 | title: '是否带清除按钮',
529 | description: '填写内容后才会出现x哦',
530 | type: 'boolean',
531 | 'x-component': 'Switch',
532 | },
533 | picker: {
534 | title: '格式',
535 | type: 'string',
536 | 'x-component': 'Select',
537 | 'x-component-props': {
538 | allowClear: true
539 | },
540 | enum: [
541 | {
542 | label: '年/月/日',
543 | value: 'date',
544 | },
545 | {
546 | label: '周',
547 | value: 'week',
548 | },
549 | {
550 | label: '年/月',
551 | value: 'month',
552 | },
553 | {
554 | label: '季度',
555 | value: 'quarter',
556 | },
557 | {
558 | label: '年',
559 | value: 'year',
560 | },
561 | ],
562 | },
563 | showTime: {
564 | title: '时刻',
565 | type: 'string',
566 | 'x-component': 'Select',
567 | 'x-component-props': {
568 | allowClear: true
569 | },
570 | enum: [
571 | {
572 | label: '时+分+秒',
573 | value: 'HH:mm:ss',
574 | },
575 | {
576 | label: '时+分',
577 | value: 'HH:mm',
578 | },
579 | {
580 | label: '时',
581 | value: 'HH',
582 | }
583 | ],
584 | }
585 | }
586 | },
587 | },
588 | },
589 | {
590 | text: '日期范围',
591 | name: 'dateRange',
592 | widget: 'RangePicker',
593 | schema: {
594 | title: '日期范围',
595 | type: 'range',
596 | format: 'dateTime', // TODO ? format在schema中起的校验作用?是否可以完全干掉?
597 | 'x-component': 'RangePicker',
598 | 'x-component-props': {
599 | placeholder: ['开始时间', '结束时间'],
600 | },
601 | },
602 | setting: {
603 | 'x-component-props': {
604 | title: '属性',
605 | type: 'object',
606 | properties: {
607 | picker: {
608 | title: '格式',
609 | type: 'string',
610 | 'x-component': 'Select',
611 | 'x-component-props': {
612 | allowClear: true
613 | },
614 | enum: [
615 | {
616 | label: '年/月/日',
617 | value: 'date',
618 | },
619 | {
620 | label: '周',
621 | value: 'week',
622 | },
623 | {
624 | label: '年/月',
625 | value: 'month',
626 | },
627 | {
628 | label: '季度',
629 | value: 'quarter',
630 | },
631 | {
632 | label: '年',
633 | value: 'year',
634 | },
635 | ],
636 | },
637 | showTime: {
638 | title: '时刻',
639 | type: 'string',
640 | 'x-component': 'Select',
641 | 'x-component-props': {
642 | allowClear: true
643 | },
644 | enum: [
645 | {
646 | label: '时+分+秒',
647 | value: 'HH:mm:ss',
648 | },
649 | {
650 | label: '时+分',
651 | value: 'HH:mm',
652 | },
653 | {
654 | label: '时',
655 | value: 'HH',
656 | }
657 | ],
658 | }
659 | }
660 | },
661 | },
662 | },
663 | {
664 | text: '时间选择',
665 | name: 'time',
666 | widget: 'TimePicker',
667 | schema: {
668 | title: '时间选择',
669 | type: 'time',
670 | 'x-component': 'TimePicker',
671 | 'x-component-props': {
672 | placeholder: '请选择时间',
673 | },
674 | },
675 | setting: {
676 | 'x-component-props': {
677 | title: '属性',
678 | type: 'object',
679 | properties: {
680 | placeholder: {
681 | title: 'placeholder',
682 | type: 'string',
683 | },
684 | allowClear: {
685 | title: '是否带清除按钮',
686 | description: '填写内容后才会出现x哦',
687 | type: 'boolean',
688 | 'x-component': 'Switch',
689 | },
690 | showNow: {
691 | title: '是否显示此刻',
692 | type: 'boolean',
693 | 'x-component': 'Switch',
694 | },
695 | use12Hours: {
696 | title: '是否使用12小时制',
697 | type: 'boolean',
698 | 'x-component': 'Switch',
699 | },
700 | suffixIcon: {
701 | title: '后缀文案',
702 | type: 'string',
703 | },
704 | hourStep: {
705 | title: '小时选项间隔',
706 | type: 'number',
707 | 'x-component': 'Number',
708 | 'x-component-props': {
709 | min: 0,
710 | max: 24,
711 | precision: 0
712 | },
713 | },
714 | minuteStep: {
715 | title: '分钟选项间隔',
716 | type: 'number',
717 | 'x-component': 'Number',
718 | 'x-component-props': {
719 | min: 0,
720 | max: 60,
721 | precision: 0
722 | },
723 | },
724 | secondStep: {
725 | title: '秒选项间隔',
726 | type: 'number',
727 | 'x-component': 'Number',
728 | 'x-component-props': {
729 | min: 0,
730 | max: 60,
731 | precision: 0
732 | },
733 | }
734 | }
735 | },
736 | },
737 | },
738 | {
739 | text: '数字滑动条',
740 | name: 'slider',
741 | widget: 'Slider',
742 | schema: {
743 | title: '数字滑动条',
744 | type: 'number',
745 | 'x-component': 'Slider'
746 | },
747 | setting: {
748 | 'x-component-props': {
749 | title: '属性',
750 | type: 'object',
751 | properties: {
752 | showInput: {
753 | title: '是否显示数字框',
754 | type: 'boolean',
755 | 'x-component': 'Switch',
756 | },
757 | min: {
758 | title: '最小值',
759 | type: 'number',
760 | 'x-component': 'Number',
761 | },
762 | max: {
763 | title: '最大值',
764 | type: 'number',
765 | 'x-component': 'Number',
766 | },
767 | step: {
768 | title: '步长',
769 | type: 'number',
770 | 'x-component': 'Number',
771 | min: 0
772 | }
773 | }
774 | }
775 | },
776 | },
777 | {
778 | text: '上传文件',
779 | name: 'upload',
780 | widget: 'Upload',
781 | schema: {
782 | title: '上传文件',
783 | type: 'time',
784 | 'x-component': 'Upload',
785 | 'x-component-props': {
786 | placeholder: '请选择文件',
787 | },
788 | },
789 | setting: {
790 | 'x-component-props': {
791 | title: '属性',
792 | type: 'object',
793 | properties: {
794 | placeholder: {
795 | title: 'placeholder',
796 | type: 'string',
797 | }
798 | }
799 | }
800 | }
801 | }
802 | ];
803 |
804 | const advancedElements = [];
805 |
806 | const layouts = [
807 | {
808 | text: '对象',
809 | name: 'object',
810 | schema: {
811 | title: '对象',
812 | type: 'object',
813 | properties: {},
814 | },
815 | widget: 'map',
816 | setting: {},
817 | },
818 | {
819 | text: '列表',
820 | name: 'list',
821 | widget: 'list',
822 | schema: {
823 | title: '数组',
824 | type: 'array',
825 | items: {
826 | type: 'object',
827 | properties: {},
828 | },
829 | },
830 | // setting: {
831 | // minItems: {
832 | // title: '最小长度',
833 | // type: 'number',
834 | // },
835 | // maxItems: {
836 | // title: '最大长度',
837 | // type: 'number',
838 | // },
839 | // 'x-component-props': {
840 | // title: '属性',
841 | // type: 'object',
842 | // properties: {
843 | // foldable: {
844 | // title: '是否可折叠',
845 | // type: 'boolean',
846 | // },
847 | // },
848 | // },
849 | // },
850 | },
851 | ];
852 |
853 | let result = [elements, advancedElements, layouts];
854 |
855 | result = result.map(list =>
856 | list.map(item => ({
857 | ...item,
858 | setting: { ...commonSettings, ...(item.setting || {}) },
859 | mobileSetting: { ...commonSettings, ...(item.mobileSetting || {}) }
860 | })),
861 | );
862 |
863 | export default result;
864 |
--------------------------------------------------------------------------------