├── .babelrc
├── .build
├── copy-jsonlint.js
├── webpack.config.js
└── webpack.designer.config.js
├── .eslintrc
├── .examples
├── _shared
│ └── form.json
├── antd
│ ├── app.jsx
│ ├── gpt
│ │ ├── app.css
│ │ ├── app.jsx
│ │ ├── index.html
│ │ ├── index.js
│ │ ├── server
│ │ │ ├── .env.smaple
│ │ │ ├── index.mjs
│ │ │ ├── package-lock.json
│ │ │ └── package.json
│ │ ├── speech.js
│ │ └── voice.gif
│ ├── index.html
│ └── index.js
├── react
│ ├── app.jsx
│ ├── index.html
│ └── index.js
├── tdesign-vue
│ ├── app.vue
│ ├── index.html
│ └── index.js
└── vue
│ ├── app.vue
│ ├── index.html
│ └── index.js
├── .github
└── workflows
│ └── publish.yml
├── .gitignore
├── .husky
├── .gitignore
└── pre-commit
├── .npmignore
├── .vscode
└── settings.json
├── CHANGELOG.md
├── README.md
├── assets
├── formast-demo.png
└── logo.psd
├── designer
├── .gitignore
├── .npmignore
├── README.md
├── examples
│ ├── browser
│ │ └── index.html
│ └── spec
│ │ ├── components
│ │ ├── cat-list
│ │ │ └── index.js
│ │ ├── form-group
│ │ │ └── form-group.jsx
│ │ └── input
│ │ │ └── index.js
│ │ ├── example.json
│ │ ├── index.html
│ │ └── index.js
├── package-lock.json
├── package.json
├── src
│ ├── app
│ │ ├── app.jsx
│ │ ├── components-designer.jsx
│ │ ├── layout-designer.jsx
│ │ ├── methods-designer.jsx
│ │ ├── model-designer.jsx
│ │ ├── schema-designer.jsx
│ │ └── state-designer.jsx
│ ├── components
│ │ ├── button
│ │ │ └── button.jsx
│ │ ├── close
│ │ │ └── close.jsx
│ │ ├── confirm
│ │ │ └── confirm.jsx
│ │ ├── designer
│ │ │ └── designer.jsx
│ │ ├── drag-drop
│ │ │ ├── drag-drop-designer.jsx
│ │ │ ├── drag-drop.jsx
│ │ │ └── store.js
│ │ ├── form
│ │ │ ├── associative-input.jsx
│ │ │ └── form.jsx
│ │ ├── icon
│ │ │ └── index.js
│ │ ├── modal
│ │ │ └── modal.jsx
│ │ ├── prompt
│ │ │ └── prompt.jsx
│ │ ├── rich-prop-editor
│ │ │ └── rich-prop-editor.jsx
│ │ └── tabs
│ │ │ └── tabs.jsx
│ ├── config
│ │ ├── components.jsx
│ │ ├── constants.js
│ │ └── model.js
│ ├── hooks
│ │ └── popup.js
│ ├── index.js
│ ├── libs
│ │ ├── popover.js
│ │ └── popup.js
│ ├── styles
│ │ ├── _vars.less
│ │ ├── app.less
│ │ ├── button.less
│ │ ├── components-designer.less
│ │ ├── confirm.less
│ │ ├── designer.less
│ │ ├── drag-drop.less
│ │ ├── form.less
│ │ ├── formast.less
│ │ ├── index.less
│ │ ├── layout-designer.less
│ │ ├── methods-designer.less
│ │ ├── modal.less
│ │ ├── model-designer.less
│ │ ├── popover.less
│ │ ├── popup.less
│ │ ├── prompt.less
│ │ ├── schema-designer.less
│ │ ├── state-designer.less
│ │ └── tabs.less
│ ├── types
│ │ ├── layout.type.js
│ │ └── model.type.js
│ └── utils
│ │ └── index.js
└── webpack.config.js
├── docs
├── .nojekyll
├── CNAME
├── README.md
├── _coverpage.md
├── _sidebar.md
├── advance
│ ├── custom-package.md
│ ├── relation-drive.md
│ └── render-engine.md
├── antd.md
├── contribution.md
├── index.html
├── learn.md
├── logo.png
├── macro.md
├── parser.md
├── quick-start.md
├── react.md
├── scenes
│ ├── compute-relate-to.md
│ ├── required-relate-to.md
│ ├── show-relate-to.md
│ ├── validate-relate-to.md
│ └── validate-when-submit.md
├── schema.md
├── tdesign-vue.md
├── tencent6566531939634536404.txt
├── theme-react.md
├── theme-vue.md
└── vue.md
├── package-lock.json
├── package.json
├── react.d.ts
└── src
├── _shared
├── component-configs.js
├── components.d.ts
├── index.js
├── index.less
└── utils.js
├── antd
├── components.jsx
├── gpt
│ ├── chatgpt.prompt
│ └── gpt.jsx
└── index.js
├── core
├── index.js
├── schema-parser.js
└── utils.js
├── index.js
├── react
├── hooks.js
├── index.js
└── utils.js
├── tdesign-vue
├── components.js
└── index.js
├── theme-react
├── components.jsx
├── index.js
├── input-number.jsx
└── input-select.jsx
├── theme-vue
├── components.js
└── index.js
└── vue
└── index.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "@babel/preset-env",
4 | "@babel/preset-react"
5 | ],
6 | "plugins": [
7 | ["@babel/plugin-proposal-class-properties", {}, "$1"]
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/.build/copy-jsonlint.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs')
2 | const path = require('path')
3 |
4 | const contents = fs.readFileSync(path.resolve(__dirname, '../node_modules/jsonlint/web/jsonlint.js'))
5 | const str = contents.toString()
6 | const code = str.replace('var jsonlint', '/* eslint-disable */\nwindow.jsonlint')
7 | fs.writeFileSync(path.resolve(__dirname, '../src/visual/components/json-code/jsonlint.js'), code)
8 |
--------------------------------------------------------------------------------
/.build/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const TerserPlugin = require('terser-webpack-plugin')
3 | const { merge } = require('webpack-merge')
4 | const MiniCssExtractPlugin = require('mini-css-extract-plugin')
5 | const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
6 | const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
7 |
8 | const createAnalyzer = (name) => {
9 | if (process.env.NODE_ENV === 'production') {
10 | return []
11 | }
12 | return [
13 | new BundleAnalyzerPlugin({
14 | analyzerMode: 'static',
15 | reportFilename: path.resolve(__dirname, `../.reports/${name}.bundle-analysis.html`),
16 | defaultSizes: 'stat',
17 | openAnalyzer: false,
18 | excludeAssets: /\.css$/,
19 | })
20 | ]
21 | }
22 |
23 | const basicConfig = {
24 | mode: process.env.NODE_ENV === 'production' ? 'production' : 'none',
25 | output: {
26 | path: path.resolve(__dirname, '../dist'),
27 | library: {
28 | type: 'umd',
29 | name: 'formast/[name]',
30 | },
31 | },
32 | module: {
33 | rules: [
34 | {
35 | test: /\.jsx?$/,
36 | loader: 'babel-loader',
37 | exclude: [/node_modules/],
38 | options: {
39 | 'presets': [
40 | '@babel/preset-env',
41 | '@babel/preset-react',
42 | ],
43 | 'plugins': [
44 | '@babel/plugin-proposal-class-properties',
45 | ['@babel/plugin-transform-runtime', { 'regenerator': true }]
46 | ]
47 | },
48 | },
49 | ]
50 | },
51 | target: 'web',
52 | optimization: {
53 | minimize: process.env.NODE_ENV === 'production',
54 | minimizer: [
55 | new TerserPlugin({
56 | parallel: true,
57 | extractComments: false,
58 | }),
59 | ],
60 | nodeEnv: process.env.NODE_ENV,
61 | concatenateModules: false,
62 | sideEffects: true,
63 | },
64 | resolve: {
65 | alias: {
66 | tyshemo: 'tyshemo/src',
67 | 'ts-fns': 'ts-fns/es',
68 | },
69 | },
70 | }
71 |
72 | const core = merge(basicConfig, {
73 | entry: path.resolve(__dirname, '../src/core/schema-parser.js'),
74 | output: {
75 | filename: 'index.js',
76 | library: {
77 | name: 'formast',
78 | },
79 | },
80 | plugins: createAnalyzer('core'),
81 | })
82 |
83 | const react = merge(basicConfig, {
84 | entry: path.resolve(__dirname, '../src/react/index.js'),
85 | output: {
86 | filename: 'react.js',
87 | library: {
88 | name: 'formast/react',
89 | },
90 | },
91 | externals: {
92 | react: {
93 | amd: 'react',
94 | commonjs: 'react',
95 | commonjs2: 'react',
96 | root: 'React',
97 | },
98 | },
99 | plugins: createAnalyzer('react'),
100 | })
101 |
102 | const vue = merge(basicConfig, {
103 | entry: path.resolve(__dirname, '../src/vue/index.js'),
104 | output: {
105 | filename: 'vue.js',
106 | library: {
107 | name: 'formast/vue',
108 | },
109 | },
110 | externals: {
111 | vue: {
112 | amd: 'vue',
113 | commonjs: 'vue',
114 | commonjs2: 'vue',
115 | root: 'Vue',
116 | },
117 | },
118 | plugins: createAnalyzer('vue'),
119 | })
120 |
121 | module.exports = [
122 | core, react, vue,
123 | ]
124 |
--------------------------------------------------------------------------------
/.build/webpack.designer.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const TerserPlugin = require("terser-webpack-plugin")
3 | const MiniCssExtractPlugin = require('mini-css-extract-plugin')
4 | const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
5 |
6 | const CssConfig = {
7 | get loader() {
8 | return process.env.CSS_IN_JS ? 'style-loader' : MiniCssExtractPlugin.loader
9 | },
10 | get minimizer() {
11 | return process.env.CSS_IN_JS ? [] : [new CssMinimizerPlugin()]
12 | },
13 | get plugins() {
14 | return process.env.CSS_IN_JS ? [] : [
15 | new MiniCssExtractPlugin({
16 | filename: 'designer.css',
17 | }),
18 | ]
19 | },
20 | get filename() {
21 | return process.env.CSS_IN_JS ? 'designer.one.js' : 'designer.js'
22 | },
23 | }
24 |
25 | module.exports = {
26 | mode: 'production',
27 | entry: [
28 | path.resolve(__dirname, '../src/designer/styles.js'),
29 | path.resolve(__dirname, '../src/designer/index.js'),
30 | ],
31 | output: {
32 | path: path.resolve(__dirname, '../dist'),
33 | filename: CssConfig.filename,
34 | library: {
35 | type: 'umd',
36 | name: 'formast-designer',
37 | },
38 | globalObject: 'window',
39 | },
40 | module: {
41 | rules: [
42 | {
43 | test: /\.jsx?$/,
44 | loader: 'babel-loader',
45 | exclude: [/node_modules/],
46 | options: {
47 | "presets": [
48 | "@babel/preset-env",
49 | "@babel/preset-react"
50 | ],
51 | "plugins": [
52 | "@babel/plugin-proposal-class-properties",
53 | ["@babel/plugin-transform-runtime", { "regenerator": true }]
54 | ]
55 | }
56 | },
57 | {
58 | test: /\.less$/,
59 | use: [
60 | CssConfig.loader,
61 | {
62 | loader: 'css-loader',
63 | },
64 | {
65 | loader: 'less-loader',
66 | },
67 | ],
68 | },
69 | {
70 | test: /\.css$/,
71 | use: [
72 | CssConfig.loader,
73 | {
74 | loader: 'css-loader',
75 | },
76 | ],
77 | },
78 | ]
79 | },
80 | target: 'web',
81 | optimization: {
82 | minimize: true,
83 | minimizer: [
84 | ...CssConfig.minimizer,
85 | new TerserPlugin({
86 | parallel: true,
87 | extractComments: false,
88 | }),
89 | ],
90 | nodeEnv: process.env.NODE_ENV,
91 | concatenateModules: false,
92 | sideEffects: true,
93 | },
94 | plugins: [
95 | ...CssConfig.plugins,
96 | ],
97 | }
98 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "settings": {
3 | "react": {
4 | "version": "detect"
5 | }
6 | },
7 | "parser": "babel-eslint",
8 | "parserOptions": {
9 | "ecmaFeatures": {
10 | "jsx": true
11 | }
12 | },
13 | "extends": [
14 | "eslint-config-tencent",
15 | "plugin:react/recommended"
16 | ],
17 | "plugins": [
18 | "react"
19 | ],
20 | "rules": {
21 | "react/prop-types": 0,
22 | "no-console": ["error", { "allow": ["debug", "warn", "error"] }]
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/.examples/antd/app.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import schemaJson from '../_shared/form.json';
3 | import * as Options from '../../src/antd';
4 | import { Form } from '../react/app.jsx'
5 |
6 | export default function App() {
7 | return
8 | }
9 |
--------------------------------------------------------------------------------
/.examples/antd/gpt/app.css:
--------------------------------------------------------------------------------
1 | section {
2 | height: 100%;
3 | flex: 1;
4 | display: flex;
5 | position: relative;
6 | }
7 |
8 | .voice-overlay {
9 | position: absolute;
10 | top: 0;
11 | left: 0;
12 | width: 100%;
13 | height: 100%;
14 | background-color: rgba(0, 0, 0, .3);
15 | display: flex;
16 | align-items: center;
17 | justify-content: center;
18 | display: none;
19 | }
20 |
21 | .voice-overlay img {
22 | width: 200px;
23 | height: auto;
24 | }
25 |
26 | main {
27 | flex: 1;
28 | display: flex;
29 | }
30 |
31 | .center {
32 | flex: 1;
33 | display: flex;
34 | align-items: center;
35 | justify-content: center;
36 | }
37 |
38 | aside {
39 | width: 360px;
40 | display: flex;
41 | flex-direction: column;
42 | }
43 |
44 | .prompt {
45 | flex: 1;
46 | overflow-x: hidden;
47 | display: flex;
48 | }
49 |
50 | .prompt-input {
51 | flex: 1;
52 | }
53 |
54 | .buttons {
55 | margin-top: 20px;
56 | display: flex;
57 | align-items: center;
58 | justify-content: space-between;
59 | }
60 |
--------------------------------------------------------------------------------
/.examples/antd/gpt/app.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useRef } from 'react';
2 | import SchemaForm from '../../../src/antd/gpt/gpt';
3 | import { Button, Spin, Input, message } from 'antd';
4 | import { AudioOutlined } from '@ant-design/icons';
5 | import VoiceGif from './voice.gif';
6 | import { createSspeechRecorder } from './speech'
7 | import './app.css';
8 |
9 | const { TextArea } = Input;
10 |
11 | export default function App() {
12 | const [prompt, setPropmt] = useState(`
13 | 模型:
14 | name 姓名 string
15 | age 年龄 number
16 | sex 性别 string options=男,女
17 | 字段逻辑:
18 | 当年龄大于10时,性别才展示,否则隐藏
19 | 当年龄大于15时,性别必填
20 | 布局:
21 | 姓名
22 | 年龄
23 | 性别
24 | 提交目标地址:
25 | /api/save
26 | `.trim());
27 | const [schema, setSchema] = useState({});
28 | const [loading, setLoading] = useState(false);
29 | const [recording, setRecording] = useState(false);
30 | const speechRecorder = useRef();
31 |
32 | const handleSend = async () => {
33 | if (!prompt.trim()) {
34 | message.error('请填写提示语');
35 | return;
36 | }
37 |
38 | setLoading(true);
39 | try {
40 | const res = await fetch('http://127.0.0.1:3005/api/gpt', {
41 | method: 'post',
42 | body: JSON.stringify({ prompt }),
43 | headers: {
44 | 'Content-Type': 'application/json',
45 | },
46 | }).then(res => res.json());
47 | const { code, error, data } = res;
48 | if (code) {
49 | message.error(error)
50 | }
51 | else {
52 | const { schema } = data;
53 | setSchema(schema)
54 | }
55 | }
56 | catch (e) {
57 | message.error(e.message)
58 | }
59 | finally {
60 | setLoading(false)
61 | }
62 | }
63 |
64 | const handleEndSpeech = () => {
65 | setRecording(false);
66 | speechRecorder.current?.stop();
67 | speechRecorder.current = null;
68 | }
69 |
70 | const handleStartSpeech = () => {
71 | setRecording(true)
72 | speechRecorder.current = createSspeechRecorder({
73 | onChange: setPropmt,
74 | onEnd: handleEndSpeech,
75 | })
76 | }
77 |
78 | return (
79 |
80 |
81 | {loading ?
: prompt ? : null}
82 |
83 |
90 |
91 |
92 | );
93 | }
94 |
--------------------------------------------------------------------------------
/.examples/antd/gpt/index.html:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/.examples/antd/gpt/index.js:
--------------------------------------------------------------------------------
1 | import 'antd/dist/antd.css';
2 | import React from 'react'
3 | import ReactDOM from 'react-dom'
4 | import App from './app.jsx'
5 |
6 | ReactDOM.render(
7 | React.createElement(App),
8 | document.querySelector('#root')
9 | )
10 |
--------------------------------------------------------------------------------
/.examples/antd/gpt/server/.env.smaple:
--------------------------------------------------------------------------------
1 | HOST=127.0.0.1
2 | PORT=3005
3 | API=/api/chatgpt
4 |
--------------------------------------------------------------------------------
/.examples/antd/gpt/server/index.mjs:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import express from 'express'
3 | import dotenv from 'dotenv'
4 |
5 | dotenv.config()
6 |
7 | const app = express()
8 |
9 | app.use(express.urlencoded({ extended: false }))
10 | app.use(express.json())
11 |
12 | app.all('*', (_, res, next) => {
13 | res.header('Access-Control-Allow-Origin', '*')
14 | res.header('Access-Control-Allow-Headers', '*')
15 | res.header('Access-Control-Allow-Methods', '*')
16 | res.header('Access-Control-Allow-Credentials', 'true')
17 | next()
18 | })
19 |
20 | app.post('/api/gpt', async (req, res) => {
21 | const { prompt } = req.body
22 | const content = `
23 | JSON结构规则:
24 | {
25 | // 模型描述
26 | model: {
27 | // 字段属性
28 | [key: string]: {
29 | // 默认值
30 | // string = ""
31 | // number = null
32 | // 如果存在options,使用options中第一个option的value
33 | default: any
34 |
35 | // 字段文本
36 | label: string
37 |
38 | // 字段类型
39 | type: string
40 |
41 | // 隐藏逻辑,支持表达式
42 | hidden?: boolean | string
43 |
44 | // 必填逻辑,支持表达式
45 | required?: boolean | string
46 |
47 | // 最小值(包含)
48 | min?: number
49 |
50 | // 最大值(包含)
51 | max?: number
52 |
53 | // 只读,字段值不允许修改,支持表达式
54 | readonly?: boolean | string
55 |
56 | // 字段由计算获得,其值必须是表达式
57 | compute?: string
58 |
59 | // 备选项
60 | options?: {
61 | // 选项文本
62 | label: string
63 | // 选项值
64 | value: string
65 | }
66 |
67 | // 其他属性
68 | [key: string]: string
69 | }
70 | }
71 |
72 | // 布局描述
73 | // 布局有3层
74 | layout: {
75 | // 顶层组件,页面
76 | type: 'Form'
77 |
78 | attrs: {
79 | // 表单提交的目标地址
80 | action: string
81 | }
82 |
83 | // 页面内有多少行
84 | children: {
85 | // 一行组件
86 | type: 'FormItem'
87 |
88 | // 这一行绑定的模型上的字段,用于展示label等信息
89 | // 如果这一行只有一个字段需要被输入,那么直接绑定这个字段
90 | bind: string
91 |
92 | // 行内输入组件
93 | children: {
94 | // 使用的组件名
95 | // 组件名支持:
96 | // Input 单行文本输入框
97 | // InputNumber 数字输入框
98 | // Select 下拉选择框
99 | // InputText 多行文本输入框
100 | // Radios 单选按钮组
101 | // Checkboxes 多选按钮组
102 | type: string
103 |
104 | // 绑定模型字段,绑定后,字段的元数据信息会被绑定到组件上,组件不需要传入其他任何配置信息
105 | bind: string
106 | }[]
107 | }[]
108 | }
109 | }
110 |
111 | 动态语法规则:
112 | 使用"{ ... }"来表达该值是一个表达式,例如"{ name }",其中name是字段名。
113 |
114 | ${prompt.trim()}
115 |
116 | 期望格式: JSON,只返回JSON代码。
117 | `
118 |
119 | const promptMessage = content.replace(/\s+/g, ' ').trim()
120 |
121 | const now = new Date()
122 | const month = now.getMonth() + 1
123 | const today = now.getFullYear() + '-' + (month < 10 ? '0' + month : month) + '-' + now.getDate()
124 |
125 | try {
126 | const result = await axios.post(process.env.API, {
127 | prompt: promptMessage,
128 | systemMessage: `You are ChatGPT, a large language model trained by OpenAI. Answer as concisely as possible.\nKnowledge cutoff: 2021-09-01\nCurrent date: ${today}`,
129 | options: {},
130 | })
131 | const { data } = result
132 |
133 | if (data.status === 'Success') {
134 | const { id, text } = data.data
135 | try {
136 | const schema = JSON.parse(text)
137 | res.json({ code: 0, data: { id, schema } })
138 | }
139 | catch (e) {
140 | console.log(prompt)
141 | console.error('返回结果:', text)
142 | res.status(415)
143 | res.json({ code: 415, error: '结果不是JSON,请重新输入表单描述试试' })
144 | }
145 | return
146 | }
147 |
148 | console.error('返回错误:', data.message)
149 | res.status(415)
150 | res.json({ code: 415, error: data.message })
151 | }
152 | catch (e) {
153 | console.error(e)
154 | res.status(500)
155 | res.json({ error: e.message })
156 | }
157 | })
158 |
159 | app.listen(process.env.PORT, process.env.HOST, () => console.log(`服务启动 http://${process.env.HOST}:${process.env.PORT}`))
160 |
--------------------------------------------------------------------------------
/.examples/antd/gpt/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "server",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "keywords": [],
7 | "author": "",
8 | "license": "ISC",
9 | "dependencies": {
10 | "axios": "^1.3.4",
11 | "dotenv": "^16.0.3",
12 | "express": "^4.18.2"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/.examples/antd/gpt/speech.js:
--------------------------------------------------------------------------------
1 | // 兼容处理
2 | const __SpeechRecognition = window.SpeechRecognition ?? window.webkitSpeechRecognition;
3 |
4 | class SpeechRec extends __SpeechRecognition {
5 | // 识别成功的钩子函数
6 | onResult(fn) {
7 | this.addEventListener("result", fn);
8 | }
9 |
10 | // 识别结束的钩子函数
11 | onEnd(fn) {
12 | this.addEventListener("end", fn);
13 | }
14 |
15 | onSpeechStart(fn) {
16 | this.addEventListener('speechstart', fn)
17 | }
18 |
19 | onSpeechEnd(fn) {
20 | this.addEventListener('speechend', fn)
21 | }
22 |
23 | // https://developer.mozilla.org/en-US/docs/Web/API/SpeechRecognition/SpeechRecognition
24 | setSettings(settings) {
25 | const { grammars: grammarStrs, lang, interimResults, maxAlternatives, continuous } = settings
26 |
27 | if (grammarStrs) {
28 | const grammars = new SpeechGrammarList()
29 | grammarStrs.map((str) => {
30 | grammars.addFromString(str, 1)
31 | })
32 | this.grammars = grammars
33 | }
34 |
35 | if (lang) {
36 | this.lang = lang
37 | }
38 |
39 | if (interimResults) {
40 | this.interimResults = interimResults
41 | }
42 |
43 | if (maxAlternatives) {
44 | this.maxAlternatives = maxAlternatives
45 | }
46 |
47 | if (continuous) {
48 | this.continuous = continuous
49 | }
50 | }
51 | }
52 |
53 | const speechRec = new SpeechRec()
54 | speechRec.setSettings({
55 | continuous: true,// 多次识别语音
56 | interimResults: true,// 在识别过程中是否允许更新识别的结果
57 | lang: "zh-CN",// 语言
58 | })
59 |
60 | export function createSspeechRecorder({
61 | // 说话过程中内容变化,用于展示文本
62 | onChange,
63 | // 说了一段话结束,注意,此时服务并没有结束,可以继续说话
64 | onEnd,
65 | }) {
66 | speechRec.onResult((event) => {
67 | const text = Array.from(event.results).map(result => result[0].transcript)
68 | .join('')
69 | .trim()
70 | onChange(text)
71 | })
72 |
73 | speechRec.onSpeechEnd(() => {
74 | onEnd?.()
75 | })
76 |
77 | speechRec.start()
78 |
79 | return speechRec
80 | }
81 |
--------------------------------------------------------------------------------
/.examples/antd/gpt/voice.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tencent-cdc/formast/d354ba0ab11dfb027618aabbcccb153b74f8c84e/.examples/antd/gpt/voice.gif
--------------------------------------------------------------------------------
/.examples/antd/index.html:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.examples/antd/index.js:
--------------------------------------------------------------------------------
1 | import 'antd/dist/antd.css';
2 | import React from 'react'
3 | import ReactDOM from 'react-dom'
4 | import App from './app.jsx'
5 |
6 | ReactDOM.render(
7 | React.createElement(App),
8 | document.querySelector('#root')
9 | )
10 |
--------------------------------------------------------------------------------
/.examples/react/app.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useCallback, useRef } from 'react';
2 | import { Formast } from '../../src/react';
3 | import schemaJson from '../_shared/form.json';
4 | import { isEmpty } from 'ts-fns';
5 | import * as Options from '../../src/theme-react/index.js';
6 |
7 | export default function App() {
8 | return
9 | }
10 |
11 | export function Form(props) {
12 | const [errors, setErrors] = useState([]);
13 | const [data, setData] = useState({});
14 | const [random, setRandom] = useState(Math.random());
15 | const ref = useRef();
16 |
17 | const handleSubmit = useCallback((e) => {
18 | e.preventDefault();
19 |
20 | const errors = ref.current.validate();
21 | if (errors.length) {
22 | setErrors(errors);
23 | setData({});
24 | return;
25 | }
26 |
27 | const data = ref.current.toData();
28 | setErrors([]);
29 | setData(data);
30 | }, []);
31 |
32 | const getJson = () => new Promise((r) => {
33 | setTimeout(() => r(props.schemaJson), 2000);
34 | });
35 |
36 | const onSetRandom = () => {
37 | setRandom(Math.random());
38 | };
39 |
40 | return (
41 |
42 |
{
51 | ref.current = model;
52 | window.__model = model;
53 | }}
54 | >
55 | 正在加载...
56 |
57 |
58 | {errors.map((err, i) => {
59 | return
{err.message}
60 | })}
61 |
62 |
63 | {isEmpty(data) ? null : JSON.stringify(data, null, 4)}
64 |
65 |
66 | );
67 | }
68 |
--------------------------------------------------------------------------------
/.examples/react/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.examples/react/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import App from './app.jsx'
4 |
5 | ReactDOM.render(
6 | React.createElement(App),
7 | document.querySelector('#root')
8 | )
--------------------------------------------------------------------------------
/.examples/tdesign-vue/app.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
11 |
{{data}}
12 |
13 |
14 |
15 |
21 |
22 |
76 |
--------------------------------------------------------------------------------
/.examples/tdesign-vue/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.examples/tdesign-vue/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import App from './app.vue'
3 |
4 | new Vue({
5 | el: '#root',
6 | render: h => h(App),
7 | })
8 |
--------------------------------------------------------------------------------
/.examples/vue/app.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 正在加载...
5 |
6 |
9 |
{{data}}
10 |
11 |
12 |
13 |
64 |
--------------------------------------------------------------------------------
/.examples/vue/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.examples/vue/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import App from './app.vue'
3 |
4 | new Vue({
5 | el: '#root',
6 | render: h => h(App),
7 | })
8 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish
2 | on:
3 | push: ## pr merged
4 | ## only works on master branch
5 | branches:
6 | - master
7 | jobs:
8 | publish:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v2
12 | - name: Use Node.js 14.x
13 | uses: actions/setup-node@v1
14 | with:
15 | node-version: 14.x
16 | registry-url: https://registry.npmjs.org/
17 | - run: npm set config loglevel=info
18 | - name: Install
19 | run: npm ci --verbose || npm i --verbose
20 | - name: Can Version Publish
21 | run: npx can-npm-publish
22 | - name: Lint
23 | run: npm run lint
24 | - name: Build
25 | run: npm run build
26 | - name: Publish NPM
27 | uses: JS-DevTools/npm-publish@v1
28 | with:
29 | token: "${{ secrets.NPM_AUTH_TOKEN }}"
30 | - name: Git Tag
31 | uses: butlerlogic/action-autotag@stable
32 | with:
33 | GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
34 | tag_prefix: "v"
35 | - name: Git Push Changes
36 | uses: ad-m/github-push-action@master
37 | with:
38 | github_token: ${{ secrets.GITHUB_TOKEN }}
39 | branch: ${{ github.ref }}
40 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .cache/
3 | /dist/
4 | /*.js
5 | /*.jsx
6 | .egwrc
7 | /react/
8 | /core/
9 | /resources/
10 | /visual/
11 | /antd/
12 | /react-d/
13 | jsonlint.js
14 | /vue/
15 | /.reports/
16 | /vue-d/
17 | /_shared/
18 | /theme-vue/
19 | /theme-react/
20 | /tdesign-vue/
21 |
--------------------------------------------------------------------------------
/.husky/.gitignore:
--------------------------------------------------------------------------------
1 | _
2 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | npm run lint
5 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .babelrc
2 | .examples/
3 | .cache/
4 | .build/
5 | .ci/
6 | .husky/
7 | .vscode/
8 | /docs/
9 | /doc/
10 | code.yml
11 | webpack.config.js
12 | .orange-ci.yml
13 | CNAME
14 | .egwrc
15 | .gitignore
16 | .github/
17 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.codeActionsOnSave": {
3 | "source.fixAll.eslint": true,
4 | "source.fixAll.stylelint": true
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4 |
5 | ### 2.2.1 (2023-02-14)
6 |
7 | ## 2.2.0 (2023-02-10)
8 |
9 |
10 | ### Features
11 |
12 | * 添加了designer ([5438a7f](https://github.com/tencent-cdc/formast/commit/5438a7ffe706be81ea5bdd1df05fc698d9de4590))
13 |
14 | ### 2.1.8 (2022-09-07)
15 |
16 | ### 2.1.7 (2022-06-24)
17 |
18 | ### 2.1.6 (2022-06-24)
19 |
20 | ### 2.1.5 (2022-06-24)
21 |
22 | ### 2.1.4 (2022-06-23)
23 |
24 |
25 | ### Bug Fixes
26 |
27 | * 修复一些特殊情况下读值报错 ([bfc6de7](https://github.com/tencent-cdc/formast/commit/bfc6de7031d83c435a84a3fb0ef2dbd123301766))
28 |
29 | ### 2.1.3 (2022-03-06)
30 |
31 | ### 2.1.2 (2022-03-02)
32 |
33 |
34 | ### Bug Fixes
35 |
36 | * 解决微信屏蔽问题 ([d4a11ca](https://github.com/tencent-cdc/formast/commit/d4a11cac4db8fa65f889b006a84948feed7ca3e8))
37 |
38 | ### 2.1.1 (2022-01-17)
39 |
40 | ## 2.1.0 (2022-01-17)
41 |
42 |
43 | ### Features
44 |
45 | * 支持layout直接传数组 ([1d9ce0d](https://github.com/tencent-cdc/formast/commit/1d9ce0d89c271e3372994fd94d726464b7decf58))
46 |
47 | ### 2.0.3 (2022-01-11)
48 |
49 |
50 | ### Bug Fixes
51 |
52 | * update deps ([9ff33d3](https://github.com/tencent-cdc/formast/commit/9ff33d3b39f6afcac0ef353a4b0b82bc5f20f2b0))
53 |
54 | ### 2.0.2 (2022-01-10)
55 |
56 | ### 2.0.1 (2022-01-10)
57 |
58 |
59 | ### Bug Fixes
60 |
61 | * 修复发版问题 ([0b1ee3a](https://github.com/tencent-cdc/formast/commit/0b1ee3a9e901277b24d0726bba7eae8b8fbb69cc))
62 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Formast(帆桅)复杂业务场景下的企业级动态表单框架
7 |
8 |
9 |
10 |
11 |
12 | ## :hear_no_evil: 什么是 Formast?
13 |
14 | Formast(帆桅)是一款应对复杂业务场景的动态表单框架,它基于(后端的)JSON 实现表单的界面渲染、交互、数据提交。
15 |
16 | 复杂业务系统中,存在很强的表单灵活性需求。业务方实际业务的快速变化,对表单的细节的变更很频繁。通过将表单的描述以 JSON 的形式动态交由前端渲染,可以在某些场景下,大大提升应对复杂业务的细节变更能力,而无需经过前后端代码修改和发版过程,让表单更加灵活,降低企业在表单处理中的开发成本。
17 |
18 | Formast 从表单的原子逻辑出发,抽象出一门独特的描述语言,基于特定 Schema 的 JSON,解释为不同平台的表单交互界面,既可以跨框架,也可以跨终端。分层设计,区分模型与视图,从开发模式上彻底解决表单中数据与视图关系的混杂不清问题。独特的基于计算的依赖联动,让使用者以最简单的方式处理字段与字段之间的联动关系。在运行过程中,Formast 拥有不俗的性能。同时,相比于其他表单方案,Formast 更易于理解,更容易上手。
19 |
20 | ## :tada: 项目特色
21 |
22 | - 专门为复杂业务场景的企业级表单设计,考虑到几十种业务表单的复杂需求,与大多数动态表单生成器都不同
23 | - 表单抽象语言 Schema JSON 表达形式
24 | - 分离表单 JSON 解释引擎和渲染引擎,轻松实现跨平台渲染
25 | - 超高的性能
26 | - 与现有框架无缝对接,支持 React、Vue,只提供核心驱动,其他全由开发者自由实现
27 | - 分层理念:表单分为视图层、模型层、控制层(由 Formast 实现),在特定场景下这将大大提升表单的可靠性
28 |
29 | ## :book: 使用方法
30 |
31 | 你可以通过 npm 安装 formast:
32 |
33 | ```
34 | npm i formast
35 | ```
36 |
37 | 也可以通过 CDN 直接引入 Foramst:
38 |
39 | ```html
40 |
41 | ```
42 |
43 | 它的导出方式有两种,不同的导出方式对构建工具或依赖的要求不同,你可以通过[快速上手](https://formast.js.org/#/quick-start)获得相关的使用信息。
44 |
45 | 大多数情况下,我们会使用 react 或 vue 的引擎来在项目中使用,通过读取后端接口返回的 JSON 来创建表单。具体效果如图:
46 |
47 | [](https://codesandbox.io/s/dazzling-matan-d1c4j)
48 |
49 | 更多使用详情请 [阅读文档](https://formast.js.org)。
50 |
51 | ## :camel: 参与贡献
52 |
53 | 我们欢迎社区中所有同学参与贡献,无论是发现了某些代码层面的 bug,还是文档中的错别字,抑或功能上的缺陷,都可以向我们提 PR。如果你希望 Formast 在功能上进行扩展,或想到一些不错的想法,也可以在社区中向我们提出,一起探讨。
54 |
55 | 具体的贡献手册请[阅读这里](docs/contribution.md)。
56 | ## :balance_scale: 开源许可
57 |
58 | MIT
59 |
--------------------------------------------------------------------------------
/assets/formast-demo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tencent-cdc/formast/d354ba0ab11dfb027618aabbcccb153b74f8c84e/assets/formast-demo.png
--------------------------------------------------------------------------------
/assets/logo.psd:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tencent-cdc/formast/d354ba0ab11dfb027618aabbcccb153b74f8c84e/assets/logo.psd
--------------------------------------------------------------------------------
/designer/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .cache/
3 | dist/
4 | docs/
--------------------------------------------------------------------------------
/designer/.npmignore:
--------------------------------------------------------------------------------
1 | .babelrc
2 | examples/
3 | .cache/
4 | code.yml
5 | webpack.config.js
6 | docs/
7 |
--------------------------------------------------------------------------------
/designer/README.md:
--------------------------------------------------------------------------------
1 | FORMAST DESIGNER
2 | ================
3 |
4 | Formast(帆桅)表单可视化设计器,用以通过填写拖拽等界面形式,生成用于 formast 的 JSON。
5 |
6 | ## 安装
7 |
8 | ```
9 | npm i formast-designer
10 | ```
11 |
12 | 你也可以通过 CDN 进行引用。
13 |
14 | ```html
15 |
16 | ```
17 |
18 | CDN 引用时,接口通过 `window['formast-designer']` 导出。
19 |
20 | ```js
21 | const { createFormastDesigner } = window['formast-designer']
22 | ```
23 |
24 | ## 使用
25 |
26 | ```js
27 |
28 |
29 | const designer = createFormastDesigner('#designer', config)
30 | ```
31 |
32 | 通过 `createFormastDesigner` 你可以创建一个 FormastDesigner 实例,并且挂载在一个 DOM 节点上,你需要设置这个 DOM 节点的宽高,从而限制设计器内部的宽高。另外,你还需要自己撰写部分 css 来控制 formast 的表单元素的样式。
33 |
34 | 所有的交互效果内部都已经写好了,可自定义的东西,需要通过 `config` 进行配置,但是 `config` 是可选的。
35 |
36 | 设计器对外暴露的接口如下:
37 |
38 | ### on(event, callback)
39 |
40 | 监听设计器内部暴露的事件。
41 |
42 | ```js
43 | designer.on('save', (json) => {
44 | console.log(json)
45 | })
46 | ```
47 |
48 | 内置事件包含:
49 |
50 | - save: 点击保存按钮,点击按钮没有任何效果,你需要自己在回调函数中写具体效果
51 | - reset: 点击重置按钮,点击该按钮不会真的马上重置设计器内容,需要你在回调函数中通过调用 setJSON 和 refresh 方法刷新设计器内容。
52 | - import: 点击导入按钮后触发,点击会弹窗对话框让你选择 JSON 文件。
53 | - export: 点击导出按钮后触发,下载按钮会帮你下载一份 JSON。
54 |
55 | ### getJSON()
56 |
57 | 获取当前设计器中的内容能生成的 JSON 对象。
58 |
59 | ### setJSON(json)
60 |
61 | 设置设计器中的新 JSON,它不会触发设计器内界面更新,一般需要配合 refresh 一起使用。
62 |
63 | ### refresh()
64 |
65 | 更新设计器内的界面。
66 |
67 | ### mount(el)/unmount()
68 |
69 | 将设计器挂载/卸载。
70 | 使用 `createFormastDesigner` 时,会自动挂载。
71 |
72 | ## 配置
73 |
74 | 在使用 `createFormastDesigner` 时,你可以传入一个 `config` 来进行自定义配置。
75 |
76 | ```
77 | {
78 | disableSave: 禁用保存按钮,
79 | disableReset: 禁用重置按钮,
80 | disableImport: 禁用导入按钮,
81 | disableExport: 禁用导出按钮,
82 | itemsSetting: { // 对组件设计进行配置
83 | groups: [ // 分组,在设计器右侧素材区,不同素材将被分组
84 | {
85 | id: String, // 给分组分配一个唯一标识,可以是已经存在的分组,配置覆盖的时候,同 id 的分组信息将被合并
86 | title: String, // 分组显示名
87 | items: [
88 | {
89 | id: String, // 组件的名字,将作为组件在 formast 中使用
90 | title: String, // 组件显示名
91 | icon: ifexist(String), // 组件显示时前面可以有一个 icon,icon 从 react-icons 所有 MIT 的图标中挑选
92 | direction: ifexist('h'), // 组件内部元素是否横向排布(默认是纵向的)
93 |
94 | // 组件的 props 配置,通过该配置,在设计时,点击组件的 geer icon,会展开配置界面
95 | props: ifexist([
96 | {
97 | key: String, // prop 的名字,例如 type, value, onChange 等
98 | types: new List(Object.values(VALUE_TYPES)), // 该 prop 可以支持哪种类型的值,0-纯文本, 1-表达式, 2-函数式,具体你可以在配置的时候传入不同值看效果
99 | title: ifexist(String), // prop 在配置界面的显示名
100 | defender: ifexist(Function), // prop 的值会被直接反应到设计器界面上让你可以即使预览效果,但是有的时候,你填写的内容会有问题,通过 defender 来兜底这种问题
101 | }
102 | ]),
103 |
104 | allows: ifexist([String]), // 组件内部如果可以挂载子组件,那么可以挂载那些子组件,可以填写其他 item 的 id/tag
105 | needs: ifexist([String]), // 组件如果要被以子组件形式挂载,那么只允许这些组件挂载自己
106 | tag: ifexist(String), // allows/needs 根据 id/tag 来进行判断,但是每个 item 的 id 是唯一的,而不同 item tag 可以相同,这样可以批量处理一些是否允许挂载问题
107 |
108 | recoverGroupsFromJSON(children), // 将来自上层传递的JSON格式化为符合mount中DropBox需要的内容
109 | convertGroupsToJSON(groups), // 将来自下层DropBox提交格式化为最终保存的JSON
110 |
111 | // 当前这个 item 在渲染到设计器中时,如何进行渲染?
112 | // 你需要自己写挂载程序来决定渲染效果
113 | mount(el, monitor) {
114 | // monitor 在下面详细解释
115 | ReactDOM.render(, el)
116 | },
117 | // 当组件更新时,该方法被调用,如果是 react 系统,mount 和 update 一般是一样的,但是,其他系统中,mount 和 update 可能不同,因此,设计时,我将这两个东西分开
118 | update(el, monitor) {},
119 | // 点击 delete 图标删除组件时如何卸载组件
120 | unmount(el) {
121 | ReactDOM.unmountComponentAtNode(el)
122 | },
123 | }
124 | ]
125 | }
126 | ]
127 | },
128 | layoutSetting: {}, // 和 items 内容结构一样,只是作用在表单设计上
129 | }
130 | ```
131 |
132 | 这个配置对象比较复杂,但是你慢慢阅读,可以理解其中的设计逻辑。
133 |
134 | **Monitor**
135 |
136 | 在 `mount` `update` 配置中,提供了 `monitor` 这个参数,它用于提供一些设计器内部的运行时结果。
137 | 你可以通过 console.log 打印出来看它有些什么东西。
138 | 比较常用的是 `DropBox` 和 `getComputedProps`。
139 |
140 | `DropBox` 是用于提供内部可以放置子组件的 React 组件,目前仅支持 react 系统实现内部子组件效果。具体使用如下:
141 |
142 | ```js
143 | mount(el, monitor) {
144 | const { DropBox } = monitor
145 | ReactDOM.render(
146 | 子组件:
147 |
148 | , el)
149 | }
150 | ```
151 |
152 | 将 DropBox 放在你需要拖放的位置,这样,你就可以为某一个组件放置子组件。
153 |
154 | `getComputedProps` 则用于根据 props 配置项以及你在配置面板中填写的内容动态计算一个 props 对象给你,你可以用这些 props 完成更详细的预览效果,例如:
155 |
156 | ```js
157 | mount(el, monitor) {
158 | const { getComputedProps } = monitor
159 | const props = getComputedProps()
160 | ReactDOM.render(, el)
161 | }
162 | ```
163 |
--------------------------------------------------------------------------------
/designer/examples/browser/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
14 |
15 |
16 |
17 |
18 |
23 |
--------------------------------------------------------------------------------
/designer/examples/spec/components/cat-list/index.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tencent-cdc/formast/d354ba0ab11dfb027618aabbcccb153b74f8c84e/designer/examples/spec/components/cat-list/index.js
--------------------------------------------------------------------------------
/designer/examples/spec/components/form-group/form-group.jsx:
--------------------------------------------------------------------------------
1 | import { React, Section, Text } from 'nautil'
2 |
3 | export function FormGroup(props) {
4 | const { title, children } = props
5 |
6 | return (
7 |
11 | )
12 | }
--------------------------------------------------------------------------------
/designer/examples/spec/components/input/index.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tencent-cdc/formast/d354ba0ab11dfb027618aabbcccb153b74f8c84e/designer/examples/spec/components/input/index.js
--------------------------------------------------------------------------------
/designer/examples/spec/example.json:
--------------------------------------------------------------------------------
1 | {
2 | "model": {
3 | "schema": {
4 | "gp_name": {
5 | "label": "GP名称",
6 | "default": "",
7 | "type": "string",
8 | "placeholder": "请填写GP名称",
9 | "required": true,
10 | "disabled": false,
11 | "validators": [
12 | "required('GP名称必须填写')",
13 | {
14 | "determine": true,
15 | "validate(v)": "v <= 50",
16 | "message": "GP名称不能超过50个字符"
17 | }
18 | ]
19 | },
20 | "description": {
21 | "label": "简介",
22 | "default": "",
23 | "type": "string",
24 | "placeholder": "请填写GP简介",
25 | "required": true,
26 | "disabled": false,
27 | "validators": [
28 | "required('简介必须填写')",
29 | "maxLen(1000, '简介不能超过1000字')"
30 | ]
31 | },
32 | "website": {
33 | "label": "登录网址",
34 | "default": "",
35 | "type": "string",
36 | "placeholder": "请填写GP提供的登录网址",
37 | "required": false,
38 | "disabled": false,
39 | "validators": [
40 | "maxLen(50, '网址长度不能超过50个字符')"
41 | ]
42 | },
43 | "icon": {
44 | "label": "LOGO",
45 | "default": null,
46 | "type": "file",
47 | "required": false,
48 | "disabled": false,
49 | "drop(v)": "!v",
50 | "state()": "{ icon_url: null }"
51 | }
52 | }
53 | },
54 | "items": {
55 | "GPName": {
56 | "fields": ["gp_name"],
57 | "render($gp_name)!": [
58 | "FormItem",
59 | {},
60 | ["FormCell", {},
61 | ["Input", {
62 | "label": "{ $gp_name.label }",
63 | "value": "{ $gp_name.value }",
64 | "onChange(e)": "{ $gp_name.value = e.target.value }"
65 | }]
66 | ]
67 | ]
68 | }
69 | }
70 | }
--------------------------------------------------------------------------------
/designer/examples/spec/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
31 |
32 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/designer/examples/spec/index.js:
--------------------------------------------------------------------------------
1 | import { createFormastDesigner } from '../../src/index.js'
2 | import { InputConfig, TextareaConfig, SelectConfig, FormGroupConfig, FormItemConfig, RadioConfig, CheckboxConfig, InputNumberConfig, FormLoopConfig } from '../../src/config/components.jsx' // 内置的例子
3 | import { Popup } from '../../src/libs/popup.js'
4 | import { SchemaAttributes } from '../../src/config/model.js'
5 |
6 | const layout = {
7 | groups: [
8 | {
9 | id: 'layout',
10 | title: '布局素材',
11 | items: [
12 | FormGroupConfig,
13 | FormItemConfig,
14 | FormLoopConfig,
15 | ],
16 | },
17 | {
18 | id: 'atom',
19 | title: '原子素材',
20 | items: [
21 | InputConfig,
22 | InputNumberConfig,
23 | TextareaConfig,
24 | SelectConfig,
25 | RadioConfig,
26 | CheckboxConfig,
27 | ],
28 | },
29 | ],
30 | }
31 | const model = {
32 | schema: {
33 | attributes: SchemaAttributes,
34 | },
35 | }
36 |
37 | const cacheJson = sessionStorage.getItem('__JSON__')
38 | const json = cacheJson ? JSON.parse(cacheJson) : {}
39 | const editor = createFormastDesigner('#form-editor', {
40 | attachTopBar(el) {
41 | el.innerHTML = `
42 |
43 | Formast Designer
44 |
45 | `
46 | },
47 | json,
48 | layout,
49 | model,
50 | })
51 |
52 | editor.on('save', (json) => {
53 | console.log(json)
54 | const formJSON = JSON.stringify(json)
55 | sessionStorage.setItem('__JSON__', formJSON)
56 | Popup.toast('保存成功')
57 | })
58 |
59 | editor.on('reset', () => {
60 | editor.setJSON({})
61 | })
62 |
63 | // editor.mount('#form-editor')
64 | // editor.unmount()
65 |
66 | // 使用已经存在的备份,可以从服务端拉取后异步set,整个编辑区会重新刷新
67 | // editor.setJSON(json)
68 | // 之后必须自己调用refresh刷新界面
69 | // editor.refresh()
70 |
71 | // // 获取当前的json
72 | // editor.getJSON()
73 |
74 | // editor.on('download', (json) => {
75 | // // ...
76 | // })
77 |
--------------------------------------------------------------------------------
/designer/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "formast-designer",
3 | "version": "0.2.1",
4 | "description": "Formast(帆桅)表单可视化设计器",
5 | "main": "dist/index.js",
6 | "scripts": {
7 | "dev": "parcel examples/spec/index.html --out-dir .cache/.dist",
8 | "build:bundle": "NODE_ENV=production webpack",
9 | "build:module": "EXPOSE_MODULE=1 NODE_ENV=production webpack",
10 | "build": "npm run build:module && npm run build:bundle",
11 | "build:docs": "parcel build examples/spec/index.html --out-dir docs"
12 | },
13 | "author": "tisontang",
14 | "license": "ISC",
15 | "dependencies": {
16 | "@babel/runtime": "^7.12.18",
17 | "formast": "^0.9.1",
18 | "nautil": "^0.24.4",
19 | "react-dnd": "^11.1.3",
20 | "react-dnd-html5-backend": "^11.1.3",
21 | "react-icons": "^4.2.0",
22 | "scopex": "^2.1.0",
23 | "ts-fns": "^9.3.1",
24 | "tyshemo": "^10.8.0"
25 | },
26 | "devDependencies": {
27 | "@babel/core": "^7.12.17",
28 | "@babel/plugin-proposal-class-properties": "^7.12.13",
29 | "@babel/plugin-transform-runtime": "^7.12.17",
30 | "@babel/preset-env": "^7.12.17",
31 | "@babel/preset-react": "^7.12.13",
32 | "babel-loader": "^8.2.2",
33 | "css-loader": "^5.0.2",
34 | "exports-loader": "^2.0.0",
35 | "less": "^4.1.1",
36 | "less-loader": "^8.0.0",
37 | "parcel-bundler": "^1.12.4",
38 | "style-loader": "^2.0.0",
39 | "webpack": "^5.23.0",
40 | "webpack-cli": "^4.5.0",
41 | "webpack-node-externals": "^2.5.2"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/designer/src/app/app.jsx:
--------------------------------------------------------------------------------
1 | import { React, Component, Section, If, createRef } from 'nautil'
2 | import { Button } from '../components/button/button.jsx'
3 | import { classnames, globalModelScope } from '../utils'
4 | import { ModelDesigner } from './model-designer.jsx'
5 | import { ComponentsDesigner } from './components-designer.jsx'
6 | import { DndProvider } from 'react-dnd'
7 | import { HTML5Backend } from 'react-dnd-html5-backend'
8 | import { LayoutDesigner } from './layout-designer.jsx'
9 | import { decideby } from 'ts-fns'
10 |
11 | class App extends Component {
12 | state = {
13 | activeTopTab: 'layout',
14 | }
15 |
16 | topBar = createRef()
17 |
18 | onInit() {
19 | // 进来以后实例化,在render中好用
20 | const { json } = this.props
21 | const { model = {} } = json || {}
22 | globalModelScope.set(model)
23 | }
24 |
25 | handleModelJSONChange = (model) => {
26 | const { json = {}, onJSONChange } = this.props
27 | const { components, layout } = json
28 | const next = { model, components, layout }
29 | onJSONChange(next)
30 | }
31 |
32 | handleComponentsChange = (components) => {
33 | const { json = {}, onJSONChange } = this.props
34 | const { model, layout } = json
35 | const next = { model, components, layout }
36 | onJSONChange(next)
37 | }
38 |
39 | handleLayoutChange = (layout) => {
40 | const { json = {}, onJSONChange } = this.props
41 | const { components, model } = json
42 | const next = { model, components, layout }
43 | onJSONChange(next)
44 | }
45 |
46 | onMounted() {
47 | const { config, attachTopBar } = this.props
48 | const activeTopTab = decideby(() => {
49 | if (!config.disableLayout) {
50 | return 'layout'
51 | }
52 | else if (!config.disableComponents) {
53 | return 'components'
54 | }
55 | else {
56 | return 'model'
57 | }
58 | })
59 | this.setState({ activeTopTab })
60 |
61 | if (attachTopBar) {
62 | attachTopBar(this.topBar.current)
63 | }
64 | }
65 |
66 | render() {
67 | const { config = {}, onSave, onReset, onExport, json = {}, onImport, attachTopBar } = this.props
68 | const { activeTopTab } = this.state
69 | const setActiveTopTab = (activeTopTab) => this.setState({ activeTopTab })
70 | const { model } = json
71 | const disabled = [config.disableModel, config.disableComponents, config.disableLayout].filter(item => !!item).length > 1
72 | const hasNoBtns = [config.disableSave, config.disableReset, config.disableImport, config.disableExport].filter(item => !!item).length === 4
73 |
74 | return (
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 | {!config.disableModel ? setActiveTopTab('model')}>模型设计 : null}
84 | {!config.disableComponents ? setActiveTopTab('components')}>组件设计 : null}
85 | {!config.disableLayout ? setActiveTopTab('layout')}>表单设计 : null}
86 |
87 |
88 |
89 | {!config.disableSave ? : null}
90 | {!config.disableReset ? : null}
91 | {!config.disableImport ? : null}
92 | {!config.disableExport ? : null}
93 |
94 |
95 |
96 |
97 | {!config.disableModel && activeTopTab === 'model' ? : null}
98 | {!config.disableComponents && activeTopTab === 'components' ? : null}
99 | {!config.disableLayout && activeTopTab === 'layout' ? : null}
100 |
101 |
102 | )
103 | }
104 | }
105 |
106 | export default App
107 |
--------------------------------------------------------------------------------
/designer/src/app/components-designer.jsx:
--------------------------------------------------------------------------------
1 | import { React, Component, Section, Text, Each } from 'nautil'
2 | import { classnames } from '../utils/index.js'
3 | import { LayoutDesigner } from './layout-designer.jsx'
4 | import { Close } from '../components/close/close.jsx'
5 | import { Button } from '../components/button/button.jsx'
6 |
7 | import { Form, FormItem, Label, Input } from '../components/form/form.jsx'
8 | import { AutoModal } from '../components/modal/modal.jsx'
9 | import { Popup } from '../libs/popup.js'
10 | import { Confirm } from '../components/confirm/confirm.jsx'
11 |
12 | export class ComponentsDesigner extends Component {
13 | state = {
14 | selectedComponentName: '',
15 | newComponentName: '',
16 | }
17 |
18 | onMounted() {
19 | // 进入的时候,如果有组件,那么默认选中第一个组件
20 | const { json = {} } = this.attrs
21 | const keys = Object.keys(json)
22 | if (!keys.length) {
23 | return
24 | }
25 | const selectedComponentName = keys[0]
26 | this.setState({ selectedComponentName })
27 | }
28 |
29 | handleInputComponent = (name) => {
30 | const newComponentName = name ? name[0].toUpperCase() + name.substr(1) : name
31 | this.setState({ newComponentName })
32 | }
33 |
34 | handleSubmitComponent = () => {
35 | const { newComponentName: name } = this.state
36 | const { json = {}, onComponentsJSONChange } = this.props
37 | const { components = {} } = json
38 |
39 | if (name in components) {
40 | Popup.toast('组件已经存在,请使用其他组件名')
41 | return false
42 | }
43 |
44 | const componentsJSON = {
45 | ...components,
46 | [name]: {
47 | 'render!': [],
48 | },
49 | }
50 |
51 | onComponentsJSONChange(componentsJSON)
52 | this.setState({ selectedComponentName: name, newComponentName: '' })
53 | return true
54 | }
55 |
56 | handleRemoveComponent = (e, name) => {
57 | e.stopPropagation()
58 |
59 | const { json = {}, onComponentsJSONChange } = this.props
60 | const { components = {} } = json
61 |
62 | const next = { ...components }
63 | delete next[name]
64 |
65 | onComponentsJSONChange(next)
66 | return true
67 | }
68 |
69 | handleChangeComponent = (componentJSON) => {
70 | const { selectedComponentName } = this.state
71 | const { json = {}, onComponentsJSONChange } = this.props
72 | const { components = {} } = json
73 |
74 | const next = { ...components }
75 | next[selectedComponentName] = componentJSON
76 |
77 | onComponentsJSONChange(next)
78 | }
79 |
80 | Render(props) {
81 | const { layoutConfig, json = {} } = props
82 | const componentsJSON = json.components || {}
83 | const componentNames = Object.keys(componentsJSON)
84 |
85 | const { selectedComponentName } = this.state
86 | const selectedComponentJSON = selectedComponentName && componentsJSON[selectedComponentName]
87 |
88 | return (
89 |
90 |
91 |
92 | this.setState({ selectedComponentName: componentName })}
95 | >
96 | {componentName}
97 | }
99 | title="提示"
100 | content={`确认删除组件${componentName}吗?`}
101 | onSubmit={(e) => this.handleRemoveComponent(e, componentName)}
102 | />
103 |
104 | } />
105 | }
108 | onCancel={() => this.setState({ newComponentName: '' })}
109 | onClose={() => this.setState({ newComponentName: '' })}
110 | onSubmit={this.handleSubmitComponent}
111 | >
112 |
118 |
119 |
120 |
121 |
122 |
125 | } />
126 |
127 |
128 | )
129 | }
130 | }
131 | export default ComponentsDesigner
--------------------------------------------------------------------------------
/designer/src/app/methods-designer.jsx:
--------------------------------------------------------------------------------
1 | import { React, Component, Section, produce, Each } from 'nautil'
2 | import { Button } from '../components/button/button.jsx'
3 | import { Popup } from '../libs/popup.js'
4 | import { Modal } from '../components/modal/modal.jsx'
5 | import { Form, FormItem, Label, Input, Textarea } from '../components/form/form.jsx'
6 | import { classnames, parseKey } from '../utils'
7 | import { Confirm } from '../components/confirm/confirm.jsx'
8 |
9 | export class MethodsDesigner extends Component {
10 | static state = {
11 | isShow: false,
12 | form: {
13 | name: '',
14 | params: '',
15 | fn: '',
16 | },
17 | edit: '',
18 | }
19 |
20 | state = MethodsDesigner.state
21 |
22 | handleAddMethod = () => {
23 | this.setState({ isShow: true })
24 | }
25 |
26 | handleSubmitMehod = () => {
27 | const { form, edit } = this.state
28 | const { methodsJSON } = this.attrs
29 | const { onMethodsJSONChange } = this.props
30 |
31 | const { name, params, fn } = form
32 |
33 | if (!name) {
34 | return Popup.toast('请填写方法名')
35 | }
36 |
37 | if (!fn) {
38 | return Popup.toast('请填写函数体')
39 | }
40 |
41 | const method = `${name}(${params})`
42 | const next = produce(methodsJSON, json => {
43 | if (edit && method !== edit) {
44 | delete json[edit]
45 | }
46 | json[method] = fn
47 | })
48 |
49 | onMethodsJSONChange(next)
50 |
51 | this.setState(MethodsDesigner.state)
52 | }
53 |
54 | handleCancelMehod = () => {
55 | this.setState(MethodsDesigner.state)
56 | }
57 |
58 | handleEditMethod = (method) => {
59 | const { methodsJSON } = this.attrs
60 | const fn = methodsJSON[method]
61 | const [name, params] = parseKey(method)
62 | this.setState({
63 | form: {
64 | name,
65 | params: params.join(','),
66 | fn,
67 | },
68 | edit: method,
69 | isShow: true,
70 | })
71 | }
72 |
73 | handleRemoveMethod = (method) => {
74 | const { methodsJSON } = this.attrs
75 | const { onMethodsJSONChange } = this.props
76 |
77 | const next = produce(methodsJSON, json => {
78 | delete json[method]
79 | })
80 |
81 | onMethodsJSONChange(next)
82 | }
83 |
84 | render() {
85 | const { isShow, form } = this.state
86 | const { methodsJSON } = this.attrs
87 | return (
88 |
89 |
90 |
91 |
92 |
93 | this.handleRemoveMethod(method)}
96 | trigger={show => }
97 | />
98 |
99 | } />
100 |
101 |
104 |
105 |
106 |
120 |
121 |
122 | )
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/designer/src/app/state-designer.jsx:
--------------------------------------------------------------------------------
1 | import { React, Component, Section, Textarea } from 'nautil'
2 | import { Popup } from '../libs/popup.js'
3 | import { classnames } from '../utils'
4 |
5 | export class StateDesigner extends Component {
6 | state = {
7 | json: '',
8 | }
9 |
10 | onMounted() {
11 | const { stateJSON = {}, config = {} } = this.props
12 | const state = { ...config, ...stateJSON }
13 | const json = JSON.stringify(state, null, 4)
14 | this.setState({ json })
15 | }
16 |
17 | handleStateJSONChange = (e) => {
18 | const { onStateJSONChange } = this.props
19 | const value = e.target.value
20 | this.setState({ json: value })
21 |
22 | try {
23 | const state = JSON.parse(value)
24 | Popup.hide()
25 | onStateJSONChange(state)
26 | }
27 | catch (e) {
28 | Popup.toast('JSON格式不对:' + e)
29 | }
30 | }
31 |
32 | render() {
33 | const { json } = this.state
34 |
35 | return (
36 |
39 | )
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/designer/src/components/button/button.jsx:
--------------------------------------------------------------------------------
1 | import { Button as NButton } from 'nautil'
2 | import { classnames } from '../../utils'
3 |
4 | export const Button = NButton.extend(props => {
5 | return {
6 | stylesheet: classnames(
7 | 'button',
8 | props.primary ? 'button-primary' : props.secondary ? 'button-secondary' : null,
9 | props.large ? 'button--large' : null,
10 | ),
11 | deprecated: ['primary', 'secondary', 'large'],
12 | }
13 | })
14 |
--------------------------------------------------------------------------------
/designer/src/components/close/close.jsx:
--------------------------------------------------------------------------------
1 | import { React, Button, Component } from 'nautil'
2 | import { classnames } from '../../utils'
3 |
4 | export class Close extends Component {
5 | render() {
6 | return
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/designer/src/components/confirm/confirm.jsx:
--------------------------------------------------------------------------------
1 | import { React, Section, useState, useCallback } from 'nautil'
2 | import { Modal } from '../modal/modal.jsx'
3 | import { classnames } from '../../utils'
4 |
5 | export function Confirm(props) {
6 | const { title, content, onCancel, onSubmit, width, trigger } = props
7 | const [isShow, toggleShow] = useState(false)
8 |
9 | const handleCancel = useCallback((e) => {
10 | toggleShow(false)
11 | onCancel && onCancel(e)
12 | }, [])
13 |
14 | const handleSubmit = useCallback((e) => {
15 | toggleShow(false)
16 | onSubmit && onSubmit(e)
17 | }, [])
18 |
19 | const handleShow = useCallback(() => {
20 | toggleShow(true)
21 | }, [])
22 |
23 | return (
24 | <>
25 | {trigger(handleShow)}
26 |
27 |
28 |
29 | >
30 | )
31 | }
32 | export default Confirm
33 |
--------------------------------------------------------------------------------
/designer/src/components/designer/designer.jsx:
--------------------------------------------------------------------------------
1 | import { React, Component, Section, ifexist, Any, nonable, Int } from 'nautil'
2 | import { classnames } from '../../utils'
3 | import { DragDesigner, DropDesigner } from '../drag-drop/drag-drop-designer.jsx'
4 | import { LayoutConfigType } from '../../types/layout.type.js'
5 |
6 | export class Designer extends Component {
7 | static props = {
8 | elements: nonable(Array),
9 | config: LayoutConfigType,
10 | buttons: ifexist(Any),
11 | settings: ifexist(Any),
12 | onRemove: true,
13 | onSelect: true,
14 | onChange: true,
15 | expParser: ifexist(Function),
16 | max: nonable(Int),
17 | }
18 | static propsCheckAsync = true
19 |
20 | handleRemove = (monitor) => {
21 | const { onRemove } = this.props
22 | onRemove(monitor)
23 | }
24 | handleSelect = (selected) => {
25 | const { onSelect } = this.props
26 | onSelect(selected)
27 | }
28 | handleChange = (monitors) => {
29 | const { onChange } = this.props
30 | // 只需要顶层第一个
31 | const top = monitors.length ? monitors[0] : null
32 | onChange(top)
33 | }
34 |
35 | render() {
36 | const { elements, config, expParser, max, settings } = this.props
37 |
38 | return (
39 | <>
40 |
43 |
56 | {settings ? (
57 |
60 | ) : null}
61 | >
62 | )
63 | }
64 | }
65 | export default Designer
66 |
--------------------------------------------------------------------------------
/designer/src/components/drag-drop/drag-drop.jsx:
--------------------------------------------------------------------------------
1 | import { useDrag, useDrop } from 'react-dnd'
2 | import { React } from 'nautil'
3 | import { classnames } from '../../utils'
4 |
5 | const DND_TYPES = {
6 | BASIC: 'basic',
7 | }
8 |
9 | export function DragBox(props) {
10 | const { type = DND_TYPES.BASIC, children, source, canDrag, beginDrag, endDrag, className, render } = props
11 | const [, drag, preview] = useDrag({
12 | item: { type, source },
13 | canDrag(monitor) {
14 | const cursor = monitor.getClientOffset()
15 | return canDrag ? canDrag(source, cursor) : true
16 | },
17 | begin(monitor) {
18 | const cursor = monitor.getClientOffset()
19 | beginDrag && beginDrag(source, cursor)
20 | },
21 | end(_, monitor) {
22 | const cursor = monitor.getClientOffset()
23 | endDrag && endDrag(source, cursor)
24 | },
25 | })
26 |
27 | if (render) {
28 | return {render(drag)}
29 | }
30 |
31 | return {children}
32 | }
33 |
34 | export function DropBox(props) {
35 | const { type, className, children, canDrop, onHover, onDrop } = props
36 | const [{ isOver, canDrop: canDropCursor }, drop] = useDrop({
37 | accept: type ? [DND_TYPES.BASIC, type] : DND_TYPES.BASIC,
38 | canDrop(item, monitor) {
39 | const cursor = monitor.getClientOffset()
40 | return canDrop ? canDrop(item.source, cursor) : true
41 | },
42 | hover(item, monitor) {
43 | const cursor = monitor.getClientOffset() // 鼠标位置
44 | onHover && onHover(item.source, cursor)
45 | },
46 | drop(item, monitor) {
47 | const cursor = monitor.getClientOffset()
48 | onDrop(item.source, cursor)
49 | },
50 | collect(monitor) {
51 | return {
52 | isOver: !!monitor.isOver(),
53 | cursor: monitor.getClientOffset(),
54 | canDrop: monitor.canDrop(),
55 | }
56 | },
57 | })
58 |
59 | return {children}
60 | }
61 |
--------------------------------------------------------------------------------
/designer/src/components/drag-drop/store.js:
--------------------------------------------------------------------------------
1 | import { applyStore, Store } from 'nautil'
2 |
3 | export const store = new Store(null)
4 |
5 | const { useStore, connect } = applyStore(store)
6 | export { useStore, connect }
7 |
--------------------------------------------------------------------------------
/designer/src/components/form/associative-input.jsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tencent-cdc/formast/d354ba0ab11dfb027618aabbcccb153b74f8c84e/designer/src/components/form/associative-input.jsx
--------------------------------------------------------------------------------
/designer/src/components/form/form.jsx:
--------------------------------------------------------------------------------
1 | import {
2 | React,
3 | Input as NInput,
4 | Textarea as NTextarea,
5 | Select as NSelect,
6 | Text,
7 | Section,
8 | Form as NForm,
9 | useUniqueKeys,
10 | produce,
11 | } from 'nautil'
12 | import { Button } from '../button/button.jsx'
13 | import { classnames } from '../../utils'
14 |
15 | export const Input = NInput.extend(props => {
16 | return {
17 | stylesheet: [
18 | classnames('input', props.small ? 'input--small' : null),
19 | props.width ? { width: props.width, flexGrow: 'unset' } : null,
20 | ],
21 | deprecated: ['width', 'small'],
22 | }
23 | })
24 |
25 | export const Textarea = NTextarea.extend(props => {
26 | return {
27 | stylesheet: [
28 | classnames('textarea'),
29 | props.width ? { width: props.width, flexGrow: 'unset' } : null,
30 | props.minHeight ? { minHeight: props.minHeight } : null,
31 | ],
32 | deprecated: ['width', 'minHeight'],
33 | }
34 | })
35 |
36 | export const Select = NSelect.extend(props => {
37 | return {
38 | stylesheet: [
39 | classnames('select'),
40 | props.width ? { width: props.width, flexGrow: 'unset' } : null,
41 | ],
42 | deprecated: ['width'],
43 | }
44 | })
45 |
46 | export const Label = Text.extend({
47 | stylesheet: [classnames('label')],
48 | })
49 |
50 | export const Form = NForm.extend(props => ({
51 | stylesheet: [classnames('form', props.aside ? 'form--aside' : null)],
52 | deprecated: ['aside'],
53 | props: {
54 | ...props,
55 | onSubmit: props.onSubmit || (e => e.preventDefault()),
56 | },
57 | }))
58 |
59 | export const FormItem = Section.extend(props => ({
60 | stylesheet: [
61 | classnames(
62 | 'form-item',
63 | props.small ? 'form-item--small' : null,
64 | ),
65 | ],
66 | deprecated: ['small'],
67 | }))
68 |
69 | export const FormLoop = (props) => {
70 | const { items, render, onAdd, onDel, onChange, canAdd, canRemove } = props
71 | const handleAdd = (index) => {
72 | onAdd(index)
73 | }
74 | const handleDel = (index) => {
75 | onDel(index)
76 | }
77 | const keys = useUniqueKeys(items)
78 |
79 | return (
80 |
81 | {items.map((item, i) => {
82 | const handleChange = (item) => {
83 | const next = produce(items, items => {
84 | Object.assign(items[i], item)
85 | })
86 | onChange(next)
87 | }
88 | return (
89 |
90 |
91 | {render(i, item, handleChange)}
92 |
93 |
94 | {!canAdd || canAdd() ? : null}
95 | {!canRemove || canRemove() ? : null}
96 |
97 |
98 | )
99 | })}
100 | {!items.length && (!canAdd || canAdd()) ? : null}
101 |
102 | )
103 | }
104 |
105 | export function Switcher(props) {
106 | const { className, value, onChange } = props
107 | const handleChange = (e) => {
108 | onChange(e, !value)
109 | }
110 | return (
111 |
112 |
116 |
117 | )
118 | }
--------------------------------------------------------------------------------
/designer/src/components/icon/index.js:
--------------------------------------------------------------------------------
1 | export * from 'react-icons/ai'
2 | export * from 'react-icons/bs'
3 | export * from 'react-icons/di'
4 | export * from 'react-icons/fi'
5 | export * from 'react-icons/fc'
6 | export * from 'react-icons/go'
7 | export * from 'react-icons/hi'
8 | export * from 'react-icons/io5'
9 | export * from 'react-icons/cg'
--------------------------------------------------------------------------------
/designer/src/components/modal/modal.jsx:
--------------------------------------------------------------------------------
1 | import { React, useState, useCallback, Section } from 'nautil'
2 | import { isFunction } from 'ts-fns'
3 | import { Button } from '../button/button.jsx'
4 | import { classnames } from '../../utils'
5 | import { Close } from '../close/close.jsx'
6 |
7 | export const Modal = (props) => {
8 | const { isShow, onClose, title, children, onCancel, onSubmit, disableCancel, disableClose, keepAlive, width, className } = props
9 | return (
10 |
11 |
12 | {!disableClose ? : null}
13 | {title ? : null}
14 | {isShow || keepAlive ? typeof children === 'function' ? children() : children : null}
15 |
16 | {!disableCancel ? : null}
17 |
18 |
19 |
20 |
21 | )
22 | }
23 | export default Modal
24 |
25 |
26 | export function AutoModal(props) {
27 | const { content, children, onCancel, onSubmit, onClose, trigger, ...attrs } = props
28 | const [isShow, toggleShow] = useState(false)
29 |
30 | const handleCancel = useCallback(() => {
31 | toggleShow(false)
32 | onCancel && onCancel()
33 | }, [])
34 |
35 | const handleClose = useCallback(() => {
36 | toggleShow(false)
37 | onClose && onClose()
38 | }, [])
39 |
40 | const handleSubmit = useCallback(() => {
41 | const res = onSubmit && onSubmit()
42 | if (res || !onSubmit) {
43 | toggleShow(false)
44 | }
45 | }, [])
46 |
47 | const handleShow = useCallback(() => {
48 | toggleShow(true)
49 | }, [])
50 |
51 | return (
52 | <>
53 | {trigger(handleShow)}
54 |
55 | {content && isFunction(content) ? content() : content || children}
56 |
57 | >
58 | )
59 | }
60 |
--------------------------------------------------------------------------------
/designer/src/components/prompt/prompt.jsx:
--------------------------------------------------------------------------------
1 | import { React, useRef, useEffect, useState, If, Each, useCallback } from 'nautil'
2 | import { classnames } from '../../utils'
3 |
4 | export function Prompt(props) {
5 | const { options, type, children, onSelect } = props
6 | const ref = useRef()
7 | const focused = useRef()
8 | const [style, setStyle] = useState(null)
9 |
10 | useEffect(() => {
11 | const el = ref.current
12 |
13 | let timer = null
14 |
15 | const handleFocus = (e) => {
16 | const input = e.target
17 |
18 | if (input.nodeName.toLowerCase() !== type) {
19 | return
20 | }
21 |
22 | clearTimeout(timer)
23 | const useStyle = () => {
24 | const { left, bottom, width } = input.getBoundingClientRect()
25 | // 监控textarea尺寸变化
26 | timer = setTimeout(() => {
27 | setStyle({ left, top: bottom, width })
28 | useStyle()
29 | }, 16)
30 | }
31 | useStyle()
32 | focused.current = input
33 | }
34 | const handleBlur = (e) => {
35 | setStyle(null)
36 | clearTimeout(timer)
37 | focused.current = null
38 | }
39 |
40 | el.addEventListener('focus', handleFocus, true)
41 | el.addEventListener('blur', handleBlur, true)
42 |
43 | return () => {
44 | clearTimeout(timer)
45 | el.removeEventListener('focus', handleFocus)
46 | el.removeEventListener('blur', handleBlur)
47 | }
48 | }, [])
49 |
50 | const handleSelect = useCallback((e, item) => {
51 | onSelect(e, item, focused)
52 | })
53 |
54 | return (
55 |
56 | {children}
57 |
58 |
59 |
60 |
61 | handleSelect(e, item)}>{item.text}
62 | } />
63 |
64 | } />
65 |
66 | )
67 | }
--------------------------------------------------------------------------------
/designer/src/components/rich-prop-editor/rich-prop-editor.jsx:
--------------------------------------------------------------------------------
1 | import { React, produce, Text } from 'nautil'
2 | import { Label, Select, Input, Textarea, Switcher } from '../form/form.jsx'
3 | import { VALUE_TYPES } from '../../config/constants.js'
4 | import { BsFillQuestionCircleFill } from 'react-icons/bs'
5 | import { Popover } from '../../libs/popover.js'
6 |
7 | const attrTypes = [
8 | { value: VALUE_TYPES.STR, text: '纯文本' },
9 | { value: VALUE_TYPES.ENUM, text: '枚举' },
10 | { value: VALUE_TYPES.BOOL, text: '开关' },
11 | { value: VALUE_TYPES.EXP, text: '表达式' },
12 | { value: VALUE_TYPES.FN, text: '函数' },
13 | ]
14 |
15 | export function RichPropEditor(props) {
16 | const { label, data, onChange, types, options, description, placeholder = '' } = props
17 | const items = attrTypes.filter(item => types ? types.includes(item.value) : true)
18 |
19 | const handleChangeType = (e) => {
20 | onChange(produce(data, data => {
21 | const type = data.type = +e.target.value
22 |
23 | if ([VALUE_TYPES.STR, VALUE_TYPES.EXP, VALUE_TYPES.FN].includes(type)) {
24 | data.value = data.value + ''
25 | }
26 | else if (type === VALUE_TYPES.ENUM) {
27 | if (!(options && options.some(item => item.value === data.value))) {
28 | data.value = options ? options[0].value : data.defender ? data.defender(data.value) : 0
29 | }
30 | }
31 | else if (type === VALUE_TYPES.BOOL) {
32 | try {
33 | const value = !!JSON.parse(data.value + '')
34 | data.value = value
35 | }
36 | catch (e) {
37 | data.value = false
38 | }
39 | }
40 | }))
41 | }
42 |
43 | return (
44 | <>
45 |
49 |
50 | {data.type === VALUE_TYPES.STR ? onChange(produce(data, data => { data.value = e.target.value }))} disabled={data.disabled} placeholder={placeholder} /> : null}
51 | {data.type === VALUE_TYPES.ENUM ?