├── .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 | 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 | 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 | Formast 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 | [![](assets/formast-demo.png)](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 |
8 |
{title}
9 | {children} 10 |
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 |
33 |
34 |
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 | 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 | 113 | 114 | 115 | this.handleInputComponent(e.target.value)} placeholder="全英文,首字母大写" /> 116 | 117 | 118 | 119 |
120 |
121 | 122 |
123 | 124 |
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 |
{method}: {fn}
92 | 93 | this.handleRemoveMethod(method)} 96 | trigger={show => } 97 | /> 98 |
99 | } /> 100 | 101 |
102 | 103 |
104 | 105 | 106 |
107 | 108 | 109 | this.update(state => { state.form.name = e.target.value })} /> 110 | 111 | 112 | 113 | this.update(state => { state.form.params = e.target.value })} /> 114 | 115 | 116 | 117 |