├── .eslintignore ├── src ├── drag │ └── index.js ├── schema │ ├── base │ │ ├── button │ │ │ ├── index.js │ │ │ └── Schema.js │ │ ├── cascader │ │ │ ├── index.js │ │ │ └── Schema.js │ │ ├── checkbox │ │ │ ├── index.js │ │ │ └── Schema.js │ │ ├── grid │ │ │ ├── index.js │ │ │ └── Schema.js │ │ ├── input │ │ │ ├── index.js │ │ │ └── Schema.js │ │ ├── line │ │ │ ├── index.js │ │ │ └── Schema.js │ │ ├── radio │ │ │ ├── index.js │ │ │ └── Schema.js │ │ ├── rate │ │ │ ├── index.js │ │ │ └── Schema.js │ │ ├── select │ │ │ ├── index.js │ │ │ └── Schema.js │ │ ├── slider │ │ │ ├── index.js │ │ │ └── Schema.js │ │ ├── switch │ │ │ ├── index.js │ │ │ └── Schema.js │ │ ├── table │ │ │ ├── index.js │ │ │ └── Schema.js │ │ ├── text │ │ │ ├── index.js │ │ │ └── Schema.js │ │ ├── textarea │ │ │ ├── index.js │ │ │ └── Schema.js │ │ ├── upload │ │ │ ├── index.js │ │ │ └── Schema.js │ │ ├── autoComplete │ │ │ ├── index.js │ │ │ └── Schema.js │ │ ├── datePicker │ │ │ ├── index.js │ │ │ └── Schema.js │ │ ├── formButton │ │ │ ├── index.js │ │ │ └── Schema.js │ │ ├── inputNumber │ │ │ ├── index.js │ │ │ └── Schema.js │ │ ├── timePicker │ │ │ ├── index.js │ │ │ └── Schema.js │ │ └── index.js │ ├── index.js │ ├── RootSchema.js │ ├── FormSchema.js │ └── BaseSchema.js ├── render │ ├── index.js │ ├── VueRender.js │ └── ReactRender.js ├── style │ ├── index.js │ └── Background.js ├── rule │ ├── index.js │ ├── rules │ │ ├── index.js │ │ ├── URLRule.js │ │ ├── EmailRule.js │ │ ├── StringRule.js │ │ ├── JSONRule.js │ │ ├── NumberRule.js │ │ └── PatternRule.js │ └── Rule.js ├── constant │ ├── static.js │ ├── default-schema.js │ └── index.js ├── logic │ ├── Properties.js │ ├── index.js │ ├── Effect.js │ ├── EventLogic.js │ ├── Logic.js │ └── ValueLogic.js ├── hook │ ├── index.js │ ├── Hook.js │ ├── SyncHook.js │ └── SyncWaterfallHook.js ├── store │ ├── Dict.js │ ├── API.js │ ├── mutations │ │ ├── index.js │ │ ├── style.js │ │ ├── logic.js │ │ ├── model.js │ │ ├── rule.js │ │ ├── schema.js │ │ ├── store.js │ │ └── widget.js │ ├── TypeBuilder.js │ ├── types.js │ ├── StoreConf.js │ ├── Fetch.js │ └── index.js ├── script │ └── index.js ├── context │ └── index.js ├── main.js ├── event │ └── index.js ├── helper │ ├── index.js │ ├── util.js │ └── epUtil.js └── worker │ └── index.js ├── public └── imgs │ └── epage-qrcode.png ├── .babelrc ├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── index.html ├── .github └── ISSUE_TEMPLATE │ └── bug_report.md ├── script ├── webpack.base.js ├── webpack.dev.js ├── color.js ├── verify-commit-msg.js └── webpack.build.js ├── CONTRIBUTING.md ├── README.md ├── LICENSE ├── package.json └── CHANGELOG.md /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/*.js 2 | .vscode/ 3 | node_modules/ 4 | -------------------------------------------------------------------------------- /src/drag/index.js: -------------------------------------------------------------------------------- 1 | import VueDrag from 'vuedraggable' 2 | 3 | export { 4 | VueDrag 5 | } 6 | -------------------------------------------------------------------------------- /src/schema/base/button/index.js: -------------------------------------------------------------------------------- 1 | import Schema from './Schema' 2 | 3 | export default Schema 4 | -------------------------------------------------------------------------------- /src/schema/base/cascader/index.js: -------------------------------------------------------------------------------- 1 | import Schema from './Schema' 2 | 3 | export default Schema 4 | -------------------------------------------------------------------------------- /src/schema/base/checkbox/index.js: -------------------------------------------------------------------------------- 1 | import Schema from './Schema' 2 | 3 | export default Schema 4 | -------------------------------------------------------------------------------- /src/schema/base/grid/index.js: -------------------------------------------------------------------------------- 1 | import Schema from './Schema' 2 | 3 | export default Schema 4 | -------------------------------------------------------------------------------- /src/schema/base/input/index.js: -------------------------------------------------------------------------------- 1 | import Schema from './Schema' 2 | 3 | export default Schema 4 | -------------------------------------------------------------------------------- /src/schema/base/line/index.js: -------------------------------------------------------------------------------- 1 | import Schema from './Schema' 2 | 3 | export default Schema 4 | -------------------------------------------------------------------------------- /src/schema/base/radio/index.js: -------------------------------------------------------------------------------- 1 | import Schema from './Schema' 2 | 3 | export default Schema 4 | -------------------------------------------------------------------------------- /src/schema/base/rate/index.js: -------------------------------------------------------------------------------- 1 | import Schema from './Schema' 2 | 3 | export default Schema 4 | -------------------------------------------------------------------------------- /src/schema/base/select/index.js: -------------------------------------------------------------------------------- 1 | import Schema from './Schema' 2 | 3 | export default Schema 4 | -------------------------------------------------------------------------------- /src/schema/base/slider/index.js: -------------------------------------------------------------------------------- 1 | import Schema from './Schema' 2 | 3 | export default Schema 4 | -------------------------------------------------------------------------------- /src/schema/base/switch/index.js: -------------------------------------------------------------------------------- 1 | import Schema from './Schema' 2 | 3 | export default Schema 4 | -------------------------------------------------------------------------------- /src/schema/base/table/index.js: -------------------------------------------------------------------------------- 1 | import Schema from './Schema' 2 | 3 | export default Schema 4 | -------------------------------------------------------------------------------- /src/schema/base/text/index.js: -------------------------------------------------------------------------------- 1 | import Schema from './Schema' 2 | 3 | export default Schema 4 | -------------------------------------------------------------------------------- /src/schema/base/textarea/index.js: -------------------------------------------------------------------------------- 1 | import Schema from './Schema' 2 | 3 | export default Schema 4 | -------------------------------------------------------------------------------- /src/schema/base/upload/index.js: -------------------------------------------------------------------------------- 1 | import Schema from './Schema' 2 | 3 | export default Schema 4 | -------------------------------------------------------------------------------- /src/render/index.js: -------------------------------------------------------------------------------- 1 | import VueRender from './VueRender' 2 | 3 | export { 4 | VueRender 5 | } 6 | -------------------------------------------------------------------------------- /src/schema/base/autoComplete/index.js: -------------------------------------------------------------------------------- 1 | import Schema from './Schema' 2 | 3 | export default Schema 4 | -------------------------------------------------------------------------------- /src/schema/base/datePicker/index.js: -------------------------------------------------------------------------------- 1 | import Schema from './Schema' 2 | 3 | export default Schema 4 | -------------------------------------------------------------------------------- /src/schema/base/formButton/index.js: -------------------------------------------------------------------------------- 1 | import Schema from './Schema' 2 | 3 | export default Schema 4 | -------------------------------------------------------------------------------- /src/schema/base/inputNumber/index.js: -------------------------------------------------------------------------------- 1 | import Schema from './Schema' 2 | 3 | export default Schema 4 | -------------------------------------------------------------------------------- /src/schema/base/timePicker/index.js: -------------------------------------------------------------------------------- 1 | import Schema from './Schema' 2 | 3 | export default Schema 4 | -------------------------------------------------------------------------------- /src/style/index.js: -------------------------------------------------------------------------------- 1 | import Background from './Background' 2 | 3 | export { 4 | Background 5 | } 6 | -------------------------------------------------------------------------------- /public/imgs/epage-qrcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epage-team/epage-core/HEAD/public/imgs/epage-qrcode.png -------------------------------------------------------------------------------- /src/rule/index.js: -------------------------------------------------------------------------------- 1 | import Rule from './Rule' 2 | import * as rules from './rules' 3 | 4 | Rule.set(rules) 5 | 6 | export default Rule 7 | -------------------------------------------------------------------------------- /src/constant/static.js: -------------------------------------------------------------------------------- 1 | // 表单可展示的模式 2 | export const modes = () => ['edit', 'display'] 3 | export const defaultProps = () => ['hidden', 'disabled'] 4 | -------------------------------------------------------------------------------- /src/constant/default-schema.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 生成默认schema对象 3 | */ 4 | export default () => ({ 5 | key: '', 6 | type: 'object', 7 | widget: '' 8 | }) 9 | -------------------------------------------------------------------------------- /src/logic/Properties.js: -------------------------------------------------------------------------------- 1 | export default class Properties { 2 | constructor (key, value) { 3 | this.key = key 4 | this.value = value 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/hook/index.js: -------------------------------------------------------------------------------- 1 | import SyncWaterfallHook from './SyncWaterfallHook' 2 | import SyncHook from './SyncHook' 3 | 4 | export { 5 | SyncHook, 6 | SyncWaterfallHook 7 | } 8 | -------------------------------------------------------------------------------- /src/constant/index.js: -------------------------------------------------------------------------------- 1 | import { modes, defaultProps } from './static' 2 | 3 | export { default as defaultSchema } from './default-schema' 4 | export { 5 | modes, 6 | defaultProps 7 | } 8 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { "modules": "umd" }] 4 | ], 5 | "ignore": [ 6 | // "src/modules/worker/index.js" 7 | ], 8 | "comments": false 9 | } 10 | -------------------------------------------------------------------------------- /src/store/Dict.js: -------------------------------------------------------------------------------- 1 | import Fetch from './Fetch' 2 | export default class Dict extends Fetch { 3 | /* eslint no-useless-constructor: 0 */ 4 | constructor (props) { 5 | super(props) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /src/store/API.js: -------------------------------------------------------------------------------- 1 | import Fetch from './Fetch' 2 | 3 | export default class API extends Fetch { 4 | /* eslint no-useless-constructor: 0 */ 5 | constructor (props) { 6 | super(props) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["standard", "plugin:vue/recommended"], 3 | // add your custom rules here 4 | 'rules': { 5 | "vue/no-parsing-error": [0, { 6 | "x-invalid-end-tag": false 7 | }] 8 | } 9 | } -------------------------------------------------------------------------------- /src/hook/Hook.js: -------------------------------------------------------------------------------- 1 | // Hook基础类 2 | export default class Hook { 3 | constructor () { 4 | this.tasks = [] 5 | } 6 | 7 | call () {} 8 | tap (cb) { 9 | if (typeof cb !== 'function') return 10 | this.tasks.push(cb) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/schema/index.js: -------------------------------------------------------------------------------- 1 | import BaseSchema from './BaseSchema' 2 | import FormSchema from './FormSchema' 3 | import RootSchema from './RootSchema' 4 | import base from './base' 5 | 6 | export { 7 | BaseSchema, 8 | FormSchema, 9 | RootSchema, 10 | base 11 | } 12 | -------------------------------------------------------------------------------- /src/hook/SyncHook.js: -------------------------------------------------------------------------------- 1 | import Hook from './Hook' 2 | // 串行同步 3 | export default class SyncHook extends Hook { 4 | constructor () { 5 | super() 6 | this.tasks = [] 7 | } 8 | 9 | call (...args) { 10 | this.tasks.forEach(hook => hook(...args)) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *node_modules/ 3 | */**/node_modules/ 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | test/unit/coverage 8 | test/e2e/reports 9 | selenium-debug.log 10 | output/ 11 | web-ui.tar.gz 12 | .vscode 13 | *.iml 14 | .idea/ 15 | package-lock.json 16 | yarn.lock 17 | -------------------------------------------------------------------------------- /src/logic/index.js: -------------------------------------------------------------------------------- 1 | import Effect from './Effect' 2 | import EventLogic from './EventLogic' 3 | import ValueLogic from './ValueLogic' 4 | import Logic from './Logic' 5 | 6 | Logic.Effect = Effect 7 | Logic.EventLogic = EventLogic 8 | Logic.ValueLogic = ValueLogic 9 | 10 | export default Logic 11 | -------------------------------------------------------------------------------- /src/rule/rules/index.js: -------------------------------------------------------------------------------- 1 | export { default as StringRule } from './StringRule' 2 | export { default as NumberRule } from './NumberRule' 3 | export { default as EmailRule } from './EmailRule' 4 | export { default as URLRule } from './URLRule' 5 | export { default as PatternRule } from './PatternRule' 6 | export { default as JSONRule } from './JSONRule' 7 | -------------------------------------------------------------------------------- /src/script/index.js: -------------------------------------------------------------------------------- 1 | 2 | export default class Script { 3 | constructor (ctx) { 4 | this.ctx = ctx 5 | } 6 | 7 | exec (script) { 8 | try { 9 | /* eslint-disable no-new-func */ 10 | const fun = new Function('ctx', script) 11 | return fun(this.ctx) 12 | } catch (e) { 13 | console.log(e) 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | epage core test 7 | 8 | 9 | 10 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/logic/Effect.js: -------------------------------------------------------------------------------- 1 | import Properties from './Properties' 2 | 3 | export default class Effect { 4 | constructor () { 5 | // schema key for effected widgets 6 | this.key = '' 7 | // properties for effected effected widgets 8 | this.properties = [ 9 | new Properties('hidden', false), 10 | new Properties('disabled', false) 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/store/mutations/index.js: -------------------------------------------------------------------------------- 1 | import model from './model' 2 | import schema from './schema' 3 | import widget from './widget' 4 | import logic from './logic' 5 | import rule from './rule' 6 | import store from './store' 7 | import style from './style' 8 | 9 | export default { 10 | model, 11 | schema, 12 | widget, 13 | logic, 14 | rule, 15 | store, 16 | style 17 | } 18 | -------------------------------------------------------------------------------- /src/hook/SyncWaterfallHook.js: -------------------------------------------------------------------------------- 1 | import Hook from './Hook' 2 | // 串行同步 3 | export default class SyncWaterfallHook extends Hook { 4 | constructor () { 5 | super() 6 | this.tasks = [] 7 | } 8 | 9 | call (parm) { 10 | const [first, ...others] = this.tasks 11 | if (typeof first !== 'function') return parm 12 | return others.reduce((ret, task) => task(ret), first(parm)) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/schema/base/text/Schema.js: -------------------------------------------------------------------------------- 1 | import BaseSchema from '../../BaseSchema' 2 | 3 | export default class TextSchema extends BaseSchema { 4 | constructor (props) { 5 | super() 6 | this.option = { 7 | content: '' 8 | } 9 | this.create(props) 10 | } 11 | } 12 | 13 | // 静态配置,同类widget公有 14 | Object.assign(TextSchema, { 15 | title: '文本', 16 | widget: 'text', 17 | preview: '' 18 | }) 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 1. Go to '...' 15 | 2. Click on '....' 16 | 17 | **Expected behavior** 18 | 19 | **Device (please complete the following information):** 20 | - OS: [e.g. iOS8.1] 21 | - Browser [e.g. stock browser, safari] 22 | - Version [e.g. 22] 23 | -------------------------------------------------------------------------------- /script/webpack.base.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | const scirptPath = [ 4 | path.resolve(__dirname, '../src') 5 | ] 6 | 7 | module.exports = { 8 | module: { 9 | rules: [ 10 | { 11 | test: /\.js$/, 12 | loader: 'babel-loader', 13 | include: scirptPath 14 | } 15 | ] 16 | }, 17 | resolve: { 18 | extensions: ['.js', '.vue'], 19 | alias: { 20 | '@': path.resolve(__dirname, '../src') 21 | } 22 | }, 23 | stats: { children: false } 24 | } 25 | -------------------------------------------------------------------------------- /src/store/mutations/style.js: -------------------------------------------------------------------------------- 1 | import types from '../types' 2 | 3 | export default { 4 | // 更新style 5 | [types.$STYLE_UPDATE] ({ rootSchema, flatSchemas }, { key, style }) { 6 | let schema = null 7 | if (key) { // update widget 8 | if (key in flatSchemas) schema = flatSchemas[key] 9 | } else { // update root 10 | schema = rootSchema 11 | } 12 | if (!schema) return 13 | const originStyle = { ...schema.style } 14 | schema.style = Object.assign(originStyle, style) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/schema/base/slider/Schema.js: -------------------------------------------------------------------------------- 1 | import FormSchema from '../../FormSchema' 2 | 3 | export default class SliderSchema extends FormSchema { 4 | constructor (props) { 5 | super() 6 | this.label = '滑块' 7 | this.default = 0 8 | this.create(props) 9 | } 10 | } 11 | 12 | // 静态配置,同类widget公有 13 | Object.assign(SliderSchema, { 14 | title: '滑块', 15 | widget: 'slider', 16 | preview: '', 17 | type: 'number', 18 | validators: 'number', 19 | logic: { 20 | value: ['=', '!='], 21 | // event: ['change'] 22 | event: ['change'] 23 | } 24 | }) 25 | -------------------------------------------------------------------------------- /src/store/mutations/logic.js: -------------------------------------------------------------------------------- 1 | import types from '../types' 2 | import { 3 | isArray 4 | } from '../../helper' 5 | 6 | export default { 7 | // 逻辑模式添加一个逻辑规则 8 | [types.$LOGIC_ADD] ({ rootSchema }, { logic }) { 9 | if (isArray(rootSchema.logics)) { 10 | rootSchema.logics.push(logic) 11 | } else { 12 | rootSchema.logics = [logic] 13 | } 14 | }, 15 | 16 | // 逻辑模式更新一个逻辑规则 17 | [types.$LOGIC_UPDATE] ({ rootSchema }, { logic, index }) { 18 | rootSchema.logics.splice(index, 1, logic) 19 | }, 20 | 21 | // 逻辑模式删除一个逻辑规则 22 | [types.$LOGIC_DELETE] ({ rootSchema }, { index }) { 23 | rootSchema.logics.splice(index, 1) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/rule/rules/URLRule.js: -------------------------------------------------------------------------------- 1 | 2 | export default class URLRule { 3 | static get type () { 4 | return 'url' 5 | } 6 | 7 | static get name () { 8 | return '网址' 9 | } 10 | 11 | constructor (rule = {}) { 12 | const defaultRule = { 13 | type: 'url', 14 | message: '非法URL' 15 | } 16 | this.origin = Object.assign({}, defaultRule, rule) 17 | this.rule = { 18 | type: 'url', 19 | trigger: 'blur', 20 | message: '' 21 | } 22 | this.update(this.origin) 23 | } 24 | 25 | update (rule) { 26 | if (rule) { 27 | this.rule.message = rule.message 28 | Object.assign(this.origin, rule) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/rule/rules/EmailRule.js: -------------------------------------------------------------------------------- 1 | 2 | export default class EmailRule { 3 | static get type () { 4 | return 'email' 5 | } 6 | 7 | static get name () { 8 | return '邮箱' 9 | } 10 | 11 | constructor (rule = {}) { 12 | const defaultRule = { 13 | type: 'email', 14 | message: '邮箱格式不正确' 15 | } 16 | this.origin = Object.assign({}, defaultRule, rule) 17 | this.rule = { 18 | type: 'email', 19 | trigger: 'blur', 20 | message: '' 21 | } 22 | this.update(this.origin) 23 | } 24 | 25 | update (rule) { 26 | if (rule) { 27 | this.rule.message = rule.message 28 | Object.assign(this.origin, rule) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/logic/EventLogic.js: -------------------------------------------------------------------------------- 1 | import Effect from './Effect' 2 | 3 | export default class EventLogic { 4 | constructor () { 5 | this.type = 'event' 6 | this.title = '事件逻辑' 7 | this.map = { 8 | click: { key: 'click', value: '点击' }, 9 | focus: { key: 'focus', value: '聚焦' }, 10 | blur: { key: 'blur', value: '失焦' }, 11 | change: { key: 'change', value: '改变' } 12 | } 13 | } 14 | 15 | get () { 16 | return { 17 | type: 'event', 18 | // schema.key 19 | key: '', 20 | // event type: click | change or others 21 | action: '', 22 | trigger: 'prop', // prop | script 23 | script: '', 24 | effects: [new Effect()] 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /script/webpack.dev.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const merge = require('webpack-merge') 3 | const HtmlWebpackPlugin = require('html-webpack-plugin') 4 | const webpackBaseConfig = require('./webpack.base.js') 5 | 6 | const webpackConfig = merge(webpackBaseConfig, { 7 | mode: 'development', 8 | entry: { 9 | vendor: './examples/index.js' 10 | }, 11 | devtool: 'source-map', 12 | output: { 13 | filename: '[name].js' 14 | }, 15 | plugins: [ 16 | new webpack.HotModuleReplacementPlugin(), 17 | new HtmlWebpackPlugin({ 18 | filename: 'index.html', 19 | template: './examples/index.html', 20 | inject: true 21 | }) 22 | ] 23 | }) 24 | 25 | module.exports = webpackConfig 26 | -------------------------------------------------------------------------------- /src/schema/base/textarea/Schema.js: -------------------------------------------------------------------------------- 1 | import FormSchema from '../../FormSchema' 2 | 3 | export default class TextareaSchema extends FormSchema { 4 | constructor (props) { 5 | super() 6 | this.label = '多行文本' 7 | this.placeholder = '请输入' 8 | this.default = '' 9 | this.option = { 10 | rows: 3 11 | } 12 | this.create(props) 13 | } 14 | } 15 | 16 | // 静态配置,同类widget公有 17 | Object.assign(TextareaSchema, { 18 | title: '多行文本', 19 | widget: 'textarea', 20 | preview: '', 21 | type: 'string', 22 | validators: ['string', 'email', 'url', 'pattern'], 23 | logic: { 24 | value: ['=', '!=', '<>', '><'], 25 | // event: ['focus', 'blur', 'change'] 26 | event: ['change'] 27 | } 28 | }) 29 | -------------------------------------------------------------------------------- /src/schema/base/line/Schema.js: -------------------------------------------------------------------------------- 1 | import BaseSchema from '../../BaseSchema' 2 | 3 | export default class LineSchema extends BaseSchema { 4 | constructor (props) { 5 | super() 6 | this.option = { 7 | direction: 'horizontal', // vertical 8 | // hposition => top, middle, bottom 9 | hposition: 'middle', 10 | // vposition: left, center, right 11 | vposition: 'center', 12 | length: 100, 13 | thickness: 1, 14 | type: 'dashed', // solid | dashed | dotted 15 | color: '#aaa' 16 | } 17 | this.create(props) 18 | } 19 | } 20 | 21 | // 静态配置,同类widget公有 22 | Object.assign(LineSchema, { 23 | title: '线条', 24 | widget: 'line', 25 | preview: '', 26 | logic: { 27 | value: null, 28 | event: null 29 | } 30 | }) 31 | -------------------------------------------------------------------------------- /src/schema/base/input/Schema.js: -------------------------------------------------------------------------------- 1 | import FormSchema from '../../FormSchema' 2 | 3 | export default class InputSchema extends FormSchema { 4 | constructor (props) { 5 | super() 6 | this.label = '输入框' 7 | this.placeholder = '请输入' 8 | this.default = '' 9 | this.option = { 10 | password: false, 11 | prefix: '', // 前缀字符 12 | suffix: '' // 后缀字符 13 | } 14 | this.create(props) 15 | } 16 | } 17 | 18 | // 静态配置,同类widget公有 19 | Object.assign(InputSchema, { 20 | title: '单行文本', 21 | widget: 'input', 22 | preview: '', 23 | type: 'string', 24 | validators: ['string', 'email', 'url', 'pattern'], 25 | logic: { 26 | value: ['=', '!=', '<>', '><'], 27 | // event: ['focus', 'blur', 'change'] 28 | event: ['change'] 29 | } 30 | }) 31 | -------------------------------------------------------------------------------- /src/schema/base/switch/Schema.js: -------------------------------------------------------------------------------- 1 | import FormSchema from '../../FormSchema' 2 | 3 | export default class SwitchSchema extends FormSchema { 4 | constructor (props) { 5 | super() 6 | this.label = '开关' 7 | this.default = false 8 | this.rules = [{ 9 | type: 'boolean', 10 | message: '必填', 11 | trigger: 'change', 12 | required: false 13 | }] 14 | this.option = { 15 | open: '是', 16 | close: '否' 17 | } 18 | this.create(props) 19 | } 20 | } 21 | 22 | // 静态配置,同类widget公有 23 | Object.assign(SwitchSchema, { 24 | title: '开关', 25 | widget: 'switch', 26 | preview: '', 27 | type: 'boolean', 28 | validators: [], 29 | logic: { 30 | value: ['=', '!='], 31 | // event: ['change'] 32 | event: ['change'] 33 | } 34 | }) 35 | -------------------------------------------------------------------------------- /src/schema/base/formButton/Schema.js: -------------------------------------------------------------------------------- 1 | import BaseSchema from '../../BaseSchema' 2 | 3 | export default class ButtonSchema extends BaseSchema { 4 | constructor (props) { 5 | super() 6 | this.option = { 7 | // 动作类型:submit、goback、reset 8 | actionType: 'submit', 9 | text: '提交', 10 | // 按钮类型:primary、ghost、dashed、text、default 11 | type: 'primary', 12 | // 是否通栏 13 | long: false, 14 | // 形状,可选 'square': 方角;'circle': 圆角 15 | shape: 'square', 16 | url: '', 17 | method: 'POST' 18 | } 19 | this.create(props) 20 | } 21 | } 22 | 23 | // 静态配置,同类widget公有 24 | Object.assign(ButtonSchema, { 25 | title: '表单按钮', 26 | widget: 'formButton', 27 | preview: '', 28 | logic: { 29 | value: null, 30 | event: [] 31 | } 32 | }) 33 | -------------------------------------------------------------------------------- /src/schema/base/button/Schema.js: -------------------------------------------------------------------------------- 1 | import BaseSchema from '../../BaseSchema' 2 | 3 | export default class ButtonSchema extends BaseSchema { 4 | constructor (props) { 5 | super() 6 | this.disabled = false 7 | this.option = { 8 | text: '提交', 9 | // 按钮类型:primary、dashed、text、default 10 | type: 'primary', 11 | // 按钮图标 12 | icon: '', 13 | // 是否通栏 14 | long: false, 15 | // 是否幽灵按钮 16 | ghost: false, 17 | // 形状,可选 'square': 方角;'circle': 圆角 18 | shape: 'square', 19 | script: '' 20 | } 21 | this.create(props) 22 | } 23 | } 24 | 25 | // 静态配置,同类widget公有 26 | Object.assign(ButtonSchema, { 27 | title: '按钮', 28 | widget: 'button', 29 | preview: '', 30 | logic: { 31 | value: null, 32 | event: ['click'] 33 | } 34 | }) 35 | -------------------------------------------------------------------------------- /src/context/index.js: -------------------------------------------------------------------------------- 1 | import { isPlainObject } from '../helper' 2 | 3 | export default class Context { 4 | constructor (props = {}) { 5 | this.store = props.store 6 | this.state = Object.assign({}, props.state) 7 | this.$el = props.$el 8 | this.$render = props.$render 9 | this.instance = props.instance 10 | } 11 | 12 | setState (state) { 13 | return new Promise((resolve, reject) => { 14 | if (isPlainObject(state)) { 15 | const newState = {} 16 | for (const i in state) { 17 | if (i in this.state) { 18 | newState[i] = state[i] 19 | } 20 | } 21 | Object.assign(this.instance, newState) 22 | resolve() 23 | } else { 24 | reject(new Error('state should be plain object!')) 25 | } 26 | }) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/schema/base/rate/Schema.js: -------------------------------------------------------------------------------- 1 | import FormSchema from '../../FormSchema' 2 | 3 | export default class RateSchema extends FormSchema { 4 | constructor (props) { 5 | super() 6 | this.label = '评分' 7 | this.default = 0 8 | this.rules = [{ 9 | required: false, 10 | message: '必填', 11 | min: 0, 12 | type: 'number', 13 | trigger: 'change' 14 | }] 15 | this.option = { 16 | count: 5, 17 | allowHalf: false, 18 | showText: false 19 | } 20 | this.create(props) 21 | } 22 | } 23 | 24 | // 静态配置,同类widget公有 25 | Object.assign(RateSchema, { 26 | title: '评分', 27 | widget: 'rate', 28 | preview: '', 29 | type: 'number', 30 | validators: [], 31 | logic: { 32 | value: ['=', '!='], 33 | // event: ['change'] 34 | event: ['change'] 35 | } 36 | }) 37 | -------------------------------------------------------------------------------- /src/schema/base/datePicker/Schema.js: -------------------------------------------------------------------------------- 1 | import FormSchema from '../../FormSchema' 2 | 3 | export default class DatePickerSchema extends FormSchema { 4 | constructor (props) { 5 | super() 6 | this.label = '日期' 7 | this.placeholder = '请选择' 8 | this.default = '' 9 | this.rules = [{ 10 | required: false, 11 | message: '必填', 12 | trigger: 'change' 13 | }] 14 | this.option = { 15 | range: false, 16 | format: 'yyyy-MM-dd' 17 | } 18 | this.create(props) 19 | } 20 | } 21 | 22 | // 静态配置,同类widget公有 23 | Object.assign(DatePickerSchema, { 24 | title: '日期选择', 25 | widget: 'datePicker', 26 | preview: '', 27 | type: ['string', 'array'], 28 | validators: [], 29 | logic: { 30 | value: ['=', '!='], 31 | // event: ['focus', 'blur', 'change'] 32 | event: ['change'] 33 | } 34 | }) 35 | -------------------------------------------------------------------------------- /src/schema/base/timePicker/Schema.js: -------------------------------------------------------------------------------- 1 | import FormSchema from '../../FormSchema' 2 | 3 | export default class TimePickerSchema extends FormSchema { 4 | constructor (props) { 5 | super() 6 | this.label = '时间' 7 | this.placeholder = '请选择' 8 | this.default = '' 9 | this.rules = [{ 10 | required: false, 11 | message: '必填', 12 | trigger: 'change' 13 | }] 14 | this.option = { 15 | range: false, 16 | format: 'HH:mm:ss' 17 | } 18 | this.create(props) 19 | } 20 | } 21 | 22 | // 静态配置,同类widget公有 23 | Object.assign(TimePickerSchema, { 24 | title: '时间选择', 25 | widget: 'timePicker', 26 | preview: '', 27 | type: ['string', 'array'], 28 | validators: [], 29 | logic: { 30 | value: ['=', '!='], 31 | // event: ['focus', 'blur', 'change'] 32 | event: ['change'] 33 | } 34 | }) 35 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # 如何贡献代码 2 | 3 | 欢迎一起共建 epage-core 4 | 5 | ## Branch 说明 6 | 7 | ``` 8 | publish 9 | ↑ 10 | dev --> master 11 | ↑ 12 | PR 13 | ``` 14 | 15 | 主要有两个分支:`dev` 及 `master` 16 | 17 | - `dev` 分支:开发及发版均在此分支,提PR也提交到这里 18 | - `master` 分支:稳定分支,在`dev`开发并发布后合并到`master`,然后打上tag 19 | 20 | **说明:** 21 | 22 | 有较大新功能会以 `feat/xxx` 创建分支 23 | 24 | ## Commit 格式 25 | 26 | ``` 27 | [{keyword}]({scope}): {description} 28 | ``` 29 | 30 | - `{keyword}` 31 | - `revert` 撤销某些提交 32 | - `feat` 新增功能 33 | - `fix` 更新或者修复 bug 34 | - `docs` 文档修改更新 35 | - `style` 代码格式(不影响代码运行的变动) 36 | - `refactor` 重构(即不是新增功能,也不是修改bug的代码变动) 37 | - `perf` 性能优化 38 | - `test` 测试新增或更新 39 | - `chore` 构建过程或辅助工具的变动 40 | - `{scope}`: 可选,用于说明 commit 影响的范围 41 | - `{description}` 42 | - 尽可能详细的描述就好,1-50个字符 43 | 44 | for example: 45 | 46 | - [feat]: 新增富文本widget 47 | - [fix]: 修复下拉框展开后点击空白不消失 48 | 49 | -------------------------------------------------------------------------------- /src/schema/base/table/Schema.js: -------------------------------------------------------------------------------- 1 | import BaseSchema from '../../BaseSchema' 2 | 3 | export default class TableSchema extends BaseSchema { 4 | constructor (props) { 5 | super() 6 | this.label = '表格' 7 | this.option = { 8 | type: 'static', 9 | columns: [{ 10 | type: 'index', 11 | title: 'No', 12 | key: '', 13 | align: 'left' 14 | }], 15 | page: { 16 | total: 0, 17 | current: 1, 18 | size: 10, 19 | showTotal: true, 20 | position: 'right' 21 | }, 22 | data: [], 23 | dynamicData: [], 24 | noDataText: '暂无内容' 25 | } 26 | this.create(props) 27 | } 28 | } 29 | 30 | // 静态配置,同类widget公有 31 | Object.assign(TableSchema, { 32 | title: '表格', 33 | widget: 'table', 34 | preview: '', 35 | logic: { 36 | // value: [], 37 | // event: [] 38 | } 39 | }) 40 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Worker from './worker' 2 | import Rule from './rule' 3 | import Event from './event' 4 | import Store from './store' 5 | import Logic from './logic' 6 | import Context from './context' 7 | import Script from './script' 8 | import TypeBuilder from './store/TypeBuilder' 9 | import Dict from './store/Dict' 10 | import API from './store/API' 11 | import * as helper from './helper' 12 | import * as constant from './constant' 13 | import * as schema from './schema' 14 | import * as style from './style' 15 | import * as hook from './hook' 16 | import * as render from './render' 17 | import * as drag from './drag' 18 | 19 | export { 20 | Worker, 21 | Rule, 22 | Event, 23 | Store, 24 | Logic, 25 | Context, 26 | Script, 27 | TypeBuilder, 28 | Dict, 29 | API, 30 | helper, 31 | constant, 32 | schema, 33 | style, 34 | hook, 35 | render, 36 | drag 37 | } 38 | -------------------------------------------------------------------------------- /src/schema/base/inputNumber/Schema.js: -------------------------------------------------------------------------------- 1 | import FormSchema from '../../FormSchema' 2 | 3 | export default class InputNumberSchema extends FormSchema { 4 | constructor (props) { 5 | super() 6 | this.label = '数字' 7 | this.placeholder = '请输入' 8 | this.default = 0 9 | this.rules = [{ 10 | required: false, 11 | message: '必填', 12 | trigger: 'change', 13 | type: 'number' 14 | }] 15 | this.option = { 16 | min: 0, 17 | max: 100, 18 | step: 1, 19 | precision: 1 20 | } 21 | this.create(props) 22 | } 23 | } 24 | 25 | // 静态配置,同类widget公有 26 | Object.assign(InputNumberSchema, { 27 | title: '数字框', 28 | widget: 'inputNumber', 29 | preview: '', 30 | type: 'number', 31 | validators: [], 32 | logic: { 33 | value: ['=', '>', '<', '!='], 34 | // event: ['focus', 'blur', 'change'] 35 | event: ['change'] 36 | } 37 | }) 38 | -------------------------------------------------------------------------------- /src/schema/RootSchema.js: -------------------------------------------------------------------------------- 1 | import Schema from './base/grid/Schema' 2 | 3 | /** 4 | * root schema 5 | */ 6 | export default class RootSchema extends Schema { 7 | constructor (props) { 8 | super() 9 | this.title = '' 10 | this.description = '' 11 | this.size = 'default' 12 | this.container = true 13 | this.children = [{ 14 | span: 24, 15 | list: [] 16 | }] 17 | this.logics = [] 18 | // global setting for label 19 | this.label = { 20 | width: 80, 21 | position: 'right', 22 | colon: false 23 | } 24 | this.store = { 25 | dicts: [] 26 | } 27 | this.style = { 28 | 'margin-right': 'auto', 29 | 'margin-left': 'auto', 30 | background: [], 31 | container: { 32 | 'background-color': '', 33 | background: [] 34 | } 35 | } 36 | 37 | this.create(props) 38 | this.createChildren(props) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/style/Background.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 解析背景样式 3 | */ 4 | export default class Background { 5 | constructor (background) { 6 | const { 7 | position, 8 | repeat, 9 | size, 10 | image, 11 | attachment 12 | } = background || {} 13 | 14 | this.position = position || 'top center' 15 | this.repeat = repeat || 'no-repeat' // 'no-repeat' | 'repeat-x' | 'repeat-y' 16 | this.size = size || 'contain' // 'cover' | 'contain' 17 | // this.color = color || '' // 'cover' | '' 18 | this.image = image || '' // 'cover' | '' 19 | this.attachment = attachment || 'scroll' // 'scroll' | 'fixed' 20 | } 21 | 22 | toString () { 23 | const { 24 | position, 25 | repeat, 26 | size, 27 | image, 28 | attachment 29 | } = this 30 | const img = image ? ` ${repeat} ${position} /${size} ${attachment} url(${image})` : '' 31 | 32 | return `${img}` 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/schema/base/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | autoComplete: require('./autoComplete').default, 3 | button: require('./button').default, 4 | cascader: require('./cascader').default, 5 | checkbox: require('./checkbox').default, 6 | datePicker: require('./datePicker').default, 7 | // formButton: require('./formButton').default, 8 | grid: require('./grid').default, 9 | input: require('./input').default, 10 | inputNumber: require('./inputNumber').default, 11 | radio: require('./radio').default, 12 | rate: require('./rate').default, 13 | select: require('./select').default, 14 | slider: require('./slider').default, 15 | table: require('./table').default, 16 | switch: require('./switch').default, 17 | textarea: require('./textarea').default, 18 | text: require('./text').default, 19 | timePicker: require('./timePicker').default, 20 | upload: require('./upload').default, 21 | line: require('./line').default 22 | } 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Epage Core 2 | 3 | [Epage](https://github.com/didi/epage) 可视化配置工具核心依赖。 4 | 5 | ## 文档 6 | 7 | 官网:[http://epage.didichuxing.com](http://epage.didichuxing.com) 8 | 9 | - [开发文档](http://epage.didichuxing.com/developer/) 10 | 11 | 12 | ## 演示地址 13 | 14 | - **[Demo](http://epage.didichuxing.com/examples/epage.html)** 15 | 16 | ## 安装 17 | 18 | ```sh 19 | npm install epage-core -S 20 | # 或者 yarn add epage 21 | ``` 22 | 23 | ## 仓库更新说明 24 | 25 | 本仓库为`Epage渲染器`及`Epage设计器`核心依赖,更新日志查看[CHANGLOG](./CHANGELOG.md)。 26 | 27 | 更多`Epage渲染器`及相关工具参见:[https://github.com/epage-team](https://github.com/epage-team)。 28 | 29 | ## 设计器及渲染器示例 30 | 31 | ```js 32 | import { 33 | Event, 34 | Worker, 35 | Logic, 36 | Rule, 37 | Store, 38 | schema, 39 | helper, 40 | constant 41 | } from 'epage-core' 42 | 43 | ``` 44 | 45 | ## 交流群 46 | 47 | QQ群 48 | 49 | 50 | 51 | ## License 52 | 53 | [MIT](http://opensource.org/licenses/MIT) 54 | -------------------------------------------------------------------------------- /src/rule/rules/StringRule.js: -------------------------------------------------------------------------------- 1 | export default class StringRule { 2 | static get type () { 3 | return 'string' 4 | } 5 | 6 | static get name () { 7 | return '字符串' 8 | } 9 | 10 | constructor (rule = {}) { 11 | const defaultRule = { 12 | type: 'string', 13 | message: '字符串不符合规则' 14 | } 15 | this.origin = Object.assign({}, defaultRule, rule) 16 | this.rule = { 17 | type: 'string', 18 | trigger: 'blur', 19 | message: '' 20 | } 21 | this.update(this.origin) 22 | } 23 | 24 | update (rule) { 25 | if (rule) { 26 | const { min, max } = rule 27 | const intMin = parseInt(min) 28 | const intMax = parseInt(max) 29 | 30 | this.rule.message = rule.message 31 | if (isNaN(intMin)) { 32 | delete this.rule.min 33 | } else { 34 | this.rule.min = intMin 35 | } 36 | if (isNaN(intMax)) { 37 | delete this.rule.max 38 | } else { 39 | this.rule.max = intMax 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/rule/rules/JSONRule.js: -------------------------------------------------------------------------------- 1 | 2 | export default class JSONRule { 3 | static get type () { 4 | return 'json' 5 | } 6 | 7 | static get name () { 8 | return 'JSON' 9 | } 10 | 11 | constructor (rule = {}) { 12 | const defaultRule = { 13 | type: 'string', 14 | message: '非法的json格式!' 15 | } 16 | this.origin = Object.assign({}, defaultRule, rule) 17 | this.rule = { 18 | type: 'string', 19 | trigger: 'blur', 20 | validator: function (rule, value, callback) { 21 | if (!value || !(value + '').trim()) { 22 | return callback() 23 | } 24 | try { 25 | JSON.parse(value) 26 | callback() 27 | } catch (e) { 28 | return callback(new Error(rule.message)) 29 | } 30 | }, 31 | message: '非法的json格式!' 32 | } 33 | this.update(this.origin) 34 | } 35 | 36 | update (rule) { 37 | if (rule) { 38 | this.rule.message = rule.message 39 | Object.assign(this.origin, rule) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/schema/base/autoComplete/Schema.js: -------------------------------------------------------------------------------- 1 | import FormSchema from '../../FormSchema' 2 | import { getRuleValidator } from '../../../helper' 3 | 4 | export default class AutoCompleteSchema extends FormSchema { 5 | constructor (props) { 6 | super() 7 | this.label = '自动完成' 8 | this.placeholder = '请输入' 9 | this.default = '' 10 | this.option = { 11 | type: 'static', 12 | url: '', 13 | adapter: 'return data', 14 | data: ['A'], 15 | dynamicData: [], 16 | clearable: true 17 | } 18 | this.create(props) 19 | const rule = { 20 | trigger: 'change', 21 | validator: getRuleValidator(this.rules[0], this.type) 22 | } 23 | this.updateRequiredRule(rule, new.target) 24 | } 25 | } 26 | 27 | // 静态配置,同类widget公有 28 | Object.assign(AutoCompleteSchema, { 29 | title: '自动完成', 30 | widget: 'autoComplete', 31 | preview: '', 32 | type: 'string', 33 | validators: [], 34 | logic: { 35 | value: ['=', '!='], 36 | // event: ['focus', 'blur', 'change'] 37 | event: [] 38 | } 39 | }) 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Shoucheng (Chengzi) Jia 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /script/color.js: -------------------------------------------------------------------------------- 1 | 2 | const styles = { 3 | bold: ['\x1B[1m', '\x1B[22m'], 4 | italic: ['\x1B[3m', '\x1B[23m'], 5 | underline: ['\x1B[4m', '\x1B[24m'], 6 | inverse: ['\x1B[7m', '\x1B[27m'], 7 | strikethrough: ['\x1B[9m', '\x1B[29m'], 8 | white: ['\x1B[37m', '\x1B[39m'], 9 | grey: ['\x1B[90m', '\x1B[39m'], 10 | black: ['\x1B[30m', '\x1B[39m'], 11 | blue: ['\x1B[34m', '\x1B[39m'], 12 | cyan: ['\x1B[36m', '\x1B[39m'], 13 | green: ['\x1B[32m', '\x1B[39m'], 14 | magenta: ['\x1B[35m', '\x1B[39m'], 15 | red: ['\x1B[31m', '\x1B[39m'], 16 | yellow: ['\x1B[33m', '\x1B[39m'], 17 | whiteBG: ['\x1B[47m', '\x1B[49m'], 18 | greyBG: ['\x1B[49;5;8m', '\x1B[49m'], 19 | blackBG: ['\x1B[40m', '\x1B[49m'], 20 | blueBG: ['\x1B[44m', '\x1B[49m'], 21 | cyanBG: ['\x1B[46m', '\x1B[49m'], 22 | greenBG: ['\x1B[42m', '\x1B[49m'], 23 | magentaBG: ['\x1B[45m', '\x1B[49m'], 24 | redBG: ['\x1B[41m', '\x1B[49m'], 25 | yellowBG: ['\x1B[43m', '\x1B[49m'] 26 | } 27 | const color = {} 28 | 29 | Object.keys(styles).forEach(function (key) { 30 | color[key] = function (msg) { 31 | return styles[key].join(msg) 32 | } 33 | }) 34 | 35 | module.exports = color 36 | -------------------------------------------------------------------------------- /script/verify-commit-msg.js: -------------------------------------------------------------------------------- 1 | const msgPath = process.env.HUSKY_GIT_PARAMS 2 | const msg = require('fs').readFileSync(msgPath, 'utf-8').trim() 3 | const child = require('child_process') 4 | const color = require('./color') 5 | 6 | function checkGitAccount () { 7 | child.exec('git config user.email', function (err, sto) { 8 | const unsafeStr = /@didi(chuxing|global)?\.com$/ 9 | 10 | if (err) process.exit(1) 11 | const account = (sto || '').trim() 12 | 13 | if (unsafeStr.test(account)) { 14 | console.error(color.red('Please switch to your personal github account !!!')) 15 | process.exit(1) 16 | } else { 17 | checkCommitMsg(msg) 18 | } 19 | }) 20 | } 21 | 22 | function checkCommitMsg (msg) { 23 | const commitRE = /^(revert|feat|fix|docs|style|refactor|perf|test|chore)(\(.+\))?: .{1,50}/ 24 | 25 | if (!commitRE.test(msg)) { 26 | console.log() 27 | console.error(color.red('Invalid commit message format.') + '\nmust start with revert|feat|fix|docs|style|refactor|perf|test|chore:\nExample: feat: upgrade v0.1 xxx\n') 28 | process.exit(1) 29 | } else { 30 | return true 31 | } 32 | } 33 | 34 | checkGitAccount() 35 | -------------------------------------------------------------------------------- /src/rule/rules/NumberRule.js: -------------------------------------------------------------------------------- 1 | 2 | export default class NumberRule { 3 | static get type () { 4 | return 'number' 5 | } 6 | 7 | static get name () { 8 | return '数字' 9 | } 10 | 11 | constructor (rule = {}) { 12 | const defaultRule = { 13 | type: 'number', 14 | trigger: 'blur', 15 | message: '数字不正确' 16 | } 17 | this.origin = Object.assign({}, defaultRule, rule) 18 | this.rule = { 19 | type: 'number', 20 | trigger: 'blur', 21 | message: '' 22 | } 23 | this.update(this.origin) 24 | } 25 | 26 | update (rule) { 27 | if (rule) { 28 | this.rule.message = rule.message 29 | const { min, max } = rule 30 | const floatMin = parseFloat(min) 31 | const floatMax = parseFloat(max) 32 | if (isNaN(floatMin)) { 33 | delete this.rule.min 34 | } else { 35 | this.rule.min = floatMin 36 | } 37 | if (isNaN(floatMax)) { 38 | delete this.rule.max 39 | } else { 40 | this.rule.max = floatMax 41 | } 42 | // this.rule.min = isNaN(floatMin) ? undefined : floatMin 43 | // this.rule.max = isNaN(floatMax) ? undefined : floatMax 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/rule/rules/PatternRule.js: -------------------------------------------------------------------------------- 1 | 2 | export default class PatternRule { 3 | static get type () { 4 | return 'pattern' 5 | } 6 | 7 | static get name () { 8 | return '正则表达式' 9 | } 10 | 11 | constructor (rule = {}) { 12 | const defaultRule = { 13 | type: 'pattern', 14 | pattern: '', 15 | message: '', 16 | ignoreCase: false, 17 | global: false, 18 | multiline: false 19 | } 20 | this.origin = Object.assign({}, defaultRule, rule) 21 | this.rule = { 22 | type: 'pattern', 23 | trigger: 'blur', 24 | pattern: new RegExp(), 25 | message: '' 26 | } 27 | this.update(this.origin) 28 | } 29 | 30 | update (rule) { 31 | if (rule) { 32 | const { pattern, ignoreCase, global, multiline } = rule 33 | this.rule.message = rule.message 34 | const flags = [ignoreCase ? 'i' : '', global ? 'g' : '', multiline ? 'm' : ''].join('') 35 | let reg = new RegExp() 36 | 37 | if (typeof pattern === 'string' && pattern.length) { 38 | try { 39 | reg = new RegExp(rule.pattern, flags) 40 | } catch (e) { 41 | // console.log(e) 42 | } 43 | } 44 | this.rule.pattern = reg 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/schema/base/radio/Schema.js: -------------------------------------------------------------------------------- 1 | import FormSchema from '../../FormSchema' 2 | import { getRuleValidator } from '../../../helper' 3 | 4 | export default class RadioSchema extends FormSchema { 5 | constructor (props) { 6 | super() 7 | this.type = 'string' 8 | this.label = '单选框' 9 | this.placeholder = '请选择' 10 | this.default = '' 11 | this.option = { 12 | type: 'static', 13 | dict: { 14 | type: 'dict', // dict | api 15 | dict: '', 16 | dictAPI: '', 17 | api: '' 18 | }, 19 | direction: 'horizontal', 20 | url: '', 21 | adapter: 'return data', 22 | data: [ 23 | { key: 'A', value: 'A' } 24 | ], 25 | dynamicData: [] 26 | } 27 | this.create(props) 28 | const rule = { 29 | trigger: 'change', 30 | validator: getRuleValidator(this.rules[0], this.type) 31 | } 32 | this.updateRequiredRule(rule, new.target) 33 | } 34 | } 35 | 36 | // 静态配置,同类widget公有 37 | Object.assign(RadioSchema, { 38 | title: '单选框', 39 | widget: 'radio', 40 | preview: '', 41 | type: ['string', 'number'], 42 | validators: [], 43 | logic: { 44 | value: ['=', '!='], 45 | // event: ['change'] 46 | event: ['change'] 47 | } 48 | }) 49 | -------------------------------------------------------------------------------- /src/schema/base/checkbox/Schema.js: -------------------------------------------------------------------------------- 1 | import FormSchema from '../../FormSchema' 2 | import { getRuleValidator } from '../../../helper' 3 | 4 | export default class CheckboxSchema extends FormSchema { 5 | constructor (props) { 6 | super() 7 | this.type = 'array' 8 | this.label = '多选框' 9 | this.default = [] 10 | this.rules = [{ 11 | required: false, 12 | message: '必填', 13 | type: 'array', 14 | trigger: 'change' 15 | }] 16 | this.option = { 17 | type: 'static', 18 | direction: 'horizontal', 19 | url: '', 20 | adapter: 'return data', 21 | dynamicData: [], 22 | data: [ 23 | { key: 'A', value: 'A' } 24 | ] 25 | } 26 | this.create(props) 27 | const rule = { 28 | trigger: 'change', 29 | validator: getRuleValidator(this.rules[0], this.type) 30 | } 31 | this.updateRequiredRule(rule, new.target) 32 | } 33 | } 34 | 35 | // 静态配置,同类widget公有 36 | Object.assign(CheckboxSchema, { 37 | title: '多选框', 38 | widget: 'checkbox', 39 | preview: '', 40 | type: ['array', 'array'], 41 | validators: [], 42 | logic: { 43 | value: ['<>', '><'], 44 | // event: ['change'] 45 | event: ['change'] 46 | } 47 | }) 48 | -------------------------------------------------------------------------------- /src/store/mutations/model.js: -------------------------------------------------------------------------------- 1 | import types from '../types' 2 | import { 3 | isFunction, 4 | isString, 5 | checkValueType, 6 | convertNameModelToKeyModel 7 | } from '../../helper' 8 | 9 | export default { 10 | // 设置表单model,即用户填写的表单数据 11 | [types.$MODEL_SET] (state, { model, useName }) { 12 | const { flatSchemas } = state 13 | const { flatWidgets } = this.getters 14 | const _model = {} 15 | let keyModel = model // 以schema.key为key组成的model对象 16 | 17 | if (useName) { 18 | keyModel = convertNameModelToKeyModel(model, flatSchemas) 19 | } 20 | const conbinedModel = Object.assign({}, state.model, keyModel) 21 | 22 | for (const i in conbinedModel) { 23 | const schema = flatSchemas[i] 24 | 25 | if (!schema) { 26 | continue 27 | } 28 | 29 | const Widget = flatWidgets[schema.widget] 30 | 31 | if (!Widget || !isFunction(Widget.Schema)) { 32 | continue 33 | } 34 | 35 | if (schema.type === 'json' && !isString(conbinedModel[i])) { 36 | conbinedModel[i] = JSON.stringify(conbinedModel[i]) 37 | } 38 | 39 | if (checkValueType(conbinedModel[i], Widget.Schema.type, schema.dynamic)) { 40 | _model[i] = conbinedModel[i] 41 | } 42 | } 43 | state.model = _model 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/schema/FormSchema.js: -------------------------------------------------------------------------------- 1 | import Schema from './BaseSchema' 2 | import { 3 | setKeyAndName, 4 | updateRequiredRule, 5 | getSchemaType 6 | } from '../helper' 7 | 8 | export default class FormSchema extends Schema { 9 | constructor (props) { 10 | const { schema, widgets = {} } = props || {} 11 | super() 12 | // form name 13 | this.name = '' 14 | // type of widget value 15 | this.type = getSchemaType(schema, new.target) 16 | // label name 17 | this.label = '' 18 | this.description = '' 19 | this.help = '' 20 | this.disabled = false 21 | // 校验规则,参考 [async-validator](https://github.com/yiminghe/async-validator) 22 | this.rules = [{ 23 | required: false, 24 | message: '必填', 25 | type: 'string', 26 | trigger: 'blur' 27 | }] 28 | this.create(props) 29 | 30 | if (widgets[this.widget]) { 31 | updateRequiredRule(this, widgets[this.widget].Schema) 32 | } 33 | } 34 | 35 | // over write the base Schema method 36 | create (props) { 37 | const { clone, dynamic, flatSchemas } = props || {} 38 | Schema.prototype.create.call(this, props) 39 | setKeyAndName(this, clone, dynamic, flatSchemas) 40 | } 41 | 42 | updateRequiredRule (rule, Schema) { 43 | updateRequiredRule(this, Schema, rule) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/schema/base/select/Schema.js: -------------------------------------------------------------------------------- 1 | import FormSchema from '../../FormSchema' 2 | import { getRuleValidator } from '../../../helper' 3 | 4 | export default class SelectSchema extends FormSchema { 5 | constructor (props) { 6 | super() 7 | this.label = '下拉框' 8 | this.placeholder = '请选择' 9 | this.default = '' 10 | this.rules = [{ 11 | required: false, 12 | message: '必填', 13 | trigger: 'change' 14 | }] 15 | this.option = { 16 | type: 'static', 17 | url: '', 18 | adapter: 'return data', 19 | dynamicData: [], 20 | data: [ 21 | { key: 'A', value: 'A' }, 22 | { key: 'B', value: 'B' } 23 | ], 24 | multiple: false, 25 | clearable: true 26 | } 27 | this.create(props) 28 | const rule = { 29 | trigger: 'change', 30 | validator: getRuleValidator(this.rules[0], this.type) 31 | } 32 | this.updateRequiredRule(rule, new.target) 33 | } 34 | } 35 | 36 | // 静态配置,同类widget公有 37 | Object.assign(SelectSchema, { 38 | title: '下拉框', 39 | widget: 'select', 40 | preview: '', 41 | type: ['string', 'number', 'array', 'array', 'array'], 42 | validators: [], 43 | logic: { 44 | value: ['=', '!=', '<>', '><'], 45 | // event: ['focus', 'blur', 'change'] 46 | event: ['change'] 47 | } 48 | }) 49 | -------------------------------------------------------------------------------- /src/schema/base/cascader/Schema.js: -------------------------------------------------------------------------------- 1 | import FormSchema from '../../FormSchema' 2 | import { getRuleValidator } from '../../../helper' 3 | 4 | export default class CascaderSchema extends FormSchema { 5 | constructor (props) { 6 | super() 7 | this.type = 'array' 8 | this.label = '级联选择' 9 | this.placeholder = '请选择' 10 | this.default = [] 11 | this.rules = [{ required: false, message: '必填', type: 'array', trigger: 'change' }] 12 | this.option = { 13 | type: 'static', 14 | url: '', 15 | adapter: 'return data', 16 | dynamicData: [], 17 | delimiter: '/', // 展示时的分隔符 18 | clearable: true, 19 | notFoundText: '无匹配数据', 20 | filterable: false, 21 | data: [ 22 | { 23 | key: 'A', 24 | value: 'option A', 25 | children: [{ 26 | key: 'B', 27 | value: 'option B' 28 | }] 29 | } 30 | ] 31 | } 32 | this.create(props) 33 | const rule = { 34 | trigger: 'change', 35 | validator: getRuleValidator(this.rules[0], this.type) 36 | } 37 | this.updateRequiredRule(rule, new.target) 38 | } 39 | } 40 | 41 | // 静态配置,同类widget公有 42 | Object.assign(CascaderSchema, { 43 | title: '级联选择', 44 | widget: 'cascader', 45 | preview: '', 46 | type: ['array', 'array'], 47 | validators: [], 48 | logic: { 49 | value: ['=', '!='], 50 | // event: ['focus', 'blur', 'change'] 51 | event: ['change'] 52 | } 53 | }) 54 | -------------------------------------------------------------------------------- /src/schema/base/upload/Schema.js: -------------------------------------------------------------------------------- 1 | import FormSchema from '../../FormSchema' 2 | 3 | export default class UploadSchema extends FormSchema { 4 | constructor (props) { 5 | super() 6 | this.label = '上传' 7 | this.placeholder = '请选择文件' 8 | this.default = [] 9 | this.rules = [{ 10 | required: false, 11 | message: '必填', 12 | trigger: 'change', 13 | type: 'array' 14 | }] 15 | this.option = { 16 | // 上传的地址,必填 17 | action: '', 18 | // 匹配上传接口返回的数据格式 19 | adapter: 'return { name: data.filename, url:data.url }', 20 | // 支持的文件类型,与 accept 不同的是,format 是识别文件的后缀名 21 | // accept 为 input 标签原生的 accept 属性,会在选择文件时过滤 22 | // 可以两者结合使用 23 | format: [], 24 | // 接受上传的文件类型 25 | accept: '', 26 | // 上传控件的类型,可选值为 select(点击选择),drag(支持拖拽) 27 | type: 'select', 28 | // 是否支持多选文件 29 | multiple: true, 30 | // 文件大小限制,单位 kb 31 | maxSize: 5000, 32 | // 是否显示已上传文件列表 33 | showUploadList: true, 34 | // 设置上传的请求头部 35 | headers: [], 36 | // 上传的文件字段名 37 | name: 'file', 38 | // 支持发送 cookie 凭证信息 39 | withCredentials: false, 40 | // 上传时附带的额外参数 41 | data: {} 42 | } 43 | this.create(props) 44 | } 45 | } 46 | 47 | // 静态配置,同类widget公有 48 | Object.assign(UploadSchema, { 49 | title: '上传', 50 | widget: 'upload', 51 | preview: '', 52 | type: 'array', 53 | validators: [], 54 | logic: { 55 | value: [], 56 | // event: ['change'] 57 | event: [] 58 | } 59 | }) 60 | -------------------------------------------------------------------------------- /src/event/index.js: -------------------------------------------------------------------------------- 1 | import { include } from '../helper' 2 | /** 3 | * 表单设计器事件处理 4 | */ 5 | export default class EpEvent { 6 | constructor () { 7 | this.$events = {} 8 | } 9 | 10 | /** 11 | * add event listener for widget 12 | * @param {String} key [required] schema.key 13 | * @param {String} type [required] event type 14 | * @param {Function} callback [required] event callback 15 | * @return {event} this 16 | */ 17 | on (key, type, callback) { 18 | if (!(key in this.$events)) { 19 | this.$events[key] = { [type]: [callback] } 20 | } 21 | if (!(type in this.$events[key])) { 22 | this.$events[key][type] = [callback] 23 | } 24 | const callbacks = this.$events[key][type] 25 | if (!include(callbacks, callback)) { 26 | this.$events[key][type].push(callback) 27 | } 28 | 29 | return this 30 | } 31 | 32 | /** 33 | * remove event listener for widget 34 | * @param {String} key [required] schema.key 35 | * @param {String} type event type 36 | * @param {Function} callback event callback 37 | * @return {event} this 38 | */ 39 | off (key, type, callback) { 40 | if (key in this.$events) { 41 | if (type in this.$events[key]) { 42 | const callbacks = this.$events[key][type] 43 | const index = callbacks.indexOf(callback) 44 | if (index > -1) { 45 | callbacks.splice(index, 1) 46 | } else { 47 | this.$events[key][type] = [] 48 | } 49 | } else if (typeof type === 'undefined') { 50 | delete this.$events[key] 51 | } 52 | } 53 | return this 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/helper/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | getRootSchemaChildren, 3 | getParentListByKey, 4 | getIndexByKey, 5 | replaceSchemaKey, 6 | flattenSchema, 7 | getWidgetType, 8 | getWidgetModel, 9 | setKeyAndName, 10 | getFormData, 11 | getSchema, 12 | storeFetchClean, 13 | mergeWidgets, 14 | mergeWidget, 15 | setValidators, 16 | updateRequiredRule, 17 | getSchemaType, 18 | getRequiredRuleType, 19 | getRuleValidator, 20 | usePlugins, 21 | convertNameModelToKeyModel, 22 | cleanDefaultValue, 23 | getDefaults 24 | } from './epUtil' 25 | 26 | import { 27 | randomStr, 28 | isPlainObject, 29 | isFunction, 30 | isString, 31 | isNumber, 32 | jsonClone, 33 | isArray, 34 | ajax, 35 | isNotEmptyString, 36 | include, 37 | debounce, 38 | getValueType, 39 | checkValueType, 40 | formatDate, 41 | isNumberString, 42 | copy 43 | } from './util' 44 | 45 | export { 46 | getRootSchemaChildren, 47 | getParentListByKey, 48 | getIndexByKey, 49 | replaceSchemaKey, 50 | flattenSchema, 51 | getWidgetType, 52 | getWidgetModel, 53 | setKeyAndName, 54 | getFormData, 55 | getSchema, 56 | storeFetchClean, 57 | mergeWidgets, 58 | mergeWidget, 59 | setValidators, 60 | updateRequiredRule, 61 | getSchemaType, 62 | getRequiredRuleType, 63 | getRuleValidator, 64 | usePlugins, 65 | convertNameModelToKeyModel, 66 | cleanDefaultValue, 67 | getDefaults, 68 | 69 | randomStr, 70 | isPlainObject, 71 | isFunction, 72 | isString, 73 | isNumber, 74 | isArray, 75 | jsonClone, 76 | ajax, 77 | isNotEmptyString, 78 | include, 79 | debounce, 80 | getValueType, 81 | checkValueType, 82 | formatDate, 83 | isNumberString, 84 | copy 85 | } 86 | -------------------------------------------------------------------------------- /src/schema/BaseSchema.js: -------------------------------------------------------------------------------- 1 | import { 2 | isPlainObject, 3 | isArray, 4 | randomStr, 5 | include, 6 | jsonClone 7 | } from '../helper' 8 | 9 | // this prop list should be ignore when copy schema 10 | const IGNORE_PROP_LIST = ['list'] 11 | 12 | /** 13 | * the basic Schema 14 | */ 15 | export default class BaseSchema { 16 | constructor (props) { 17 | const { schema } = props || {} 18 | // globally unique 19 | this.key = randomStr() 20 | // widget type 21 | this.widget = '' 22 | // should be hidden 23 | this.hidden = false 24 | // extra options for this schema 25 | this.option = {} 26 | this.style = {} 27 | // create schema instance from schema object 28 | this.create(props) 29 | if (!this.widget) { 30 | this.widget = new.target 31 | ? new.target.widget 32 | : (schema ? schema.widget : '') 33 | } 34 | } 35 | 36 | create (props) { 37 | const { schema, clone } = props || {} 38 | if (isPlainObject(schema)) { 39 | // 拷贝所有属性,没有的遵循默认声明 40 | for (const i in schema) { 41 | if (include(IGNORE_PROP_LIST, i) || !(i in this)) { 42 | continue 43 | } 44 | let item = schema[i] 45 | if (isArray(item)) { 46 | // item = Object.assign([], this[i], item) 47 | item = jsonClone(item) 48 | } else if (isPlainObject(item)) { 49 | if (isPlainObject(this[i])) { 50 | item = jsonClone({ ...this[i], ...item }) 51 | } else { 52 | item = jsonClone(item) 53 | } 54 | } 55 | this[i] = item 56 | } 57 | // 复制整个schema时 58 | if (clone || !this.key) { 59 | this.key = randomStr() 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/store/mutations/rule.js: -------------------------------------------------------------------------------- 1 | import types from '../types' 2 | import Rule from '../../rule' 3 | import { 4 | isFunction, 5 | isArray, 6 | updateRequiredRule 7 | } from '../../helper' 8 | 9 | export default { 10 | [types.$RULE_INIT] (state, { rootSchema }) { 11 | state.flatRules = new Rule(rootSchema).rules 12 | }, 13 | // 设计模式为选中widget修改规则类型 14 | [types.$RULE_TYPE_UPDATE] ({ flatRules, flatSchemas }, { key, type, index }) { 15 | const schema = flatSchemas[key] 16 | const { widgetsValidators } = this.getters 17 | const widgetValidators = widgetsValidators[schema.widget] 18 | 19 | if (!isArray(widgetValidators)) { 20 | return 21 | } 22 | const Rule = widgetValidators.find(v => v.type === type) 23 | if (!isFunction(Rule)) { 24 | return 25 | } 26 | const newRule = new Rule().rule 27 | const currentRules = flatRules[key] 28 | const originRules = [...schema.rules] 29 | 30 | currentRules.splice(index, 1, newRule) 31 | originRules.splice(index, 1, { message: newRule.message, type }) 32 | schema.rules = originRules 33 | }, 34 | 35 | [types.$RULE_REQUIRED_RULE_UPDATE] ({ flatRules, flatSchemas }, { key, rule }) { 36 | const schema = flatSchemas[key] 37 | const WidgetSchema = this.getters.flatWidgets[schema.widget].Schema 38 | 39 | updateRequiredRule(schema, WidgetSchema, rule) 40 | }, 41 | 42 | // 设计模式为选中widget修改规则错误消息 43 | [types.$RULE_MESSAGE_UPDATE] ({ flatRules }, { key, index, message }) { 44 | const currentRules = flatRules[key] 45 | 46 | if (!isArray(currentRules)) { 47 | return 48 | } 49 | currentRules[index].message = message 50 | }, 51 | 52 | // 设计模式为选中widget添加规则 53 | [types.$RULE_ADD] ({ flatSchemas }, { key }) { 54 | const schema = flatSchemas[key] 55 | const defaultRule = { type: '', message: '' } 56 | 57 | schema.rules.push(defaultRule) 58 | }, 59 | 60 | // 设计模式为选中widget删除规则 61 | [types.$RULE_REMOVE] ({ flatRules, flatSchemas }, { key, index }) { 62 | const schema = flatSchemas[key] 63 | const rules = flatRules[key] 64 | 65 | schema.rules.splice(index, 1) 66 | if (!rules) { 67 | return 68 | } 69 | rules.splice(index, 1) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/store/TypeBuilder.js: -------------------------------------------------------------------------------- 1 | import { isPlainObject, isFunction } from '../helper' 2 | 3 | export default class TypeBuilder { 4 | /** 5 | * 构建不同类型数据的默认值,builder必须返回全新数据 6 | * @param {Object} types 新builder类型,与内置类型重复将被忽略 7 | * 内置:string | boolean | number | array | object | null 8 | */ 9 | constructor (types) { 10 | this.types = { 11 | string: () => '', 12 | boolean: () => false, 13 | number: () => 0, 14 | array: () => [], 15 | object: () => ({}), 16 | null: () => null, 17 | date: () => new Date(), 18 | json: () => '{}', 19 | // composition type 20 | 'array': () => [], 21 | 'array': () => [], 22 | 'array': () => [], 23 | 'array': () => [] 24 | } 25 | this.add(types) 26 | this.convert = {} 27 | } 28 | 29 | /** 30 | * 添加自定义数据类型的builder 31 | * @param {Object} types builder 对应类型 32 | * 如:{ string: () => ''} 33 | */ 34 | add (types) { 35 | if (!isPlainObject(types)) { 36 | return 37 | } 38 | for (const i in types) { 39 | const builder = types[i] 40 | if (!(i in this.types) && isFunction(builder)) { 41 | this.types[i] = builder 42 | } 43 | } 44 | } 45 | } 46 | 47 | TypeBuilder.convert = { 48 | string2number: s => { 49 | const r = parseFloat(s) 50 | return isNaN(r) ? null : r 51 | }, 52 | string2boolean: s => !!s, 53 | number2string: n => { 54 | const r = String(n) 55 | return r === '[object Object]' ? null : r 56 | }, 57 | number2boolean: n => !!n, 58 | boolean2string: b => String(b), 59 | boolean2number: b => Number(b), 60 | string2json: s => { 61 | try { 62 | return JSON.parse(s) 63 | } catch (e) { 64 | return null 65 | } 66 | } 67 | } 68 | 69 | /** 70 | * resolve schema type 71 | * @param {String} type Schema.type 72 | * @param {Boolean} structure data structure, 73 | * @example 74 | * resolve('array', true) // array 75 | * resolve('string', true) // string 76 | */ 77 | TypeBuilder.resolve = function (type, structure) { 78 | const matched = type.match(/<(\w+)>/) 79 | if (structure) { 80 | return type.split('<')[0] 81 | } 82 | return matched ? matched[1].toLowerCase() : type.toLowerCase() 83 | } 84 | -------------------------------------------------------------------------------- /src/schema/base/grid/Schema.js: -------------------------------------------------------------------------------- 1 | import BaseSchema from '../../BaseSchema' 2 | import { 3 | isArray, 4 | isPlainObject, 5 | isNotEmptyString, 6 | jsonClone, 7 | isNumber, 8 | isFunction 9 | } from '../../../helper' 10 | 11 | const getDefaultChildren = () => [ 12 | { span: 12, list: [] }, 13 | { span: 12, list: [] } 14 | ] 15 | export default class GridSchema extends BaseSchema { 16 | constructor (props) { 17 | super() 18 | this.label = '布局' 19 | this.container = true 20 | // 设置form data数据是否平级展开还是层级嵌套 21 | // this.group = false 22 | this.children = getDefaultChildren() 23 | this.option = { 24 | gutter: 0, 25 | align: 'top', 26 | justify: 'start' 27 | } 28 | // this.style = { 29 | // background: 'rgba(255, 255, 255, 0)', 30 | // border: '0px solid #ccc', 31 | // padding: '0 0 0 0', 32 | // margin: '0 0 0 0', 33 | // 'border-radius': '4px' 34 | // } 35 | this.create(props) 36 | this.createChildren(props) 37 | } 38 | 39 | createChildren (props) { 40 | let { schema, widgets = {}, clone, dynamic } = props || {} 41 | if (isPlainObject(schema) && isNotEmptyString(schema.widget)) { 42 | // 避免影响到原schema对象,这里需要深度clone一份 43 | schema = jsonClone(schema) 44 | const children = isArray(schema.children) ? schema.children : [] 45 | if (!children.length) { 46 | children.push(getDefaultChildren()) 47 | } 48 | children.forEach(child => { 49 | if (!isNumber(child.span) || child.span < 0) { 50 | child.span = 12 51 | } 52 | const list = [] 53 | if (isArray(child.list)) { 54 | child.list.forEach(sub => { 55 | const widget = widgets[sub.widget] 56 | if (widget && isFunction(widget.Schema)) { 57 | const newSub = new widget.Schema({ schema: sub, widgets, clone, dynamic }) 58 | list.push(newSub) 59 | } 60 | }) 61 | } 62 | child.list = list 63 | }) 64 | this.children = children 65 | } 66 | } 67 | } 68 | 69 | // 静态配置,同类widget公有 70 | Object.assign(GridSchema, { 71 | title: '栅格', 72 | widget: 'grid', 73 | preview: '', 74 | logic: { 75 | value: [], 76 | event: [] 77 | } 78 | }) 79 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "epage-core", 3 | "version": "0.5.6", 4 | "description": "The core of epage", 5 | "author": "Chengzi ", 6 | "main": "./dist/epage-core.min.js", 7 | "main:epage": "./src/main.js", 8 | "scripts": { 9 | "build": "webpack --config script/webpack.build.js", 10 | "lint": "eslint --ext .vue --ext .js src/ --fix", 11 | "analyz": "npm_config_report=true npm run build" 12 | }, 13 | "files": [ 14 | "dist", 15 | "src" 16 | ], 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/epage-team/epage-core.git" 20 | }, 21 | "keywords": [ 22 | "epage", 23 | "core" 24 | ], 25 | "bugs": { 26 | "url": "https://github.com/epage-team/epage-core/issues" 27 | }, 28 | "homepage": "https://github.com/epage-team/epage-core#readme", 29 | "license": "MIT", 30 | "husky": { 31 | "hooks": { 32 | "pre-commit": "npm run lint && git add .", 33 | "commit-msg": "node script/verify-commit-msg.js" 34 | } 35 | }, 36 | "peerDependencies": { 37 | "vue": ">=2.5", 38 | "vuex": ">=3.1" 39 | }, 40 | "dependencies": { 41 | "vuedraggable": ">=2.20.0" 42 | }, 43 | "devDependencies": { 44 | "@babel/core": "^7.8.4", 45 | "@babel/preset-env": "^7.8.4", 46 | "babel-loader": "^8.0.6", 47 | "babel-plugin-transform-runtime": "^6.23.0", 48 | "clean-webpack-plugin": "^2.0.2", 49 | "cross-env": "^5.2.0", 50 | "eslint": "^6.6.0", 51 | "eslint-config-standard": "^14.1.0", 52 | "eslint-plugin-import": "^2.18.2", 53 | "eslint-plugin-node": "^10.0.0", 54 | "eslint-plugin-promise": "^4.2.1", 55 | "eslint-plugin-standard": "^4.0.1", 56 | "eslint-plugin-vue": "^6.0.1", 57 | "husky": "^4.2.3", 58 | "react": "^17.0.1", 59 | "react-dom": "^17.0.1", 60 | "uglifyjs-webpack-plugin": "^1.0.0-rc.0", 61 | "vue": "^2.5", 62 | "vuex": "^3.1.1", 63 | "webpack": "^4.35.2", 64 | "webpack-bundle-analyzer": "^3.3.2", 65 | "webpack-cli": "^3.3.5", 66 | "webpack-dev-server": "^3.3.1", 67 | "webpack-merge": "^4.2.1" 68 | }, 69 | "engines": { 70 | "node": ">= 8.10.0", 71 | "npm": ">= 4.0.0" 72 | }, 73 | "browserslist": [ 74 | "> 1%", 75 | "last 2 versions", 76 | "not ie <= 8" 77 | ] 78 | } 79 | -------------------------------------------------------------------------------- /src/rule/Rule.js: -------------------------------------------------------------------------------- 1 | 2 | import { isArray, isFunction } from '../helper' 3 | /** 4 | * 规则引擎,处理schema规则并返回validator规则 5 | */ 6 | 7 | export default class Rule { 8 | constructor (schema) { 9 | this.schema = schema 10 | this.rules = this.recursiveResolve(this.schema) 11 | } 12 | 13 | recursiveResolve (schema) { 14 | let rules = {} 15 | const recursive = (schema, rules) => { 16 | const { key, container, children, dynamic, list } = schema 17 | 18 | if (dynamic && isArray(list)) { 19 | list.forEach(sc => recursive(sc, rules)) 20 | } 21 | 22 | if (container && isArray(children)) { 23 | children.forEach(child => { 24 | if (isArray(child.list)) { 25 | child.list.forEach(item => recursive(item, rules)) 26 | } 27 | }) 28 | } else { 29 | if (key in rules) { 30 | console.warn(`${key} is already in rules`) 31 | } else { 32 | if (key) { 33 | rules[key] = this.resolve(schema) 34 | } 35 | } 36 | } 37 | 38 | return rules 39 | } 40 | rules = recursive(schema, rules) 41 | return rules 42 | } 43 | 44 | resolve (schema) { 45 | const rules = schema.rules || [] 46 | const rulesWithValidator = [] 47 | const len = rules.length 48 | 49 | for (let i = 0; i < len; i++) { 50 | // 默认是否必填作为第一个规则,直接copy到规则列表内 51 | if (i === 0) { 52 | rulesWithValidator.push(rules[0]) 53 | } else { 54 | const TmpRule = Rule.rules[rules[i].type] 55 | 56 | if (isFunction(TmpRule)) { 57 | rulesWithValidator.push(new TmpRule(rules[i]).rule) 58 | } 59 | } 60 | } 61 | return rulesWithValidator 62 | } 63 | 64 | static set (rules) { 65 | if (rules) { 66 | const map = {} 67 | Object.keys(rules).forEach(k => { 68 | const type = rules[k].type 69 | if (type in map) { 70 | return console.log(`rule warning: ${type} is already exist!`) 71 | } 72 | map[type] = rules[k] 73 | }) 74 | 75 | if (Rule.rules) { 76 | Object.assign(Rule.rules, map) 77 | // conbineList(Rule.rule.list, list) 78 | } else { 79 | Rule.rules = map 80 | } 81 | } 82 | } 83 | } 84 | 85 | Rule.rules = {} 86 | -------------------------------------------------------------------------------- /src/store/types.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * @type {Object} mutation type 全局唯一 4 | */ 5 | const types = {} 6 | const list = [ 7 | // 更新设计模式当前tab 8 | '$TAB_UPDATE', 9 | // 更新可用widgets 10 | '$WIDGETS_SET', 11 | // 设置全局schema, 12 | '$ROOT_SCHEMA_SET', 13 | // 拍平root schema 14 | '$ROOT_SCHEMA_FLAT', 15 | // 用户数据设置,kv形式的对象 16 | '$MODEL_SET', 17 | // 切换表单展示模式 18 | '$MODE_CHANGE', 19 | 20 | // 添加逻辑,值逻辑或事件逻辑 21 | '$LOGIC_ADD', 22 | // 更新逻辑 23 | '$LOGIC_UPDATE', 24 | // 删除逻辑 25 | '$LOGIC_DELETE', 26 | 27 | // 规则初始化 28 | '$RULE_INIT', 29 | // 更新widget规则类型 30 | '$RULE_TYPE_UPDATE', 31 | // 是否必填规则更新 32 | '$RULE_REQUIRED_RULE_UPDATE', 33 | // 更新widget规则错误消息 34 | '$RULE_MESSAGE_UPDATE', 35 | // 添加widget规则 36 | '$RULE_ADD', 37 | // 删除widget规则 38 | '$RULE_REMOVE', 39 | 40 | // 选中store中dict 41 | '$STORE_DICT_SELECT', 42 | // 更新store中dict 43 | '$STORE_DICT_UPDATE', 44 | // 添加store中dict 45 | '$STORE_DICT_ADD', 46 | // 删除store中dict 47 | '$STORE_DICT_DELETE', 48 | // 复制store中dict 49 | '$STORE_DICT_COPY', 50 | // 选中store中api 51 | '$STORE_API_SELECT', 52 | // 更新store中api 53 | '$STORE_API_UPDATE', 54 | // 添加store中api 55 | '$STORE_API_ADD', 56 | // 删除store中api 57 | '$STORE_API_DELETE', 58 | // 复制store中api 59 | '$STORE_API_COPY', 60 | 61 | // 更新widget默认属性 62 | '$WIDGET_DEFAULT_PROPS_UPDATE', 63 | // 通过值逻辑关系触发的widget属性改变 64 | '$WIDGET_UPDATE_BY_VALUE_LOGIC', 65 | // 通过事件逻辑关系触发的widget属性改变 66 | '$WIDGET_UPDATE_BY_EVENT_LOGIC', 67 | // 添加widget 68 | '$WIDGET_ADD', 69 | // 删除widget 70 | '$WIDGET_REMOVE', 71 | // 用户动态添加widget 72 | '$WIDGET_DYNAMIC_ADD', 73 | // 用户动态移除widget 74 | '$WIDGET_DYNAMIC_REMOVE', 75 | // 复制widget 76 | '$WIDGET_COPY', 77 | // 更改widget值类型 78 | '$WIDGET_TYPE_UPDATE', 79 | // 设置widget option 80 | '$WIDGET_OPTION_UPDATE', 81 | // 更新表单类型widget默认值 82 | '$WIDGET_DEFAULT_UPDATE', 83 | // 选中widget 84 | '$WIDGET_SELECT', 85 | // 取消选中widget 86 | '$WIDGET_DESELECT', 87 | 88 | // 容器widget添加子容器 89 | '$WIDGET_CHILD_ADD', 90 | // 容器widget删除子容器 91 | '$WIDGET_CHILD_REMOVE', 92 | // 容器widget移动子容器 93 | '$WIDGET_CHILD_MOVE', 94 | 95 | // 更新全局样式 96 | '$STYLE_UPDATE' 97 | ] 98 | 99 | list.forEach(i => (types[i] = i)) 100 | 101 | export default types 102 | -------------------------------------------------------------------------------- /script/webpack.build.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | const merge = require('webpack-merge') 4 | const CleanWebpackPlugin = require('clean-webpack-plugin') 5 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin') 6 | const webpackBaseConfig = require('./webpack.base.js') 7 | const pkg = require('../package.json') 8 | 9 | const banner = `epage-core v${pkg.version} 10 | (c) 2020-present Chengzi 11 | Released under the MIT License.` 12 | 13 | const webpackConfig = merge(webpackBaseConfig, { 14 | mode: 'production', 15 | entry: { 16 | 'epage-core': './src/main.js' 17 | }, 18 | output: { 19 | path: path.resolve(__dirname, '../dist'), 20 | publicPath: '', 21 | filename: '[name].min.js', 22 | library: 'EpageCore', 23 | libraryTarget: 'umd', 24 | umdNamedDefine: true 25 | }, 26 | externals: { 27 | react: { 28 | root: 'React', 29 | commonjs: 'react', 30 | commonjs2: 'react', 31 | amd: 'react' 32 | }, 33 | 'react-dom': { 34 | root: 'ReactDOM', 35 | commonjs: 'react-dom', 36 | commonjs2: 'react-dom', 37 | amd: 'react-dom' 38 | }, 39 | vuex: { 40 | root: 'Vuex', 41 | commonjs: 'vuex', 42 | commonjs2: 'vuex', 43 | amd: 'vuex' 44 | }, 45 | vue: { 46 | root: 'Vue', 47 | commonjs: 'vue', 48 | commonjs2: 'vue', 49 | amd: 'vue' 50 | }, 51 | sortablejs: { 52 | root: 'Sortable', 53 | commonjs: 'sortablejs', 54 | commonjs2: 'sortablejs', 55 | amd: 'sortablejs' 56 | }, 57 | vuedraggable: { 58 | root: 'vuedraggable', 59 | commonjs: 'vuedraggable', 60 | commonjs2: 'vuedraggable', 61 | amd: 'vuedraggable' 62 | } 63 | }, 64 | // devtool: 'source-map', 65 | optimization: { 66 | minimizer: [new UglifyJsPlugin({ 67 | parallel: true, 68 | sourceMap: false, 69 | uglifyOptions: { 70 | ecma: 8, 71 | warnings: false 72 | } 73 | })] 74 | }, 75 | plugins: [ 76 | new CleanWebpackPlugin({ 77 | cleanOnceBeforeBuildPatterns: path.resolve(__dirname, '../dist') 78 | }), 79 | new webpack.BannerPlugin(banner), 80 | new webpack.optimize.ModuleConcatenationPlugin() 81 | ] 82 | }) 83 | 84 | if (process.env.npm_config_report) { 85 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin 86 | webpackConfig.plugins.push(new BundleAnalyzerPlugin()) 87 | } 88 | 89 | module.exports = webpackConfig 90 | -------------------------------------------------------------------------------- /src/render/VueRender.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import Rule from '../rule' 4 | import Store from '../store' 5 | import { 6 | isArray, 7 | isFunction, 8 | isPlainObject, 9 | usePlugins 10 | } from '../helper' 11 | 12 | export default class Render { 13 | constructor (option) { 14 | const { 15 | el, 16 | store, 17 | widgets = [], 18 | mode, 19 | schema, 20 | component, 21 | Rule: CustomRule, 22 | callPlugin 23 | } = option 24 | 25 | this.el = el 26 | this.mode = mode || 'edit' 27 | this.$$origin = null 28 | this.store = null 29 | // 优先自定义组件渲染 30 | this.component = component || null 31 | this.callPlugin = callPlugin || (() => 0) 32 | usePlugins(Vue, [Vuex]) 33 | this.callPlugin('render', 'init', { Vue, ctx: this }) 34 | // 设计模式下,状态共享epage设计器内store 35 | if (store) { 36 | this.store = store 37 | this.$$origin = this.render() 38 | this.callPlugin('render', 'created', { ctx: this }) 39 | } else { 40 | this.store = new Store({ Rule: CustomRule || Rule }) 41 | if (isArray(widgets)) { 42 | this.store.initWidgets(widgets) 43 | if (isPlainObject(schema)) { 44 | this.store.initRootSchema(schema) 45 | } 46 | this.$$origin = this.render() 47 | this.callPlugin('render', 'created', { ctx: this }) 48 | } else { 49 | console.error('widgets must be an array') 50 | } 51 | } 52 | this.on = component.on 53 | this.off = component.off 54 | } 55 | 56 | validateFields () { 57 | const { $children } = this.$$origin 58 | 59 | if (isArray($children) && $children[0]) { 60 | return $children[0].validateFields(...arguments) 61 | } 62 | } 63 | 64 | resetFields () { 65 | const { $children } = this.$$origin 66 | 67 | if (isArray($children) && $children[0]) { 68 | return new Promise((resolve) => { 69 | setTimeout(() => { 70 | resolve($children[0].resetFields()) 71 | }, 0) 72 | }) 73 | } 74 | } 75 | 76 | render (option = {}) { 77 | const { el, store, mode, component } = this 78 | const extension = { store, $render: this, mode: option.mode || mode } 79 | const root = document.createElement('div') 80 | 81 | el.appendChild(root) 82 | this.callPlugin('render', 'beforeCreate', { ctx: this }) 83 | 84 | const ins = new Vue({ 85 | extension, 86 | el: root, 87 | render: h => h(component) 88 | }) 89 | 90 | return ins 91 | } 92 | 93 | destroy () { 94 | if (this.$$origin && isFunction(this.$$origin.$destroy)) { 95 | this.callPlugin('render', 'beforeDestroy', { ctx: this }) 96 | this.$$origin.$destroy() 97 | this.$$origin.$off() 98 | if (!this.el.contains(this.$$origin.$el)) return 99 | this.el.removeChild(this.$$origin.$el) 100 | this.callPlugin('render', 'destroyed', { ctx: this }) 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/render/ReactRender.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import Rule from '../rule' 4 | import Store from '../store' 5 | import { 6 | isArray, 7 | isFunction, 8 | isPlainObject, 9 | usePlugins 10 | } from '../helper' 11 | 12 | export default class Render { 13 | constructor (option) { 14 | const { 15 | el, 16 | store, 17 | widgets = [], 18 | mode, 19 | schema, 20 | component, 21 | Rule: CustomRule, 22 | callPlugin 23 | } = option 24 | 25 | this.el = el 26 | this.mode = mode || 'edit' 27 | this.$$origin = null 28 | this.store = null 29 | this.component = component 30 | this.callPlugin = callPlugin || (() => 0) 31 | 32 | const React = require('react').default 33 | this.formRef = React.createRef() 34 | 35 | usePlugins(Vue, [Vuex]) 36 | this.callPlugin('render', 'init', { Vue, ctx: this }) 37 | 38 | if (store) { 39 | this.store = store 40 | this.$$origin = this.render() 41 | } else { 42 | this.store = new Store({ Rule: CustomRule || Rule }) 43 | if (isArray(widgets)) { 44 | this.store.initWidgets(widgets) 45 | if (isPlainObject(schema)) { 46 | this.store.initRootSchema(schema) 47 | } 48 | this.$$origin = this.render() 49 | } else { 50 | console.error('widgets must be an array') 51 | } 52 | } 53 | this.on = component.on 54 | this.off = component.off 55 | } 56 | 57 | validateFields () { 58 | return new Promise((resolve, reject) => { 59 | return this.formRef.current 60 | .validateFields(...arguments) 61 | .then(values => { 62 | resolve(true) 63 | }) 64 | .catch(err => { 65 | resolve({ err }) 66 | }) 67 | }) 68 | } 69 | 70 | resetFields () { 71 | return new Promise((resolve, reject) => { 72 | this.formRef.current.resetFields() 73 | resolve(true) 74 | }) 75 | } 76 | 77 | render (option = {}) { 78 | const { el, store, mode, formRef, component } = this 79 | const props = { 80 | store, 81 | mode: option.mode || mode, 82 | formRef, 83 | onSubmit: this.onSubmit 84 | } 85 | this.callPlugin('render', 'beforeCreate', { ctx: this }) 86 | const ReactDOM = require('react-dom').default 87 | const React = require('react').default 88 | const ins = ReactDOM.render( 89 | React.createElement(component, { ...props }), 90 | el 91 | ) 92 | this.callPlugin('render', 'created', { ctx: this, ins }) 93 | return ins 94 | } 95 | 96 | destrory () { 97 | const ReactDOM = require('react-dom').default 98 | if (this.$$origin && isFunction(this.$$origin.$destroy)) { 99 | this.callPlugin('render', 'beforeDestroy', { ctx: this }) 100 | ReactDOM.unmountComponentAtNode(this.el) 101 | // this.$$origin.$off() 102 | // if(!this.el.contains(this.$$origin.$el)) return 103 | // this.el.removeChild(this.$$origin.$el) 104 | this.callPlugin('render', 'destroyed', { ctx: this }) 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ### 0.5.6(2021/4/29) 4 | 5 | - [fix] : 修复根据schema添加widget时name被重置 6 | 7 | ### 0.5.5(2021/4/14) 8 | 9 | - [feat] : 增加基础表单组件`Schema`的`change`事件暴露,可通过逻辑面板配置`change`逻辑 10 | - [fix] : 修复逻辑配置无法选择widget问题 11 | 12 | ### 0.5.4(2021/4/6) 13 | 14 | - [fix] : 修复插件使用`copy`勾子复制schema未生效问题 15 | - [fix] : 渲染器生命周期`created`位置不正确问题 16 | 17 | ### 0.5.3(2021/4/2) 18 | 19 | - [fix] : 修复`store.addWidget()`后更新`store.updateWidgetOption(rootSchema.key, {})`选项不生效问题 20 | 21 | 22 | ### 0.5.2(2021/4/1) 23 | 24 | - [feat] : `store.addWidget(widget)` 优化,widget可以为如`input`形式字符串,也可以为`{ widget: 'input', ...}` 完整schema 25 | - [feat] : `util.flattenSchema` 导出增加对象增加 `rootSchema`引用。可以通过`store.updateWidgetOption(rootSchema.key, {...})`更新`rootSchema.option`选项 26 | 27 | ### 0.5.1(2021/2/21) 28 | 29 | - [fix] : 添加`vuedraggable`依赖,作为拖拽能力 30 | 31 | ### 0.5.0(2021/2/20) 32 | 33 | - [feat] : 对外暴露`drag`模块,当前支持vue模块拖动,为自定义容器组件提供能力,依赖`vuedraggable` 34 | - [feat] : 对外暴露`hook`模块,定制设计器及渲染器时可使用,一般用于设计器插件中,可参考`epage`设计器hook使用,能力如下: 35 | 36 | ```js 37 | const syncHook = new hook.SyncHook() // 同步串行任务,不接受上一个任务结果 38 | syncHook.tap(callback) // 将callback添加到同步任务队列 39 | syncHook.call(args) // 将arguments分别传给任务队列的callback中 40 | 41 | const syncWaterHook = new hook.SyncWaterfallHook() // 同步串行任务,接受上一个任务结果 42 | // 用法同上,只是会将每个任务结果作为参数传给下个任务 43 | ``` 44 | - [feat] : 对外暴露`render`模块,直接提供`render.VueRender`,实验阶段的`ReactRender`可通过以下方式引入 45 | 46 | ```js 47 | import ReactRender from 'epage-core/src/render/ReactRender' 48 | ``` 49 | 方式 50 | - 51 | ### 0.4.1(2021/1/18) 52 | 53 | - [fix] : 修复widget被二次添加时,widget.Setting生命周期没有再次使用问题 54 | 55 | ### 0.4.0(2020/12/31) 56 | 57 | - [feat] : 开放单一widget样式配置,自定义高级背景配置 58 | - [feat] : 新增`text` widget能力,支持`{{$f[schema.name]]}}`表达式运算 59 | 60 | ### 0.3.0(2020/11/04) 61 | 62 | - [feat] : 增加整体页面配置(背景色、间距等),对外暴露`style`模块,目前包含`{ Background }` 63 | - [feat] : 字典能力补充,未发版 64 | 65 | ### 0.2.1~0.2.2(2020/10/20) 66 | 67 | - [feat] : 逻辑关系中,被控组件存在多值时,增加值的 `或`、`且` 关系 68 | - [feat] : 逻辑关系比较增加值类型条件 69 | - [fix] : 修复 `cascader` 值类型转换未递归问题 70 | - [fix] : 多个逻辑关系,改变值时出现未知现象,添加容错 71 | 72 | 73 | ### 0.2.0(2020/10/12) 74 | 75 | - [feat] : 增加widget显隐属性默认值,逻辑配置面板,不符合条件时回退到默认值 76 | - [feat] : 逻辑配置面板,可配置自定义脚本,可直接使用ctx全局变量,关于ctx可参考button的[ctx](http://epage.didichuxing.com/examples/widgets/button.html#schema-option%E5%AE%9A%E4%B9%89) 77 | 78 | ### 0.1.8(2020/09/22) 79 | 80 | - [fix] : 修复包内部import依赖大小写问题 [epage#8](https://github.com/didi/epage/issues/8) 81 | 82 | ### 0.1.7(2020/08/18) 83 | 84 | - [fix] : 修复上个版本增加`json`类型忽略`undefined`情况导致渲染报错 85 | 86 | ### 0.1.6(2020/08/17) 87 | 88 | - [feat] : 增加`json`表单类型校验 89 | 90 | ### 0.1.5(2020/08/10) 91 | 92 | - [feat] : 增加`Context`及`Scirpt`模块,方便在自定义脚本中增加context环境 93 | 94 | ```js 95 | import { Context, Script } from 'epage-core' 96 | const { $render } = this.$root.$options.extension 97 | const { script } = this.schema.option 98 | const ctx = new Context({ 99 | $el, 100 | $render, 101 | store, 102 | instance: this, 103 | state: { 104 | loading: this.loading 105 | } 106 | }) 107 | const sc = new Script(ctx) 108 | sc.exec(script) 109 | ``` 110 | 111 | 112 | ### 0.1.4(2020/08/01) 113 | 114 | - [feat] : 添加基础widget默认值 115 | 116 | ### 0.1.3(2020/07/22) 117 | 118 | - [feat] : 添加 `schema.base.RootSchema` 模块默认样式属性 119 | 120 | ### 0.1.2 121 | 122 | - [feat] : 添加 `schema.base` 子模块导出 123 | 124 | ```js 125 | import { schema } from 'epage-core' 126 | 127 | console.log(schema.base) 128 | // { input: Schema, select: Schema, ...} 129 | ``` 130 | 131 | ### 0.1.1 132 | 133 | - [feat] : 添加 `TypeBuilder` 导出 134 | 135 | ### 0.1.0 136 | 137 | - [feat] : 从 [epage](https://github.com/didi/epage) 项目抽离核心项目,作为设计器(epage)及渲染器(如epage-iview)的核心依赖 -------------------------------------------------------------------------------- /src/store/mutations/schema.js: -------------------------------------------------------------------------------- 1 | import types from '../types' 2 | import RootSchema from '../../schema/RootSchema' 3 | import API from '../API' 4 | import Dict from '../Dict' 5 | import TypeBuilder from '../TypeBuilder' 6 | import { 7 | flattenSchema, 8 | jsonClone, 9 | cleanDefaultValue, 10 | isArray, 11 | getWidgetType, 12 | getWidgetModel 13 | } from '../../helper' 14 | const typeBuilder = new TypeBuilder() 15 | 16 | export default { 17 | [types.$ROOT_SCHEMA_FLAT] (state, { rootSchema }) { 18 | state.rootSchema = rootSchema 19 | state.flatSchemas = flattenSchema(rootSchema) 20 | }, 21 | 22 | // 添加全局schema,会重新修改store中model数据 23 | [types.$ROOT_SCHEMA_SET] (state, { rootSchema }) { 24 | const { flatWidgets } = this.getters 25 | const model = {} // 最终值 26 | const defaultModel = {} // 根据schema实例定义的默认值 27 | const _rootSchema = new RootSchema({ schema: rootSchema, widgets: flatWidgets }) 28 | 29 | // fix logic field 30 | _rootSchema.logics.map(logic => { 31 | logic.trigger = logic.trigger || 'prop' 32 | logic.script = logic.script || '' 33 | logic.relation = logic.relation || 'or' 34 | }) 35 | 36 | // 初始化 model 37 | state.rootSchema = Object.assign({}, state.rootSchema, _rootSchema) 38 | this.commit(types.$ROOT_SCHEMA_FLAT, { rootSchema: state.rootSchema }) 39 | 40 | // 初始化必要的api数据 41 | const usedDictAPI = {} 42 | for (const key in state.flatSchemas) { 43 | const opt = state.flatSchemas[key].option || {} 44 | if ( 45 | opt.type === 'dict' && 46 | opt.dict && 47 | opt.dict.dict && 48 | opt.dict.type === 'dict' && 49 | opt.dict.dictAPI 50 | ) { 51 | const dictName = opt.dict.dict 52 | if (usedDictAPI[dictName]) { 53 | if (usedDictAPI[dictName].indexOf(opt.dict.dictAPI) === -1) { 54 | usedDictAPI[dictName].push(opt.dict.dictAPI) 55 | } 56 | } else { 57 | usedDictAPI[dictName] = [opt.dict.dictAPI] 58 | } 59 | } 60 | } 61 | 62 | // 初始化schema store 63 | const schemaStore = jsonClone(rootSchema.store || {}) 64 | schemaStore.current = { type: '', dict: {}, api: {} } 65 | schemaStore.apis = (schemaStore.apis || []).map(api => { 66 | const ins = new API(api) 67 | ins.getData() 68 | return ins 69 | }) 70 | schemaStore.dicts = (schemaStore.dicts || []).map(dict => { 71 | const ins = new Dict(dict) 72 | ins.getData().then(() => { 73 | if (!isArray(usedDictAPI[ins.name])) return 74 | usedDictAPI[ins.name].forEach(dictAPI => { 75 | for (let i = 0; i < ins.data.length; i++) { 76 | const item = ins.data[i] 77 | if (item.name !== dictAPI) continue 78 | const apiIns = item instanceof API ? item : new API(item) 79 | apiIns.getData() 80 | ins.data.splice(i, 1, apiIns) 81 | } 82 | }) 83 | }) 84 | return ins 85 | }) 86 | 87 | state.store = schemaStore 88 | 89 | this.commit(types.$RULE_INIT, { rootSchema: state.rootSchema }) 90 | 91 | // 遍历schema获取model默认值 92 | for (const i in state.flatSchemas) { 93 | const schema = state.flatSchemas[i] 94 | // 特殊默认值需要处理,尤其动态时间值 95 | defaultModel[i] = cleanDefaultValue(schema) 96 | if (!(i in state.model)) { 97 | // 容器widget不保留model值 98 | if (schema.container) { 99 | continue 100 | } 101 | const type = getWidgetType(flatWidgets, schema.widget) 102 | const widgetModel = getWidgetModel(type, schema, typeBuilder) // 缺省值 103 | 104 | Object.assign(model, widgetModel, defaultModel) 105 | } 106 | } 107 | this.commit(types.$MODEL_SET, { model }) 108 | this.commit(types.$WIDGET_DEFAULT_PROPS_UPDATE) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/store/StoreConf.js: -------------------------------------------------------------------------------- 1 | import types from './types' 2 | import { defaultSchema } from '../constant' 3 | import RootSchema from '../schema/RootSchema' 4 | import { 5 | isFunction, 6 | isArray 7 | } from '../helper' 8 | /* eslint no-unused-vars: 0 */ 9 | import Rule from '../rule' 10 | import mutations from './mutations' 11 | 12 | const rootSchema = new RootSchema() 13 | const selectedSchema = defaultSchema() 14 | 15 | export default class StoreConf { 16 | constructor (option) { 17 | const { Rule } = option || {} 18 | return { 19 | state: { 20 | // 设计模式下当前tab,可选 design | preview 21 | tab: '', 22 | mode: 'edit', 23 | selectedSchema, 24 | rootSchema, 25 | model: {}, 26 | widgets: [], 27 | // 拍平所有schema 28 | flatSchemas: {}, 29 | // 拍平所有规则 30 | flatRules: {}, 31 | // 运行时状态 32 | // { [key]: { hide: false, readonly: false }} 33 | defaults: {}, 34 | store: { 35 | baseURL: '', 36 | current: { 37 | // 当前 new | static | dynamic 38 | // type: 'static', 39 | type: '', // dict | api 表示当前选择的类型 40 | dict: { 41 | index: -1, // selected dict index 42 | value: {} 43 | }, 44 | api: { 45 | index: -1, // selected api index 46 | value: {} 47 | } 48 | }, 49 | dicts: [], 50 | apis: [] 51 | } 52 | }, 53 | getters: { 54 | isSelected: state => !!state.selectedSchema.key, 55 | flatWidgets: ({ widgets }) => { 56 | const serialized = {} 57 | if (!isArray(widgets)) { 58 | return serialized 59 | } 60 | widgets.forEach(item => { 61 | if (!isArray(item.widgets)) { 62 | return 63 | } 64 | item.widgets.forEach(w => { 65 | if (isFunction(w.Schema)) { 66 | serialized[w.Schema.widget] = w 67 | } 68 | }) 69 | }) 70 | return serialized 71 | }, 72 | 73 | settingWidget: (state, getters) => { 74 | const { widget: widgetName, key } = state.selectedSchema 75 | const widget = getters.flatWidgets[widgetName] || null 76 | 77 | if (!key || !widget || !widget.Setting) return null 78 | 79 | return isArray(widget.Setting) 80 | ? [...widget.Setting] 81 | : { ...widget.Setting } 82 | }, 83 | 84 | // widget schema 声明需要哪些规则validator 85 | widgetsValidators: (state, { flatWidgets }) => { 86 | const map = {} 87 | for (const i in flatWidgets) { 88 | const { Schema } = flatWidgets[i] 89 | if (!isFunction(Schema) || !isArray(Schema.validators)) { 90 | continue 91 | } 92 | map[i] = [] 93 | Schema.validators.forEach(validator => { 94 | const val = Rule.rules[validator] 95 | 96 | if (!val) { 97 | return console.log(`validator ${validator} is not exist`) 98 | } 99 | map[i].push(val) 100 | }) 101 | } 102 | return map 103 | } 104 | }, 105 | mutations: { 106 | [types.$TAB_UPDATE] (state, { tab }) { 107 | if (tab && typeof tab === 'string') { 108 | state.tab = tab 109 | } 110 | }, 111 | 112 | // 添加表单展示模式 113 | [types.$MODE_CHANGE] (state, { mode }) { 114 | state.mode = mode 115 | }, 116 | 117 | ...mutations.model, 118 | ...mutations.schema, 119 | ...mutations.widget, 120 | ...mutations.rule, 121 | ...mutations.logic, 122 | ...mutations.store, 123 | ...mutations.style 124 | }, 125 | actions: {}, 126 | modules: {} 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/logic/Logic.js: -------------------------------------------------------------------------------- 1 | import EventLogic from './EventLogic' 2 | import ValueLogic from './ValueLogic' 3 | 4 | export default class Logic { 5 | constructor (defaults) { 6 | this.defaults = defaults || {} 7 | this.map = { 8 | event: new EventLogic(), 9 | value: new ValueLogic() 10 | } 11 | } 12 | 13 | /** 14 | * compare the difference between logic value and model value 15 | * @param {Array} valueLogics list for value logics 16 | * @param {Object} model form data 17 | */ 18 | diffValueLogics (valueLogics, model, valueTypes) { 19 | const patches = [] 20 | const scripts = [] 21 | 22 | for (let j = 0; j < valueLogics.length; j++) { 23 | const logic = valueLogics[j] 24 | const valueType = valueTypes[logic.key] 25 | const valueValidator = this.map.value.map[logic.action] 26 | if (!valueValidator) continue 27 | 28 | const validation = valueValidator.validator(model[logic.key], logic.value, { logic, valueType }) 29 | 30 | if (!validation) continue 31 | // should be the same key 32 | if (!(logic.key in model)) continue 33 | 34 | /* eslint no-case-declarations: 0 */ 35 | switch (logic.trigger) { 36 | case 'script': 37 | logic.script && scripts.push(logic.script) 38 | break 39 | case 'prop': 40 | const patch = {} 41 | 42 | for (let k = 0; k < logic.effects.length; k++) { 43 | const effect = logic.effects[k] 44 | const props = {} 45 | 46 | if (!this.checkEffect(effect, logic.key)) continue 47 | 48 | effect.properties.forEach(_ => { 49 | props[_.key] = _.value 50 | }) 51 | patch[effect.key] = props 52 | } 53 | Object.keys(patch).length && patches.push(patch) 54 | break 55 | default: 56 | break 57 | } 58 | } 59 | return { patches, scripts } 60 | } 61 | 62 | /** 63 | * compare the difference between logic event and real event 64 | * @param {Array} eventLogics list for value logics 65 | * @param {String} eventType event type like on-click. should start with on- 66 | */ 67 | diffEventLogics (eventLogics, eventType) { 68 | const patches = [] 69 | const scripts = [] 70 | 71 | for (let i = 0; i < eventLogics.length; i++) { 72 | const logic = eventLogics[i] 73 | 74 | /* eslint no-case-declarations: 0 */ 75 | switch (logic.trigger) { 76 | case 'script': 77 | logic.script && scripts.push(logic.script) 78 | break 79 | case 'prop': 80 | const patch = {} 81 | // get event type: on-click => click 82 | if (logic.action !== eventType.slice(3)) continue 83 | 84 | for (let j = 0; j < logic.effects.length; j++) { 85 | const effect = logic.effects[j] 86 | const props = {} 87 | 88 | if (!this.checkEffect(effect, logic.key)) continue 89 | 90 | effect.properties.forEach(_ => { 91 | props[_.key] = _.value 92 | }) 93 | patch[effect.key] = props 94 | } 95 | 96 | Object.keys(patch).length && patches.push(patch) 97 | break 98 | default: 99 | break 100 | } 101 | } 102 | 103 | return { patches, scripts } 104 | } 105 | 106 | /** 107 | * apply properties to schema 108 | * @param {Object} flatSchemas flated schema like { 'ksddf': { widget: 'input', ...}} 109 | * @param {Array} patches properties that should be updated 110 | */ 111 | applyPatches (flatSchemas, patches = [], controlledKeys) { 112 | const propsMerged = {} 113 | patches.forEach(patch => { 114 | for (const key in patch) { 115 | if (!(key in flatSchemas)) continue 116 | propsMerged[key] = propsMerged[key] || {} 117 | Object.assign(propsMerged[key], patch[key]) 118 | } 119 | }) 120 | const controlledDefaults = {} 121 | controlledKeys.forEach(key => { 122 | if (!(key in this.defaults)) return 123 | controlledDefaults[key] = this.defaults[key] 124 | }) 125 | const result = Object.assign({}, controlledDefaults, propsMerged) 126 | for (const key in result) { 127 | Object.assign(flatSchemas[key], controlledDefaults[key], result[key]) 128 | } 129 | } 130 | 131 | /** 132 | * check the validity of the effect 133 | * @param {Object} effect affected properties 134 | * @param {String} key schema key 135 | */ 136 | checkEffect (effect, key) { 137 | return effect.key && key !== effect.key && Array.isArray(effect.properties) 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/store/mutations/store.js: -------------------------------------------------------------------------------- 1 | import types from '../types' 2 | import Dict from '../Dict' 3 | import API from '../API' 4 | import { 5 | jsonClone, 6 | storeFetchClean 7 | } from '../../helper' 8 | 9 | export default { 10 | // 选中STORE中dict 11 | [types.$STORE_DICT_SELECT] ({ store }, { dict, index, action }) { 12 | store.current.dict.value = jsonClone(dict) 13 | store.current.dict.index = index 14 | store.current.dict.action = action 15 | store.current.type = 'dict' 16 | store.current.api = {} 17 | }, 18 | 19 | // 更新dict 20 | [types.$STORE_DICT_UPDATE] ({ store, rootSchema }, { dict, index }) { 21 | const instance = new Dict(jsonClone(dict)) 22 | instance.getData() 23 | store.dicts.splice(index, 1, instance) 24 | rootSchema.store.dicts = storeFetchClean(store.dicts) 25 | }, 26 | 27 | // 添加dict 28 | [types.$STORE_DICT_ADD] ({ store, rootSchema }, { dict }) { 29 | const matched = store.dicts.filter(item => item.name === dict.name) 30 | if (matched.length === 0) { 31 | const instance = new Dict(jsonClone(dict)) 32 | instance.getData() 33 | store.dicts.push(instance) 34 | store.current.dict.index = store.dicts.length - 1 35 | store.current.dict.action = 'update' 36 | rootSchema.store.dicts = storeFetchClean(store.dicts) 37 | } 38 | }, 39 | 40 | // 更新dict 41 | [types.$STORE_DICT_DELETE] ({ store, rootSchema }, { index }) { 42 | const currentIndex = store.current.dict.index 43 | if (currentIndex === index) { 44 | store.current.type = '' 45 | store.current.dict = { 46 | index: -1, 47 | value: {}, 48 | action: '' 49 | } 50 | } else if (currentIndex > index) { 51 | store.current.dict.index-- 52 | } 53 | store.dicts.splice(index, 1) 54 | rootSchema.store.dicts = storeFetchClean(store.dicts) 55 | }, 56 | // 复制dict 57 | [types.$STORE_DICT_COPY] ({ store, rootSchema }, { index }) { 58 | const dict = jsonClone(store.dicts[index] || {}) 59 | do { 60 | dict.name += '_copy' 61 | } while (store.dicts.filter(item => item.name === dict.name).length > 0) 62 | 63 | const newDict = new Dict(dict) 64 | newDict.getData() 65 | store.dicts.splice(index + 1, 0, newDict) 66 | rootSchema.store.dicts = storeFetchClean(store.dicts) 67 | }, 68 | // 选中STORE中api 69 | [types.$STORE_API_SELECT] ({ store }, { api, index, dictIndex }) { 70 | const action = dictIndex >= 0 71 | ? '' 72 | : ( 73 | index >= 0 74 | ? 'update' 75 | : 'create' 76 | ) 77 | store.current.api = { 78 | value: jsonClone(api), 79 | index, 80 | action 81 | } 82 | store.current.type = 'api' 83 | store.current.dict = { index: dictIndex } 84 | }, 85 | 86 | // 更新api 87 | [types.$STORE_API_UPDATE] ({ store, rootSchema }, { api, index }) { 88 | const instance = new API(jsonClone(api)) 89 | instance.getData() 90 | store.apis.splice(index, 1, instance) 91 | rootSchema.store.apis = storeFetchClean(store.apis) 92 | }, 93 | 94 | // 添加api 95 | [types.$STORE_API_ADD] ({ store, rootSchema }, { api }) { 96 | const matched = store.apis.filter(item => item.name === api.name) 97 | if (matched.length === 0) { 98 | const instance = new API(jsonClone(api)) 99 | instance.getData() 100 | store.apis.push(instance) 101 | store.current.api.index = store.apis.length - 1 102 | store.current.api.action = 'update' 103 | store.current.dict = { 104 | index: -1, 105 | value: {}, 106 | action: '' 107 | } 108 | rootSchema.store.apis = storeFetchClean(store.apis) 109 | } 110 | }, 111 | 112 | // 删除api 113 | [types.$STORE_API_DELETE] ({ store, rootSchema }, { index }) { 114 | const currentIndex = store.current.api.index 115 | if (currentIndex === index) { 116 | store.current.type = '' 117 | store.current.api = { 118 | index: -1, 119 | value: {}, 120 | action: '' 121 | } 122 | } else if (currentIndex > index) { 123 | store.current.api.index-- 124 | } 125 | store.apis.splice(index, 1) 126 | rootSchema.store.apis = storeFetchClean(store.apis) 127 | }, 128 | // 复制api 129 | [types.$STORE_API_COPY] ({ store, rootSchema }, { index }) { 130 | const api = jsonClone(store.apis[index] || {}) 131 | do { 132 | api.name += '_copy' 133 | } while (store.apis.filter(item => item.name === api.name).length > 0) 134 | 135 | const newAPI = new API(api) 136 | newAPI.getData() 137 | store.apis.splice(index + 1, 0, newAPI) 138 | rootSchema.store.apis = storeFetchClean(store.apis) 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/store/Fetch.js: -------------------------------------------------------------------------------- 1 | const defaultKV = () => [{ key: '', value: '', description: '' }] 2 | export default class Fetch { 3 | constructor (props) { 4 | const { 5 | name = '', 6 | desc = '', 7 | header = defaultKV(), 8 | body = '{}', 9 | query = defaultKV(), 10 | params = defaultKV(), 11 | url = '', 12 | method = 'GET', 13 | adapter = '' 14 | } = props || {} 15 | 16 | this.name = name 17 | this.desc = desc 18 | this.header = header 19 | this.body = body 20 | this.query = query 21 | this.params = params 22 | // /:userId/test 其中userId为 form name 23 | this.url = url 24 | this.method = method 25 | this.adapter = adapter 26 | this.source = [] 27 | this.data = [] 28 | this.format = 'array' 29 | this.response = { 30 | header: [] 31 | } 32 | } 33 | 34 | getData () { 35 | const { url, body, header = [], method, adapter } = this 36 | if (!url) return Promise.reject(new Error('invalid url!')) 37 | 38 | const urlObj = this.resolveURL(url, this.params) 39 | if (!urlObj.valid) return Promise.reject(urlObj.message) 40 | 41 | let queryString = this.joinQuery() 42 | let resolvedURL = urlObj.url 43 | const headers = {} 44 | header 45 | .filter(h => h.key && h.value) 46 | .forEach(h => { headers[h.key] = h.value }) 47 | 48 | const option = { headers, method } 49 | const METHOD = method.toUpperCase() 50 | const withBodyMethods = ['POST', 'PUT'] 51 | 52 | if (withBodyMethods.indexOf(METHOD) > -1) { 53 | option.body = body 54 | } 55 | if (urlObj.widthQuery) { 56 | resolvedURL += queryString 57 | } else { 58 | queryString = queryString.slice(1) 59 | resolvedURL += `?${queryString}` 60 | } 61 | 62 | return fetch(resolvedURL, option).then(res => { 63 | const header = [] 64 | res.headers.forEach((value, key) => header.push({ key, value })) 65 | this.response.header = header 66 | /* eslint no-useless-catch: 0 */ 67 | try { 68 | return res.json() 69 | } catch (err) { 70 | throw err 71 | } 72 | }).then(res => { 73 | const _adapter = res.adapter || adapter 74 | /* eslint no-new-func: 0 */ 75 | const data = _adapter ? new Function('data', _adapter)(res) : res 76 | this.source = res 77 | this.data = data 78 | this.format = this.getFormat(data) 79 | return res 80 | }) 81 | } 82 | 83 | resolveURL (url = '', model = []) { 84 | const names = (url.match(/\/:[^:\s/?&]+/g) || []).map(_ => _.split(':')[1]) 85 | const result = { 86 | valid: true, 87 | message: '', 88 | widthQuery: false, 89 | url 90 | } 91 | const isValidURL = names.filter(name => { 92 | const tmp = model.filter(m => m.key === name)[0] 93 | if (!tmp) return false 94 | 95 | const type = typeof tmp.value 96 | 97 | return tmp.value !== '' && (type === 'string' || type === 'number' || type === 'boolean') 98 | }).length === names.length 99 | 100 | // let isValidURL = names.filter(name => { 101 | // const type = typeof model[name] 102 | // return name in model 103 | // && (type === 'string' || type === 'number' || type === 'boolean') 104 | // && model[name] !== '' 105 | // }).length === names.length 106 | 107 | if (!isValidURL) { 108 | result.valid = false 109 | result.message = 'invalid url params!' 110 | return result 111 | } 112 | names.forEach(name => { 113 | const tmp = model.filter(m => m.key === name)[0] 114 | if (!tmp) return 115 | /* eslint no-useless-escape: 0 */ 116 | result.url = result.url.replace(new RegExp(`\B?:${name}\B?`, 'g'), tmp.value) 117 | }) 118 | result.url = result.url.replace(/(&|\?)+$/, '') 119 | result.widthQuery = !!(result.url.indexOf('?') > -1) 120 | return result 121 | } 122 | 123 | joinQuery () { 124 | return (this.query || []) 125 | .filter(q => q.key && q.value) 126 | .map(q => `&${q.key}=${q.value}`) 127 | .join('') 128 | } 129 | 130 | getFormat (data, num = 2, loop = 1) { 131 | const type = Object.prototype.toString.call(data).split(' ')[1].split(']')[0].toLowerCase() 132 | let format = '' 133 | const space = new Array(num * loop + 1).join(' ') 134 | 135 | /* eslint no-case-declarations: 0 */ 136 | switch (type) { 137 | case 'number': 138 | case 'boolean': 139 | case 'string': 140 | format = type 141 | break 142 | case 'object': 143 | format = type 144 | const keys = Object.keys(data) 145 | if (keys.length) { 146 | format += '{\n' 147 | const len = keys.length 148 | loop++ 149 | keys.forEach((k, index) => { 150 | const sub = this.getFormat(data[k], num, loop) 151 | const isLast = index === len - 1 152 | const indentSpace = space.substr(0, space.length - 2) 153 | format += `${space}${k}: ${sub}` 154 | format += isLast ? `\n${indentSpace}}` : ',\n' 155 | }) 156 | } 157 | break 158 | case 'array': 159 | format = type 160 | const sub = this.getFormat(data[0], num, loop) 161 | if (sub !== 'undefined') { 162 | format = `${format}<${sub}>` 163 | } 164 | break 165 | default: 166 | break 167 | } 168 | return format 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/logic/ValueLogic.js: -------------------------------------------------------------------------------- 1 | import Effect from './Effect' 2 | 3 | export default class ValueLogic { 4 | constructor () { 5 | const booleanMap = { true: true, false: false } 6 | function isSameArray (arr1, arr2, matchedType) { 7 | if (!Array.isArray(arr1)) return false 8 | if (!Array.isArray(arr2)) return false 9 | const _arr1 = arr1.map(v => matchedType === 'number' ? parseFloat(v) : v) 10 | const _arr2 = arr2.map(v => matchedType === 'number' ? parseFloat(v) : v) 11 | 12 | if (_arr1.length !== _arr2.length) return false 13 | 14 | let result = true 15 | for (let i = 0; i < _arr1.length; i++) { 16 | if (_arr1[i] !== _arr2[i]) { 17 | result = false 18 | break 19 | } 20 | } 21 | return result 22 | } 23 | const resolveArrayType = type => { 24 | const matched = (type || '').match(/^array(?:<(.+)>)?/) 25 | if (matched && matched[1]) { 26 | return matched[1] 27 | } 28 | } 29 | const resolveValue = (value, type) => { 30 | return (value || '') 31 | .split(',') 32 | .map(v => v.trim()) 33 | .filter(_ => _) 34 | .map(v => { 35 | if (type === 'number') { 36 | v = parseFloat(v) 37 | } else if (type === 'boolean') { 38 | v = booleanMap[v] 39 | } 40 | return v 41 | }) 42 | } 43 | const include = (arr = [], v) => arr.indexOf(v) > -1 44 | const relationFn = { 45 | or: (arr1, arr2, action) => { 46 | let result = false 47 | for (let i = 0; i < arr2.length; i++) { 48 | // 包含 49 | if (action === '<>') { 50 | if (include(arr1, arr2[i])) { 51 | result = true 52 | break 53 | } 54 | // 不包含 55 | } else if (action === '><') { 56 | if (!include(arr1, arr2[i])) { 57 | result = true 58 | break 59 | } 60 | } 61 | } 62 | return result 63 | }, 64 | and: (arr1, arr2, action) => { 65 | let result = true 66 | for (let i = 0; i < arr2.length; i++) { 67 | // 包含 68 | if (action === '<>') { 69 | if (!include(arr1, arr2[i])) { 70 | result = false 71 | break 72 | } 73 | // 不包含 74 | } else if (action === '><') { 75 | if (include(arr1, arr2[i])) { 76 | result = false 77 | break 78 | } 79 | } 80 | } 81 | return result 82 | } 83 | } 84 | this.type = 'value' 85 | this.title = '逻辑' 86 | this.map = { 87 | '=': { 88 | key: '=', 89 | value: '等于', 90 | validator: (left, right, { valueType }) => { 91 | let leftValue = left 92 | let rightValue = right 93 | const matchedType = resolveArrayType(valueType) 94 | 95 | if (valueType === 'number') { 96 | leftValue = parseFloat(left) 97 | rightValue = parseFloat(right) 98 | 99 | return (isNaN(leftValue) || isNaN(leftValue)) ? false : leftValue === rightValue 100 | } else if (valueType === 'boolean') { 101 | if (right in booleanMap) { 102 | rightValue = booleanMap[right] 103 | } 104 | } else if (matchedType) { 105 | const values = resolveValue(right, matchedType) 106 | 107 | return isSameArray(left, values, matchedType) 108 | } 109 | return leftValue === rightValue 110 | } 111 | }, 112 | '!=': { 113 | key: '!=', 114 | value: '不等于', 115 | validator: (left, right, { valueType }) => { 116 | let leftValue = left 117 | let rightValue = right 118 | const matchedType = resolveArrayType(valueType) 119 | 120 | if (valueType === 'number') { 121 | leftValue = parseFloat(left) 122 | rightValue = parseFloat(right) 123 | return (isNaN(leftValue) || isNaN(leftValue)) ? true : leftValue !== rightValue 124 | } else if (valueType === 'boolean') { 125 | if (right in booleanMap) { 126 | rightValue = booleanMap[right] 127 | } 128 | } else if (matchedType) { 129 | const values = resolveValue(right, matchedType) 130 | 131 | return !isSameArray(left, values, matchedType) 132 | } 133 | return leftValue !== rightValue 134 | } 135 | }, 136 | '>': { 137 | key: '>', 138 | value: '大于', 139 | validator: (left, right) => { 140 | const leftValue = parseFloat(left) 141 | const rightValue = parseFloat(right) 142 | 143 | return (isNaN(leftValue) || isNaN(leftValue)) ? false : leftValue > rightValue 144 | } 145 | }, 146 | '<': { 147 | key: '<', 148 | value: '小于', 149 | validator: (left, right) => { 150 | const leftValue = parseFloat(left) 151 | const rightValue = parseFloat(right) 152 | 153 | return (isNaN(leftValue) || isNaN(leftValue)) ? false : leftValue < rightValue 154 | } 155 | }, 156 | '<>': { 157 | key: '<>', 158 | value: '包含', 159 | validator: (left, right, { logic, valueType }) => { 160 | let result = false 161 | const matchedType = resolveArrayType(valueType) 162 | const { relation } = logic 163 | const values = resolveValue(right, matchedType) 164 | 165 | if (relation in relationFn) { 166 | result = relationFn[relation](left, values, '<>') 167 | } 168 | return result 169 | } 170 | }, 171 | '><': { 172 | key: '><', 173 | value: '不包含', 174 | validator: (left, right, { logic, valueType }) => { 175 | let result = false 176 | const matchedType = resolveArrayType(valueType) 177 | const { relation } = logic 178 | const values = resolveValue(right, matchedType) 179 | 180 | if (relation in relationFn) { 181 | result = relationFn[relation](left, values, '><') 182 | } 183 | return result 184 | } 185 | } 186 | } 187 | } 188 | 189 | get () { 190 | return { 191 | type: 'value', 192 | key: '', 193 | action: '', 194 | value: '', 195 | relation: 'or', // or | and 或关系 | 且关系 196 | trigger: 'prop', // prop | script 197 | script: '', 198 | effects: [new Effect()] 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/worker/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 创建worker 3 | * 4 | * 一、用途: 5 | * - 处理用户输入的脚本 6 | * - 避免用户恶意获取window或document对象数据,增加安全性 7 | * - 避免用户输入死循环等错误逻辑阻塞主线程执行 8 | * 9 | * 二、用法: 10 | * ``` 11 | * let worker = new EpWorker() 12 | * worker.onmessage = function (e) { 13 | * const { 14 | * message, // 错误消息 15 | * success, // 是否执行成功 布尔类型 16 | * data // 处理后返回的数据 17 | * } = e.data 18 | * } 19 | * worker.postMessage({ 20 | * action, // 需要worker处理的类型,可选值:'fetch',其对应fn字段使用的变量为 `data` 21 | * data, // 发送给进程的数据,可以是任意类型 22 | * fn // 处理后返回的数据,可以执行的字符串函数,不包含`function`关键字(同new Function最后一个参数),需要 return处理后的数据 23 | * } 24 | * ``` 25 | */ 26 | export default class EpWorker { 27 | constructor () { 28 | return this.createWorker() 29 | } 30 | 31 | createWorker () { 32 | const blob = new Blob(['(' + this.createFunction().toString() + ')()']) 33 | const url = window.URL.createObjectURL(blob) 34 | return new Worker(url) 35 | } 36 | 37 | createFunction () { 38 | return function () { 39 | // 对注入参数进行转化 40 | self.util = { 41 | convert2kv (data, fn, children) { 42 | if (!children) { 43 | return data.map(fn) 44 | } else { 45 | return __recursive(data, fn) 46 | } 47 | function isArray (arr) { 48 | return Array.isArray(arr) 49 | } 50 | // 递归转换数据 51 | function __recursive (data, fn) { 52 | if (!isArray(data)) return [] 53 | return data.map(function (item) { 54 | const newItem = Object.assign({}, fn(item)) 55 | const rawChildren = isArray(item[children]) ? item[children] : [] 56 | newItem.children = __recursive(rawChildren, fn) 57 | return newItem 58 | }) 59 | } 60 | } 61 | } 62 | function isObj (arg) { 63 | return Object.prototype.toString.call(arg) === '[object Object]' 64 | } 65 | 66 | function t (e, r, o) { 67 | return ((function () { 68 | if (typeof Reflect === 'undefined' || !Reflect.construct) { 69 | return false 70 | } 71 | if (Reflect.construct.sham) { 72 | return false 73 | } 74 | if (typeof Proxy === 'function') { 75 | return true 76 | } 77 | try { 78 | Date.prototype.toString.call(Reflect.construct(Date, [], function () {})) 79 | return true 80 | } catch (e) { 81 | return false 82 | } 83 | }()) ? Reflect.construct : function (e, t, r) { 84 | var o = [null] 85 | o.push.apply(o, t) 86 | var C = Function.bind.apply(e, o) 87 | var u = new C() 88 | // r && n(u, r.prototype) 89 | return u 90 | }).apply(null, arguments) 91 | } 92 | function r (e) { 93 | return (function (e) { 94 | if (Array.isArray(e)) { 95 | for (var t = 0, n = new Array(e.length); t < e.length; t++) { 96 | n[t] = e[t] 97 | } 98 | return n 99 | } 100 | }(e)) || (function (e) { 101 | if (Symbol.iterator in Object(e) || Object.prototype.toString.call(e) === '[object Arguments]') { 102 | return Array.from(e) 103 | } 104 | }(e)) || (function () { 105 | throw new TypeError('Invalid attempt to spread non-iterable instance') 106 | }()) 107 | } 108 | 109 | /** 110 | * 检查转换后返回值是否含有kv属性的对象列表 111 | * @param {*} data 112 | * @returns {success: Boolean, message: String} 返回值 113 | */ 114 | function checkKVList (data) { 115 | let success = true 116 | let message = '' 117 | 118 | if (Array.isArray(data)) { 119 | for (let i = 0; i < data.length; i++) { 120 | if (!isObj(data[i]) || !('key' in data[i]) || !('value' in data[i])) { 121 | success = false 122 | message = `${data} 在索引 ${i} 不符合格式规范,请转换成[{key: '', value: ''}, ...] 形式并返回` 123 | break 124 | } 125 | } 126 | } else { 127 | success = false 128 | message = '需要返回数组' 129 | } 130 | return { success, message } 131 | } 132 | 133 | function checkTableList (data) { 134 | function isAvailableNumber (n) { 135 | return typeof n === 'number' && n >= 0 136 | } 137 | const pageErrorMsg = 'page 格式不符合规范,应为: { current: Number, size: Number, total: Number }' 138 | if (isObj(data)) { 139 | // check page 140 | const { current, size, total } = data.page || {} 141 | if (!isObj(data.page) || 142 | !isAvailableNumber(current) || 143 | !isAvailableNumber(size) || 144 | !isAvailableNumber(total) 145 | ) { 146 | return { 147 | success: false, 148 | message: pageErrorMsg 149 | } 150 | } 151 | // check data 152 | const innerData = data.data || [] 153 | for (let i = 0; i < innerData.length; i++) { 154 | if (!isObj(innerData[i])) { 155 | return { 156 | success: false, 157 | message: innerData[i] + ' 不符合格式规范' 158 | } 159 | } 160 | } 161 | } else { 162 | return { 163 | success: false, 164 | message: '需要返回对象{ page: {}, data: []}' 165 | } 166 | } 167 | return { 168 | success: true, 169 | message: '' 170 | } 171 | } 172 | 173 | // 可以后续扩展 action 及参数、检查函数 174 | const actionMap = { 175 | fetch: { 176 | args: ['data'], 177 | check: checkKVList 178 | }, 179 | tableFetch: { 180 | args: ['data'], 181 | check: checkTableList 182 | }, 183 | custom: { 184 | args: ['data'], 185 | check: function () { 186 | return { 187 | success: true, 188 | message: '' 189 | } 190 | } 191 | } 192 | } 193 | 194 | // 监听接受到的消息 195 | self.onmessage = function (e) { 196 | let result = { success: true, message: '' } 197 | const { fn, action, data } = e.data 198 | let fun = res => res 199 | 200 | if (!(action in actionMap)) { 201 | result = { 202 | success: false, 203 | /* eslint no-template-curly-in-string: 0 */ 204 | message: 'action: ' + action + ' 无法识别,只能为以下其中一种action: \n${Object.keys(actionMap).toString()}' 205 | } 206 | return self.postMessage(result) 207 | } 208 | if (!fn) { 209 | result = { 210 | success: false, 211 | message: '请传入 fn 参数' 212 | } 213 | return self.postMessage(result) 214 | } 215 | 216 | try { 217 | /* eslint-disable no-new-func */ 218 | // fun = new Function(...actionMap[action].args, fn) 219 | fun = t(Function, r(actionMap[action].args).concat([fn])) 220 | } catch (e) { 221 | result = { 222 | success: false, 223 | message: e 224 | } 225 | return self.postMessage(result) 226 | } 227 | const _data = fun(data) 228 | result = actionMap[action].check(_data) 229 | if (result.success) { 230 | const newData = { 231 | success: result.success, 232 | data: _data 233 | } 234 | self.postMessage(newData) 235 | } else { 236 | return self.postMessage(result) 237 | } 238 | } 239 | } 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /src/helper/util.js: -------------------------------------------------------------------------------- 1 | 2 | export function isFunction (value) { 3 | return typeof value === 'function' 4 | } 5 | 6 | export function isString (value) { 7 | return typeof value === 'string' 8 | } 9 | 10 | export function isNumber (value) { 11 | return typeof value === 'number' 12 | } 13 | 14 | export function isArray (value) { 15 | return Array.isArray(value) 16 | } 17 | 18 | export function jsonClone (value) { 19 | return JSON.parse(JSON.stringify(value)) 20 | } 21 | 22 | export function include (list, i) { 23 | return list.indexOf(i) !== -1 24 | } 25 | 26 | /** 27 | * get primitive value type 28 | * @param {any} value shoule be checked 29 | * @return {Boolean} 30 | */ 31 | export function getValueType (value) { 32 | return ({}).toString.call(value).slice(8).split(']')[0].toLowerCase() 33 | } 34 | 35 | /** 36 | * check if array value type is as expected 37 | * @param {Array} value 38 | * @param {String} expectedType Schema.type 39 | */ 40 | function checkValueTypeWithStringExpectedType (value, expectedType) { 41 | const matched = expectedType.match(/array<([^<>]+)>/) 42 | if (expectedType === 'array') { 43 | return true 44 | } 45 | if (!matched || !matched[1]) { 46 | return false 47 | } 48 | const innerType = matched[1] 49 | 50 | return value.filter(v => getValueType(v) === innerType).length === value.length 51 | } 52 | /** 53 | * 判断value参数值类型是否符合预期expectedType 54 | * @param {any} value 待判断值 55 | * @param {String|Number|Boolean|Undefined|Array} expectedType 期望的value值类型 56 | * @param {Boolean} dynamic 是否允许动态添加widget,由schema.dynamic决定,为true时,value必须为数组类型 57 | * @returns {Boolean} 58 | */ 59 | export function checkValueType (value, expectedType, dynamic) { 60 | const type = getValueType(value) 61 | function checkExpectedType (expectedType, type) { 62 | const map = { 63 | string: ['json'], 64 | number: ['json'], 65 | boolean: ['json'], 66 | object: ['json'], 67 | array: ['json'], 68 | null: ['json'], 69 | undefined: [] 70 | } 71 | if (isArray(expectedType)) { 72 | return include(expectedType, type) || !!expectedType.filter(t => include(map[type], t)).length 73 | } else { 74 | return type === expectedType || include(map[type], expectedType) 75 | } 76 | } 77 | if (dynamic) { 78 | if (type !== 'array') { 79 | return false 80 | } 81 | return value.filter(v => checkValueType(v, expectedType, false)).length === value.length 82 | } else { 83 | if (type === 'array') { 84 | if (isArray(expectedType)) { 85 | return !!expectedType.filter(t => checkValueTypeWithStringExpectedType(value, t)).length 86 | } else { 87 | return checkValueTypeWithStringExpectedType(value, expectedType) 88 | } 89 | } else { 90 | return checkExpectedType(expectedType, type) 91 | } 92 | } 93 | } 94 | /** 95 | * 生成指定长度字符串长度,大小写字母及数字 96 | * @param {Number} len 字符串长度 97 | */ 98 | export function randomStr (len = 8) { 99 | let result = '' 100 | const char = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' 101 | for (var i = 0; i < len; i++) { 102 | result += char.charAt(Math.floor(Math.random() * char.length)) 103 | } 104 | return 'k' + result 105 | } 106 | 107 | /** 108 | * 判断一个变量是否为原生对象 109 | * @param {any} value 待判断参数 110 | */ 111 | export function isPlainObject (value) { 112 | if (!value || typeof value !== 'object' || ({}).toString.call(value) !== '[object Object]') { 113 | return false 114 | } 115 | const proto = Object.getPrototypeOf(value) 116 | if (proto === null) { 117 | return true 118 | } 119 | const Ctor = hasOwnProperty.call(proto, 'constructor') && proto.constructor 120 | return typeof Ctor === 'function' 121 | } 122 | 123 | /** 124 | * 判断是否非空字符串 125 | * @param {String} value 待判断值 126 | */ 127 | export function isNotEmptyString (value) { 128 | if ( 129 | !isString(value) || 130 | !value.trim().length 131 | ) return false 132 | return true 133 | } 134 | 135 | /** 136 | * 判断是否为数字字符串 137 | * @param {String} value 待判断值 138 | */ 139 | export function isNumberString (value) { 140 | if (typeof value !== 'string') { 141 | return false 142 | } 143 | return /^[-+]?\d+(.\d+)?$/.test(value) 144 | } 145 | 146 | /** 147 | * 简单的http请求方法 148 | * @param {String} url 请求url 149 | * @param {Object} option 请求参数 150 | */ 151 | export function ajax (url, option) { 152 | let pro = fetch(url, option) 153 | .then(res => res.json()) 154 | 155 | if (option) { 156 | const { before } = option 157 | if (isFunction(before)) { 158 | pro = pro.then(before) 159 | } 160 | } 161 | return pro 162 | } 163 | 164 | /** 165 | * 对函数进行防抖 166 | * @param {function} func 防抖的函数 167 | * @param {number} wait 防抖的时间 168 | * @param {object} options 防抖参数 169 | * @param {boolean} options.leading 指定在延迟开始前调用 170 | * @param {boolean} options.trailing 指定在延迟结束后调用 171 | * @param {boolean} options.maxWait 允许被延迟的最大值 172 | * 173 | */ 174 | export function debounce (func, wait, options) { 175 | let lastArgs 176 | let lastThis 177 | let maxWait 178 | let result 179 | let timerId 180 | let lastCallTime 181 | let lastInvokeTime = 0 182 | let leading = false 183 | let maxing = false 184 | let trailing = true 185 | 186 | if (typeof func !== 'function') { 187 | throw new TypeError('Expected a function') 188 | } 189 | wait = parseInt(wait, 10) || 0 190 | if (isPlainObject(options)) { 191 | leading = !!options.leading 192 | maxing = 'maxWait' in options 193 | maxWait = maxing ? Math.max(parseInt(options.maxWait) || 0, wait) : maxWait 194 | trailing = 'trailing' in options ? !!options.trailing : trailing 195 | } 196 | 197 | function invokeFunc (time) { 198 | var args = lastArgs 199 | var thisArg = lastThis 200 | 201 | lastArgs = lastThis = undefined 202 | lastInvokeTime = time 203 | result = func.apply(thisArg, args) 204 | return result 205 | } 206 | 207 | function leadingEdge (time) { 208 | // Reset any `maxWait` timer. 209 | lastInvokeTime = time 210 | // Start the timer for the trailing edge. 211 | timerId = setTimeout(timerExpired, wait) 212 | // Invoke the leading edge. 213 | return leading ? invokeFunc(time) : result 214 | } 215 | 216 | function remainingWait (time) { 217 | var timeSinceLastCall = time - lastCallTime 218 | var timeSinceLastInvoke = time - lastInvokeTime 219 | var timeWaiting = wait - timeSinceLastCall 220 | 221 | return maxing 222 | ? Math.min(timeWaiting, maxWait - timeSinceLastInvoke) 223 | : timeWaiting 224 | } 225 | 226 | function shouldInvoke (time) { 227 | var timeSinceLastCall = time - lastCallTime 228 | var timeSinceLastInvoke = time - lastInvokeTime 229 | 230 | // Either this is the first call, activity has stopped and we're at the 231 | // trailing edge, the system time has gone backwards and we're treating 232 | // it as the trailing edge, or we've hit the `maxWait` limit. 233 | return (lastCallTime === undefined || (timeSinceLastCall >= wait) || 234 | (timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait)) 235 | } 236 | 237 | function timerExpired () { 238 | var time = Date.now() 239 | if (shouldInvoke(time)) { 240 | return trailingEdge(time) 241 | } 242 | // Restart the timer. 243 | timerId = setTimeout(timerExpired, remainingWait(time)) 244 | } 245 | 246 | function trailingEdge (time) { 247 | timerId = undefined 248 | 249 | // Only invoke if we have `lastArgs` which means `func` has been 250 | // debounced at least once. 251 | if (trailing && lastArgs) { 252 | return invokeFunc(time) 253 | } 254 | lastArgs = lastThis = undefined 255 | return result 256 | } 257 | 258 | function cancel () { 259 | if (timerId !== undefined) { 260 | clearTimeout(timerId) 261 | } 262 | lastInvokeTime = 0 263 | lastArgs = lastCallTime = lastThis = timerId = undefined 264 | } 265 | 266 | function flush () { 267 | return timerId === undefined ? result : trailingEdge(Date.now()()) 268 | } 269 | 270 | function debounced () { 271 | var time = Date.now() 272 | var isInvoking = shouldInvoke(time) 273 | 274 | lastArgs = arguments 275 | lastThis = this 276 | lastCallTime = time 277 | 278 | if (isInvoking) { 279 | if (timerId === undefined) { 280 | return leadingEdge(lastCallTime) 281 | } 282 | if (maxing) { 283 | // Handle invocations in a tight loop. 284 | clearTimeout(timerId) 285 | timerId = setTimeout(timerExpired, wait) 286 | return invokeFunc(lastCallTime) 287 | } 288 | } 289 | if (timerId === undefined) { 290 | timerId = setTimeout(timerExpired, wait) 291 | } 292 | return result 293 | } 294 | debounced.cancel = cancel 295 | debounced.flush = flush 296 | return debounced 297 | } 298 | 299 | export function formatDate (_date, _format) { 300 | const date = _date instanceof Date ? _date : new Date() 301 | let year = date.getFullYear() 302 | let month = date.getMonth() + 1 303 | let day = date.getDate() 304 | let minute = date.getMinutes() 305 | let hour = date.getHours() 306 | let second = date.getSeconds() 307 | const addPrefix = function (num) { 308 | const n = num + '' 309 | return n.length === 1 ? '0' + n : n 310 | } 311 | 312 | year = addPrefix(year) 313 | month = addPrefix(month) 314 | day = addPrefix(day) 315 | minute = addPrefix(minute) 316 | hour = addPrefix(hour) 317 | second = addPrefix(second) 318 | 319 | const format = _format || 'yyyy-MM-dd' 320 | return format 321 | .replace('yyyy', year) 322 | .replace('MM', month) 323 | .replace('dd', day) 324 | .replace('HH', hour) 325 | .replace('mm', minute) 326 | .replace('ss', second) 327 | } 328 | 329 | export function copy (str) { 330 | const input = document.createElement('textArea') 331 | input.setAttribute('style', 'position: absolute;z-index: -1;height: 0;width: 0;') 332 | document.body.appendChild(input) 333 | input.value = str 334 | input.select() 335 | const isCopied = document.execCommand('copy', 'false', null) 336 | 337 | return new Promise((resolve, reject) => { 338 | if (isCopied) { 339 | resolve(str) 340 | } else { 341 | reject(new Error('error')) 342 | } 343 | document.body.removeChild(input) 344 | }) 345 | } 346 | -------------------------------------------------------------------------------- /src/store/mutations/widget.js: -------------------------------------------------------------------------------- 1 | import types from '../types' 2 | import { defaultSchema, defaultProps } from '../../constant' 3 | import Logic from '../../logic' 4 | import Rule from '../../rule' 5 | import TypeBuilder from '../TypeBuilder' 6 | import { 7 | flattenSchema, 8 | isFunction, 9 | isArray, 10 | include, 11 | getParentListByKey, 12 | getIndexByKey, 13 | getRootSchemaChildren, 14 | getWidgetType, 15 | isNotEmptyString, 16 | getWidgetModel, 17 | updateRequiredRule, 18 | convertNameModelToKeyModel, 19 | getDefaults 20 | } from '../../helper' 21 | 22 | const typeBuilder = new TypeBuilder() 23 | 24 | export default { 25 | // 配置供展示的所有widget列表 26 | [types.$WIDGETS_SET] (state, { widgets }) { 27 | state.widgets = widgets 28 | }, 29 | 30 | // 更新所有widget默认属性,在new Render时调用 31 | [types.$WIDGET_DEFAULT_PROPS_UPDATE] (state) { 32 | state.defaults = getDefaults(state.flatSchemas, defaultProps()) 33 | }, 34 | 35 | // value logic改变影响 widget 属性改变 36 | [types.$WIDGET_UPDATE_BY_VALUE_LOGIC] (state, { model, callback }) { 37 | const { flatSchemas } = state 38 | const valueTypes = {} 39 | Object.keys(model || {}).forEach(k => { valueTypes[k] = flatSchemas[k].type }) 40 | const valueLogics = state.rootSchema.logics.filter(logic => logic.key && logic.type === 'value') 41 | const logic = new Logic(state.defaults) 42 | const { patches, scripts } = logic.diffValueLogics(valueLogics, model, valueTypes) 43 | const controlledKeys = [] 44 | 45 | for (const key in model) { 46 | valueLogics.filter(logic => logic.key === key).map(logic => { 47 | logic.effects.forEach(effect => { 48 | if (controlledKeys.indexOf(effect.key) === -1) { 49 | controlledKeys.push(effect.key) 50 | } 51 | }) 52 | }) 53 | } 54 | 55 | logic.applyPatches(state.flatSchemas, patches, controlledKeys) 56 | isFunction(callback) && callback(scripts) 57 | }, 58 | 59 | // event logic改变影响 widget 属性改变 60 | [types.$WIDGET_UPDATE_BY_EVENT_LOGIC] (state, { key, eventType, callback }) { 61 | // key 为当前触发的widget key 62 | const eventLogics = state.rootSchema.logics.filter(logic => logic.key === key && logic.type === 'event') 63 | const logic = new Logic(state.defaults) 64 | const { patches, scripts } = logic.diffEventLogics(eventLogics, eventType) 65 | const controlledKeys = [] 66 | eventLogics.forEach(logic => { 67 | if (logic.key === key) { 68 | logic.effects.forEach(effect => { 69 | if (controlledKeys.indexOf(effect.key) === -1) { 70 | controlledKeys.push(effect.key) 71 | } 72 | }) 73 | } 74 | }) 75 | 76 | logic.applyPatches(state.flatSchemas, patches, controlledKeys) 77 | isFunction(callback) && callback(scripts) 78 | }, 79 | 80 | // 设计模式向表单添加一个widget,schema为此widget对应的默认schema 81 | // 目前默认添加到整个表单最后 82 | [types.$WIDGET_ADD] (state, { widget, schema }) { 83 | const { selectedSchema, rootSchema, flatSchemas } = state 84 | const { flatWidgets, isSelected } = this.getters 85 | const wid = widget || schema.widget 86 | const WidgetSchema = flatWidgets[wid].Schema 87 | 88 | if (!isFunction(WidgetSchema)) { 89 | return console.error('Schema should be a constructor') 90 | } 91 | 92 | let childrenSchema = [] 93 | // 根据传入的完整schema添加,需额外配置args参数 94 | const args = (!widget && schema) ? { clone: true, schema, flatSchemas } : {} 95 | const newSchema = new WidgetSchema({ widgets: flatWidgets, ...args }) 96 | 97 | if (isSelected) { 98 | childrenSchema = getParentListByKey(selectedSchema.key, rootSchema) 99 | const index = getIndexByKey(selectedSchema.key, childrenSchema) 100 | childrenSchema.splice(index + 1, 0, newSchema) 101 | } else { 102 | childrenSchema = getRootSchemaChildren(rootSchema) 103 | childrenSchema.push(newSchema) 104 | } 105 | this.commit(types.$ROOT_SCHEMA_FLAT, { rootSchema: Object.assign({}, rootSchema) }) 106 | this.commit(types.$RULE_INIT, { rootSchema: Object.assign({}, rootSchema) }) 107 | 108 | const type = getWidgetType(flatWidgets, newSchema.widget) 109 | const model = getWidgetModel(type, newSchema, typeBuilder) 110 | this.commit(types.$MODEL_SET, { model }) 111 | state.selectedSchema = newSchema 112 | }, 113 | 114 | [types.$WIDGET_DYNAMIC_ADD] (state, { key }) { 115 | const widgets = this.getters.flatWidgets 116 | const { flatSchemas } = state 117 | const currentSchema = flatSchemas[key] 118 | const WidgetSchema = widgets[currentSchema.widget].Schema 119 | const schema = Object.assign({}, currentSchema, { list: [], dynamic: false }) 120 | const newSchema = new WidgetSchema({ schema, widgets, clone: true, dynamic: true }) 121 | // 动态添加的子schema不能为dynamic 122 | newSchema.dynamic = false 123 | const flatedSchema = flattenSchema(newSchema) 124 | const flatedRules = new Rule(newSchema).rules 125 | 126 | currentSchema.list.push(newSchema) 127 | const newSchemas = { 128 | [newSchema.key]: newSchema, 129 | ...flatedSchema 130 | } 131 | state.flatSchemas = Object.assign({}, state.flatSchemas, newSchemas) 132 | state.flatRules = Object.assign({}, state.flatRules, { ...flatedRules, [newSchema.key]: newSchema.rules }) 133 | 134 | // 初始默认值即可,获取表单数据通过方法组装嵌套格式 135 | const model = {} 136 | if (!isArray(state.model[key])) { 137 | model[key] = [] 138 | } 139 | // 新建的schema及可能的子孙schema 140 | for (const k in newSchemas) { 141 | const { type, key } = newSchemas[k] 142 | const builder = typeBuilder.types[type] 143 | if (type && isFunction(builder)) { 144 | model[key] = builder() 145 | } 146 | } 147 | state.model = Object.assign({}, state.model, model) 148 | }, 149 | 150 | [types.$WIDGET_DYNAMIC_REMOVE] (state, { key, index }) { 151 | const { flatSchemas, flatRules, model } = state 152 | const currentSchema = flatSchemas[key] 153 | const removedSchema = currentSchema.list.splice(index, 1)[0] 154 | const { key: skey } = removedSchema 155 | 156 | delete flatSchemas[skey] 157 | delete flatRules[skey] 158 | delete model[skey] 159 | state.flatSchemas = Object.assign({}, flatSchemas) 160 | state.flatRules = Object.assign({}, flatRules) 161 | state.model = Object.assign({}, model) 162 | }, 163 | 164 | // 设计模式复制一个widget 165 | [types.$WIDGET_COPY] (state, { key }) { 166 | const { flatSchemas, rootSchema } = state 167 | const schema = flatSchemas[key] 168 | const { flatWidgets } = this.getters 169 | const WidgetSchema = flatWidgets[schema.widget].Schema 170 | const parentList = getParentListByKey(key, rootSchema) 171 | const index = getIndexByKey(key, parentList) 172 | const newSchema = new WidgetSchema({ schema, widgets: flatWidgets, clone: true, flatSchemas }) 173 | 174 | parentList.splice(index + 1, 0, newSchema) 175 | this.commit(types.$ROOT_SCHEMA_FLAT, { rootSchema: Object.assign({}, rootSchema) }) 176 | this.commit(types.$RULE_INIT, { rootSchema: Object.assign({}, rootSchema) }) 177 | 178 | // set default value 179 | const type = getWidgetType(flatWidgets, newSchema.widget) 180 | const model = getWidgetModel(type, newSchema, typeBuilder) 181 | this.commit(types.$MODEL_SET, { model }) 182 | 183 | // set selectedSchema 184 | state.selectedSchema = newSchema 185 | }, 186 | 187 | // 设计模式删除一个widget 188 | [types.$WIDGET_REMOVE] (state, { key }) { 189 | const parentList = getParentListByKey(key, state.rootSchema) 190 | const index = getIndexByKey(key, parentList) 191 | const model = Object.assign({}, state.model) 192 | 193 | if (isNotEmptyString(key)) { 194 | delete model[key] 195 | state.model = model 196 | } 197 | parentList.splice(index, 1) 198 | this.commit(types.$ROOT_SCHEMA_FLAT, { rootSchema: Object.assign({}, state.rootSchema) }) 199 | this.commit(types.$RULE_INIT, { rootSchema: Object.assign({}, state.rootSchema) }) 200 | this.commit(types.$WIDGET_DESELECT) 201 | }, 202 | 203 | // 设计模式修改选中一个widget的type(返回值类型) 204 | [types.$WIDGET_TYPE_UPDATE] ({ model, flatSchemas }, { key, type }) { 205 | const schema = flatSchemas[key] 206 | const { widget } = schema 207 | const { flatWidgets } = this.getters 208 | const widgetType = getWidgetType(flatWidgets, widget) 209 | const WidgetSchema = flatWidgets[widget].Schema 210 | 211 | if (!widgetType || !type) { 212 | return 213 | } 214 | 215 | updateRequiredRule(schema, WidgetSchema, { type: TypeBuilder.resolve(type, true) }) 216 | if (!isArray(widgetType)) { 217 | schema.type = type 218 | return 219 | } 220 | if (!include(widgetType, type)) { 221 | return console.warn(`${type}不是widget(${widget})值类型`) 222 | } 223 | 224 | schema.type = type 225 | const value = model[key] 226 | if (include(type, 'array')) { 227 | if (isArray(value)) { 228 | return 229 | } 230 | // model[key] = [value] 231 | model[key] = [] 232 | } else { 233 | const builder = typeBuilder.types[type] 234 | if (isArray(value) && isFunction(builder)) { 235 | model[key] = builder() 236 | } 237 | } 238 | }, 239 | 240 | // 修改widget的option 241 | [types.$WIDGET_OPTION_UPDATE] ({ flatSchemas }, { key, option }) { 242 | const schema = flatSchemas[key] 243 | 244 | if (!schema || !option) { 245 | return 246 | } 247 | schema.option = Object.assign({ ...schema.option }, option) 248 | }, 249 | 250 | // 修改表单类型widget默认值 251 | [types.$WIDGET_DEFAULT_UPDATE] ({ flatSchemas }, { defaults, useName }) { 252 | let keyModel = defaults 253 | if (useName) { 254 | keyModel = convertNameModelToKeyModel(defaults, flatSchemas) 255 | } 256 | const keys = Object.keys(keyModel).filter(key => !!flatSchemas[key]) 257 | keys.forEach(key => { 258 | flatSchemas[key].default = keyModel[key] 259 | }) 260 | }, 261 | 262 | // 设计模式选中一个widget 263 | [types.$WIDGET_SELECT] (state, { key }) { 264 | const schema = state.flatSchemas[key] 265 | const isSameWidget = state.selectedSchema.key === key 266 | 267 | if (state.tab !== 'design' || isSameWidget || !schema) { 268 | return 269 | } 270 | state.selectedSchema = schema 271 | }, 272 | 273 | // 设计模式取消选中一个widget 274 | [types.$WIDGET_DESELECT] (state) { 275 | state.selectedSchema = defaultSchema() 276 | }, 277 | 278 | // 设计模式为选中widget的可变列表属性,执行添加操作 279 | [types.$WIDGET_CHILD_ADD] ({ flatSchemas }, { key, index, child }) { 280 | const schema = flatSchemas[key] 281 | 282 | schema.children.splice(index + 1, 0, child) 283 | }, 284 | 285 | // 设计模式为选中widget的可变列表属性,执行删除操作 286 | [types.$WIDGET_CHILD_REMOVE] ({ flatSchemas }, { key, index }) { 287 | const schema = flatSchemas[key] 288 | 289 | schema.children.splice(index, 1) 290 | }, 291 | 292 | // 设计模式为选中widget的可变列表属性,执行移动操作 293 | [types.$WIDGET_CHILD_MOVE] ({ flatSchemas }, { key, preIndex, index }) { 294 | const schema = flatSchemas[key] 295 | const children = [...schema.children] 296 | 297 | children.splice(preIndex, 1, ...children.splice(index, 1, children[preIndex])) 298 | schema.children = children 299 | } 300 | } 301 | -------------------------------------------------------------------------------- /src/helper/epUtil.js: -------------------------------------------------------------------------------- 1 | 2 | import { 3 | randomStr, 4 | isNotEmptyString, 5 | isArray, 6 | isFunction, 7 | include, 8 | jsonClone, 9 | checkValueType, 10 | formatDate 11 | } from './util' 12 | import TypeBuilder from '../store/TypeBuilder' 13 | 14 | /** 15 | * 获取根 schema 的子schema列表 16 | * @param {Object} schema 根schema 17 | */ 18 | export function getRootSchemaChildren (schema) { 19 | const list = [] 20 | 21 | if (!schema || !schema.container) { 22 | return list 23 | } 24 | if (!isArray(schema.children)) { 25 | schema.children = [] 26 | } 27 | 28 | const row = schema.children[0] 29 | if (!row) { 30 | return list 31 | } 32 | if (!isArray(row.list)) { 33 | row.list = [] 34 | } 35 | return row.list 36 | } 37 | 38 | /** 39 | * 获表单待提交的form data值 40 | * @param {Object} model store中或初始化的model数据 41 | * @param {Schema} rootSchema 根节点schema 42 | */ 43 | export function getFormData (model, rootSchema) { 44 | const formData = {} 45 | const baseTypes = ['string', 'number', 'boolean', 'json'] 46 | const convert = (t1, t2, v) => { 47 | if (include(baseTypes, t1) && include(baseTypes, t2)) { 48 | return t1 === t2 ? v : TypeBuilder.convert[`${t1}2${t2}`](v) 49 | } 50 | return v 51 | } 52 | const cleanValue = (value, schema) => { 53 | if (!schema.type) return value 54 | const resolvedType = TypeBuilder.resolve(schema.type) 55 | return isArray(value) 56 | ? value.map(v => convert(typeof v, resolvedType, v)) 57 | : convert(typeof value, resolvedType, value) 58 | } 59 | 60 | const getSchemaForm = function (schema, listItem) { 61 | const result = {} 62 | const { container, name, type, dynamic, list = [], key, group, children, hidden } = schema 63 | if (hidden) return result 64 | if (dynamic) { 65 | if (container) { 66 | result[name] = list.map(sc => { 67 | return getSchemaForm(sc, dynamic) 68 | }) 69 | } else if (type) { 70 | result[name] = list.map(sc => { 71 | return cleanValue(model[sc.key], sc) 72 | }) 73 | } 74 | } else { 75 | if (container) { 76 | if (group) { 77 | const tmpResult = {} 78 | children.forEach(child => { 79 | child.list.forEach(sc => { 80 | Object.assign(tmpResult, getSchemaForm(sc, false)) 81 | }) 82 | }) 83 | if (listItem) { 84 | Object.assign(result, tmpResult) 85 | } else { 86 | result[name] = tmpResult 87 | } 88 | } else { 89 | children.forEach(child => { 90 | child.list.forEach(sc => { 91 | Object.assign(result, getSchemaForm(sc)) 92 | }) 93 | }) 94 | } 95 | } else if (type) { 96 | result[name] = cleanValue(model[key], schema) 97 | } 98 | } 99 | return result 100 | } 101 | 102 | const { children = [] } = rootSchema 103 | children.forEach(child => { 104 | child.list.forEach(sc => { 105 | Object.assign(formData, getSchemaForm(sc)) 106 | // if (sc.container && sc.group) { 107 | // formData[sc.name] = getSchemaForm(sc) 108 | // } else { 109 | // Object.assign(formData, getSchemaForm(sc)) 110 | // } 111 | }) 112 | }) 113 | 114 | return formData 115 | } 116 | /** 117 | * get root schema and remove unused field 118 | * @param {Schema} schema root schema 119 | */ 120 | export function getSchema (_schema, stateStore = {}) { 121 | if (!_schema) { 122 | return {} 123 | } 124 | const { store, ...othersSchema } = _schema 125 | let _store = { 126 | dicts: [], 127 | apis: [] 128 | } 129 | _store.dicts = storeFetchClean(stateStore.dicts) 130 | _store.apis = storeFetchClean(stateStore.apis) 131 | 132 | _store = jsonClone(_store) 133 | 134 | const schema = jsonClone(othersSchema) 135 | schema.store = _store 136 | function clean (schema) { 137 | const option = schema.option || {} 138 | for (const i in option) { 139 | if (i === 'dynamicData') { 140 | option[i] = [] 141 | } 142 | // iview table field 143 | if (i === 'columns' && isArray(option[i])) { 144 | option[i].forEach(col => delete col.__id) 145 | } 146 | } 147 | if (schema.container && isArray(schema.children)) { 148 | schema.children.forEach(child => { 149 | if (isArray(child.list)) { 150 | child.list.forEach(s => clean(s)) 151 | } 152 | }) 153 | } 154 | } 155 | clean(schema) 156 | return schema 157 | } 158 | 159 | export function storeFetchClean (list = []) { 160 | return list.map(item => { 161 | const { source, data, response, ...others } = item 162 | return Object.assign(others, { 163 | source: [], 164 | data: [], 165 | response: { 166 | header: [] 167 | } 168 | }) 169 | }) 170 | } 171 | 172 | /** 173 | * 通过schema key 在指定容器schame或根schema找到key对应schema最近一层父级子列表 174 | * @param {String} key schema key 175 | * @param {Schema} schema 根schema或容器级schema 176 | */ 177 | export function getParentListByKey (key, schema) { 178 | const tmpSchema = schema 179 | let parentList = null 180 | function getParent (key, schema) { 181 | if (schema.container) { 182 | const { children } = schema 183 | for (let i = 0; i < children.length; i++) { 184 | const child = children[i] 185 | 186 | for (let k = 0; k < child.list.length; k++) { 187 | const subSchema = child.list[k] 188 | if (key === subSchema.key) { 189 | parentList = child.list 190 | break 191 | } else { 192 | getParent(key, subSchema) 193 | } 194 | } 195 | if (parentList) { 196 | break 197 | } 198 | } 199 | } 200 | } 201 | if (key) { 202 | getParent(key, tmpSchema) 203 | } 204 | if (!parentList) { 205 | const { children } = tmpSchema 206 | if ( 207 | isArray(children) && 208 | children[0] && 209 | isArray(children[0].list) 210 | ) parentList = children[0].list 211 | } 212 | return parentList 213 | } 214 | 215 | /** 216 | * 在schema列表中找到指定schema key对应的index值 217 | * @param {String} key shchema key 218 | * @param {Array} list schema 列表 219 | */ 220 | export function getIndexByKey (key, list) { 221 | let index = -1 222 | if (isNotEmptyString(key) && isArray(list)) { 223 | for (let i = 0, len = list.length; i < len; i++) { 224 | if (list[i].key === key) { 225 | index = i 226 | break 227 | } 228 | } 229 | } 230 | return index 231 | } 232 | 233 | /** 234 | * 替换指定schema极其所有子schema的key,一般复制widget用到 235 | * @param {Schema} schema 根schema或子schema 236 | */ 237 | export function replaceSchemaKey (schema) { 238 | function recursiveSchema (schema) { 239 | const { container, children } = schema 240 | if (container && isArray(children)) { 241 | children.forEach(child => { 242 | if (isArray(child.list)) { 243 | child.list.forEach(item => recursiveSchema(item)) 244 | } 245 | }) 246 | } 247 | schema.key = randomStr() 248 | schema.name = schema.key 249 | } 250 | recursiveSchema(schema) 251 | return schema 252 | } 253 | 254 | /** 255 | * 递归遍历对象,拉平指定key 256 | * @param {Schema Object} obj 待遍历的对象 257 | */ 258 | export function flattenSchema (schema) { 259 | const result = {} 260 | if (schema && schema.key) { 261 | result[schema.key] = schema 262 | } 263 | const ecursive = (schema, result) => { 264 | const { dynamic, list, container, children } = schema || {} 265 | 266 | if (dynamic && isArray(list)) { 267 | list.forEach(sc => ecursive(sc, result)) 268 | } 269 | 270 | if (!container || !isArray(children)) { 271 | return 272 | } 273 | 274 | children.forEach(child => { 275 | const list = child.list || [] 276 | list.forEach(item => { 277 | const { key } = item 278 | 279 | if (isNotEmptyString(key) && result[key] === undefined) { 280 | result[key] = item 281 | ecursive(item, result) 282 | } 283 | }) 284 | }) 285 | } 286 | ecursive(schema, result) 287 | return result 288 | } 289 | 290 | // 获取指定widget的数据类型,相同widget应该是统一的数据类型 291 | export function getWidgetType (flatWidgets = {}, widget) { 292 | let type = null 293 | for (const i in flatWidgets) { 294 | const { Schema } = flatWidgets[i] 295 | if (isFunction(Schema) && Schema.widget === widget) { 296 | type = Schema.type 297 | break 298 | } 299 | } 300 | return type 301 | } 302 | 303 | // 根据Schema.type及schema.key获取默认model值 304 | export function getWidgetModel (SchemaType, schema, typeBuilder) { 305 | if (!SchemaType || !isNotEmptyString(schema.key)) { 306 | return {} 307 | } 308 | // 初始化数据默认model值 309 | const defaultType = isArray(SchemaType) ? SchemaType[0] : SchemaType 310 | // 优先schema 中type字段指定值类型,没有设定使用当前widget类型的默认值类型 311 | const builder = typeBuilder.types[schema.type || defaultType] 312 | const model = { [schema.key]: '' } 313 | if (isFunction(builder)) { 314 | model[schema.key] = builder() 315 | } 316 | return model 317 | } 318 | 319 | /** 320 | * 检查并设置schema key 及 name 321 | * @param {Object} schema 待实例化的JSON 322 | * @param {Boolean} clone 是否复制 323 | * @param {Boolean} dynamic 是否为动态添加 324 | * @param {Object} 打平的schema集合 325 | */ 326 | export function setKeyAndName (schema, clone, dynamic, flatSchemas = {}) { 327 | if (!isNotEmptyString(schema.key)) { 328 | schema.key = randomStr() 329 | if (!schema.name) { 330 | schema.name = schema.key 331 | } 332 | } else { 333 | if (clone) { 334 | schema.key = randomStr() 335 | } 336 | if (!schema.name) { 337 | schema.name = schema.key 338 | } 339 | } 340 | if (clone && !dynamic) { 341 | const names = Object.keys(flatSchemas) 342 | .map(i => flatSchemas[i].name) 343 | .filter(_ => _) 344 | if (include(names, schema.name)) { 345 | schema.name = schema.key 346 | } 347 | } 348 | } 349 | 350 | /** 351 | * merge widget group 352 | * @param {Array} widgets 所有 widget 组,后面支持多个group级参数 353 | * @returns {Array} 返回新widget组 354 | */ 355 | export function mergeWidgets (widgets) { 356 | const result = isArray(widgets) ? [...widgets] : [] 357 | const groups = Array.prototype.slice.call(arguments, 1) 358 | const filterWidgets = result.filter(w => !include(groups.map(k => k.key), w.key)) 359 | 360 | return filterWidgets.concat(groups) 361 | } 362 | 363 | /** 364 | * merge 单个 widget 365 | * @param {Array} widgets 所有widget 组 366 | * @param {Object} widget widget对象 { type, key, widget: { Widget, Setting, schema }} 367 | * @returns {Array} 返回widget组 368 | */ 369 | export function mergeWidget (widgets, widget) { 370 | let hasWidget = false 371 | const groups = isArray(widgets) ? widgets : [] 372 | 373 | for (let i = 0; i < groups.length; i++) { 374 | const group = groups[i] 375 | if (!isArray(group.widgets)) { 376 | group.widgets = [] 377 | } 378 | for (let j = 0; j < group.widgets; j++) { 379 | const wSchema = widget.Schema 380 | const gSchema = group.widgets[j].Schema 381 | 382 | if (wSchema && gSchema && wSchema.widget === gSchema.widget) { 383 | hasWidget = true 384 | group.widgets[j] = widget 385 | break 386 | } 387 | } 388 | if (hasWidget) { 389 | break 390 | } 391 | } 392 | if (!hasWidget) { 393 | if (groups.length === 0) { 394 | groups.push({ title: '基础', key: 'base', widgets: [widget] }) 395 | } 396 | if (isArray(groups[0].widgets)) { 397 | groups[0].widgets.push(widget) 398 | } else { 399 | groups[0].widgets = [widget] 400 | } 401 | } 402 | return groups 403 | } 404 | 405 | /** 406 | * 设置validators 407 | * @param {Array} groups widgets 分组 408 | * @param {Object} validator 自定义的validator 409 | * @param {Boolean} replace 是否替换默认validator,还是与默认merge 410 | * example: 为input widget追加 phone规则,phone规则需要提前配置 411 | * setValidators(widgetsGroup, {input: ['phone']}) 412 | */ 413 | export function setValidators (groups, validator, replace) { 414 | if (!(isArray(groups) || !validator)) { 415 | return 416 | } 417 | // 循环widget group 418 | for (let i = 0, len = groups.length; i < len; i++) { 419 | const group = groups[i] 420 | const { widgets = [] } = group || {} 421 | // 循环特定组的widgets 列表 422 | widgets.forEach(widget => { 423 | // 循环自定义validator 对象 424 | for (const k in validator) { 425 | if (!widget.Schema || k !== widget.Schema.widget) { 426 | continue 427 | } 428 | const customValidators = validator[k] 429 | if (!isArray(customValidators)) { 430 | continue 431 | } 432 | const validators = widget.Schema.validators || [] 433 | if (replace) { 434 | widget.Schema.validators = customValidators 435 | } else { 436 | widget.Schema.validators = validators 437 | // 检查自定义widget是否已经与默认重复,重复将不添加 438 | customValidators.forEach(v => { 439 | if (!include(validators, v)) { 440 | validators.push(v) 441 | } 442 | }) 443 | } 444 | } 445 | }) 446 | } 447 | } 448 | 449 | /** 450 | * get and check rule type 451 | * @param {schema} schema widget schema. required 452 | * @param {Schema} WidgetSchema Schema class. not required 453 | */ 454 | export function getSchemaType (schema, WidgetSchema) { 455 | const widgetType = (WidgetSchema && WidgetSchema.type) ? WidgetSchema.type : null 456 | const schemaType = schema && isNotEmptyString(schema.type) ? schema.type : null 457 | // 缺省值 458 | let type = 'string' 459 | if (isArray(widgetType) && widgetType.length) { 460 | type = include(widgetType, schemaType) ? schemaType : widgetType[0] 461 | } else if (isNotEmptyString(widgetType)) { 462 | type = widgetType 463 | } 464 | return type 465 | } 466 | 467 | /** 468 | * get and check required rule type. schema.rules[0] 469 | * @param {schema} schema widget schema. required 470 | * @param {Schema} WidgetSchema Schema class. not required 471 | */ 472 | export function getRequiredRuleType (schema, WidgetSchema) { 473 | // const widgetType = (WidgetSchema && WidgetSchema.type) ? WidgetSchema.type : null 474 | // const schemaType = schema && isNotEmptyString(schema.type) ? schema.type : null 475 | // // 缺省值 476 | // let type = 'string' 477 | // if (isArray(widgetType) && widgetType.length) { 478 | // type = include(widgetType, schemaType) ? schemaType : widgetType[0] 479 | // } else if (isNotEmptyString(widgetType)) { 480 | // type = widgetType 481 | // } 482 | const type = getSchemaType(schema, WidgetSchema) 483 | return TypeBuilder.resolve(type, true) 484 | } 485 | 486 | /** 487 | * update required rule and Check the validity of the type 488 | * @param {schema} schema widget shcema. required 489 | * @param {Schema} WidgetSchema Schema class. not required 490 | * @param {Object} rule should updated rule. not required 491 | */ 492 | export function updateRequiredRule (schema, WidgetSchema, rule) { 493 | if (!schema || !isArray(schema.rules)) { 494 | return 495 | } 496 | const requiredRule = schema.rules[0] 497 | if (!requiredRule) { 498 | return 499 | } 500 | const { type, ...others } = rule || {} 501 | const newRule = Object.assign({}, requiredRule) 502 | if (type) { 503 | newRule.type = type 504 | } else { 505 | const requiredType = getRequiredRuleType(schema, WidgetSchema) 506 | if (requiredType) { 507 | newRule.type = requiredType 508 | } 509 | } 510 | schema.rules.splice(0, 1, Object.assign(newRule, others)) 511 | } 512 | 513 | /** 514 | * 自定义必选规则校验结果 515 | * @param {object} rule 516 | * @param {string} type 517 | */ 518 | export function getRuleValidator (rule, type) { 519 | const { message, required } = rule 520 | const emptyValues = [null, undefined, ''] 521 | return function (rule, value, callback) { 522 | rule.message = '' 523 | if (required) { 524 | if (isArray(value)) { 525 | if (value.length === 0) { 526 | return callback(new Error(message)) 527 | } 528 | } else { 529 | if (include(emptyValues, value)) { 530 | return callback(new Error(message)) 531 | } 532 | } 533 | } 534 | if (!include(emptyValues, value) && !checkValueType(value, type)) { 535 | return callback(new Error('类型不匹配')) 536 | } 537 | callback() 538 | } 539 | } 540 | /** 541 | * 为Vue安装依赖,已use的不在use 542 | * @param {Vue} Vue原型 543 | * @param {Array} 待安装的vue plugins 列表 544 | */ 545 | export function usePlugins (Vue, plugins) { 546 | if (!Vue || !isArray(plugins)) return 547 | const list = Vue._installedPlugins 548 | plugins.forEach(plugin => { 549 | if (isArray(list) && include(list, plugin)) return 550 | Vue.use(plugin) 551 | }) 552 | } 553 | 554 | /** 555 | * 转换name为key的model为以schema.key为key的model 556 | * @param {Object} name为key的model 557 | * @param {Object} 扁平的schema对象 558 | */ 559 | export function convertNameModelToKeyModel (model, flatSchemas) { 560 | const keyModel = {} 561 | for (const key in flatSchemas) { 562 | const schema = flatSchemas[key] 563 | for (const name in model) { 564 | if (schema.name !== name) continue 565 | keyModel[key] = model[name] 566 | } 567 | } 568 | return keyModel 569 | } 570 | 571 | /** 572 | * 根据schema.default默认值处理为最终可用默认值 573 | * @param {Schema} schema 574 | */ 575 | export function cleanDefaultValue (schema) { 576 | const { widget, option = {}, default: defaultValue } = schema 577 | const { format, range } = option 578 | let result = defaultValue 579 | 580 | if ('default' in schema) { 581 | switch (widget) { 582 | case 'datePicker': 583 | if (defaultValue === 'usedate') { 584 | const date = formatDate(new Date(), format) 585 | result = range ? [date, date] : date 586 | } else { 587 | result = defaultValue 588 | } 589 | break 590 | case 'timePicker': 591 | if (defaultValue === 'usetime') { 592 | const time = formatDate(new Date(), format) 593 | result = range ? [time, time] : time 594 | } else { 595 | result = defaultValue 596 | } 597 | break 598 | default: 599 | result = defaultValue 600 | } 601 | } 602 | return result 603 | } 604 | 605 | /** 606 | * 获取所有schema指定字段的默认值 607 | * @param {Object} flatSchemas 608 | * @param {Object} props ['value', ...] 609 | */ 610 | export function getDefaults (flatSchemas, props = []) { 611 | const result = {} 612 | for (const key in flatSchemas) { 613 | const schema = flatSchemas[key] 614 | result[key] = {} 615 | for (const prop in schema) { 616 | const item = schema[prop] 617 | if (props.indexOf(prop) === -1) continue 618 | if (typeof item === 'object') { 619 | result[key][prop] = jsonClone(item) 620 | } else { 621 | result[key][prop] = item 622 | } 623 | } 624 | } 625 | return result 626 | } 627 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | 2 | import StoreConf from './StoreConf' 3 | import Rule from '../rule' 4 | import types from './types' 5 | import { 6 | isNotEmptyString, 7 | isArray, 8 | getSchema, 9 | getFormData, 10 | include, 11 | isPlainObject 12 | } from '../helper' 13 | import { modes } from '../constant/static' 14 | const Vuex = require('vuex') 15 | 16 | export default class Store { 17 | constructor (option) { 18 | this.$$store = {} 19 | this.$$types = Object.assign({}, types) 20 | this.$$init(option) 21 | } 22 | 23 | /** 24 | * Store init 25 | * @param {Object} option the specified index logic 26 | */ 27 | $$init (option) { 28 | this.$$store = new Vuex.Store(new StoreConf({ 29 | Rule: option.Rule || Rule 30 | })) 31 | } 32 | 33 | /** 34 | * get the state of store 35 | * @returns {Object} state of store 36 | */ 37 | getState () { 38 | return this.$$store.state 39 | } 40 | 41 | /** 42 | * get the store of store 43 | * @returns {Object} store of store 44 | */ 45 | getStore () { 46 | return this.$$store.state.store 47 | } 48 | 49 | /** 50 | * select the store of store 51 | * @param {Object} dict dict of store 52 | * @param {Number} index index of store.dicts 53 | * @param {String} action update or create 54 | */ 55 | selectDict (dict, index, action) { 56 | this.$$store.commit(this.$$types.$STORE_DICT_SELECT, { dict, index, action }) 57 | } 58 | 59 | /** 60 | * update the dict of store 61 | * @param {Object} dict dict of store 62 | */ 63 | updateDict (dict, index) { 64 | this.$$store.commit(this.$$types.$STORE_DICT_UPDATE, { dict, index }) 65 | } 66 | 67 | /** 68 | * add the dict of store 69 | * @param {Object} dict dict of store 70 | */ 71 | addDict (dict) { 72 | this.$$store.commit(this.$$types.$STORE_DICT_ADD, { dict }) 73 | } 74 | 75 | /** 76 | * delete the dict of store 77 | * @param {Number} index index of store.dicts 78 | */ 79 | deleteDict (index) { 80 | this.$$store.commit(this.$$types.$STORE_DICT_DELETE, { index }) 81 | } 82 | 83 | /** 84 | * copy the dict of store 85 | * @param {Number} index index of store.dicts 86 | */ 87 | copyDict (index) { 88 | this.$$store.commit(this.$$types.$STORE_DICT_COPY, { index }) 89 | } 90 | 91 | /** 92 | * select the api of store 93 | * @param {Object} api of store 94 | * @param {Number} index index of store.apis 95 | * @param {Number} dictIndex index of store.dicts 96 | */ 97 | selectAPI (api, index, dictIndex, action) { 98 | this.$$store.commit(this.$$types.$STORE_API_SELECT, { api, index, dictIndex, action }) 99 | } 100 | 101 | /** 102 | * update the api of store 103 | * @param {Object} api of store 104 | * @param {Number} index index of store.apis 105 | */ 106 | updateAPI (api, index) { 107 | this.$$store.commit(this.$$types.$STORE_API_UPDATE, { api, index }) 108 | } 109 | 110 | /** 111 | * add the api of store 112 | * @param {Object} api of store 113 | */ 114 | addAPI (api) { 115 | this.$$store.commit(this.$$types.$STORE_API_ADD, { api }) 116 | } 117 | 118 | /** 119 | * delete the api of store 120 | * @param {Number} index index of store.apis 121 | */ 122 | deleteAPI (index) { 123 | this.$$store.commit(this.$$types.$STORE_API_DELETE, { index }) 124 | } 125 | 126 | /** 127 | * copy the api of store 128 | * @param {Number} index 129 | */ 130 | copyAPI (index) { 131 | this.$$store.commit(this.$$types.$STORE_API_COPY, { index }) 132 | } 133 | 134 | /** 135 | * get current tab on design page 136 | * @returns {String} current tab 137 | */ 138 | getTab () { 139 | return this.$$store.state.tab 140 | } 141 | 142 | /** 143 | * get form data 144 | * @returns {Object} form data 145 | */ 146 | getFormData () { 147 | const { model, rootSchema } = this.$$store.state 148 | return getFormData(model, rootSchema) 149 | } 150 | 151 | /** 152 | * get cleaned root schema 153 | * @returns {Object} form data 154 | */ 155 | getSchema () { 156 | const { rootSchema, store } = this.$$store.state 157 | return getSchema(rootSchema, store) 158 | } 159 | 160 | /** 161 | * get selected schema 162 | * @returns {Schema} selected schema 163 | */ 164 | getSelectedSchema () { 165 | return this.$$store.state.selectedSchema 166 | } 167 | 168 | /** 169 | * get root schema 170 | * @returns {Schema} root schema 171 | */ 172 | getRootSchema () { 173 | return this.$$store.state.rootSchema 174 | } 175 | 176 | /** 177 | * get the form content entered by the user 178 | * @returns {Object} form data 179 | */ 180 | getModel (option) { 181 | if (!option) { 182 | return this.$$store.state.model 183 | } 184 | if (typeof option === 'string') { 185 | return this.$$store.state.model[option] 186 | } 187 | } 188 | 189 | /** 190 | * update form data 191 | * @param {Object} model the form data 192 | */ 193 | updateModel (model = {}, useName = false) { 194 | this.$$store.commit(this.$$types.$MODEL_SET, { model, useName }) 195 | } 196 | 197 | /** 198 | * reset the form 199 | * @returns {Object} form data 200 | */ 201 | resetModel () { 202 | const { model } = this.$$store.state 203 | const newModel = {} 204 | for (const i in model) { 205 | const type = typeof model[i] 206 | switch (type) { 207 | case 'string': 208 | newModel[i] = '' 209 | break 210 | case 'number': 211 | newModel[i] = 0 212 | break 213 | case 'boolean': 214 | newModel[i] = false 215 | break 216 | case 'object': 217 | newModel[i] = isArray(model[i]) ? [] : {} 218 | break 219 | default: 220 | /* eslint no-self-assign: 0 */ 221 | newModel[i] = newModel[i] 222 | break 223 | } 224 | } 225 | this.$$store.commit(this.$$types.$MODEL_SET, { model: newModel }) 226 | } 227 | 228 | /** 229 | * get registered widgets 230 | * @returns {Array} the widget library returns 231 | */ 232 | getWidgets () { 233 | return this.$$store.state.widgets 234 | } 235 | 236 | /** 237 | * get schema collection object 238 | * @returns {Object} flated schemas 239 | */ 240 | getFlatSchemas () { 241 | return this.$$store.state.flatSchemas 242 | } 243 | 244 | /** 245 | * get key by name or custom prop 246 | * @param {String} name schema.name value 247 | * @param {String} prop 'name' is default value. schema[prop] = name 248 | * @returns {String} schema.key 249 | */ 250 | getKeyByName (name, prop) { 251 | const flatSchemas = this.$$store.state.flatSchemas || {} 252 | const field = (prop && (typeof prop === 'string')) ? prop : 'name' 253 | let key 254 | for (const i in flatSchemas) { 255 | if (flatSchemas[i][field] === name) { 256 | key = i 257 | break 258 | } 259 | } 260 | return key 261 | } 262 | 263 | /** 264 | * get rule collection object 265 | * @returns {Object} flated rules 266 | */ 267 | getFlatRules () { 268 | return this.$$store.state.flatRules 269 | } 270 | 271 | /** 272 | * get form rule 273 | * @returns {Object} rules object 274 | */ 275 | getFormRules () { 276 | const { flatSchemas, flatRules } = this.$$store.state 277 | const rules = {} 278 | for (const key in flatSchemas) { 279 | if (!flatSchemas[key].hidden) { 280 | rules[key] = flatRules[key] 281 | } 282 | } 283 | return rules 284 | } 285 | 286 | /** 287 | * determine if the widget is selected, 288 | * generally used in design mode 289 | * @returns {Boolean} selected or unselected 290 | */ 291 | isSelected () { 292 | return this.$$store.getters.isSelected 293 | } 294 | 295 | /** 296 | * get widget collection 297 | * @returns {Object} flated widget 298 | * @example 299 | * { input: { Setting, View, Schema }, ...} 300 | */ 301 | getFlatWidgets () { 302 | return this.$$store.getters.flatWidgets 303 | } 304 | 305 | /** 306 | * get the setting form of the selected widget 307 | * @returns {Vue Component} vue component form 308 | */ 309 | getSettingWidget () { 310 | return this.$$store.getters.settingWidget 311 | } 312 | 313 | /** 314 | * get the validators collection of registered widgets 315 | * @returns {Object} validators 316 | * @example 317 | * { input: [StringRule, EmailRule, URLRule, RegExpRule], ...} 318 | */ 319 | getWidgetsValidators () { 320 | return this.$$store.getters.widgetsValidators 321 | } 322 | 323 | /** 324 | * update view panel in design mode 325 | * @param {String} tab view panel 326 | */ 327 | updateTab (tab) { 328 | if (!isNotEmptyString(tab)) { 329 | return 330 | } 331 | 332 | this.$$store.commit(this.$$types.$TAB_UPDATE, { tab }) 333 | } 334 | 335 | /** 336 | * root schema of epage, should be initialize once 337 | * @param {Object} rootSchema should be initialize 338 | */ 339 | initRootSchema (rootSchema) { 340 | this.$$store.commit(this.$$types.$ROOT_SCHEMA_SET, { rootSchema }) 341 | } 342 | 343 | /** 344 | * register widgets 345 | * @param {Array} widgets the widget library returns 346 | */ 347 | initWidgets (widgets) { 348 | if (!isArray(widgets)) { 349 | return console.warn('widgets should be type of array') 350 | } 351 | 352 | this.$$store.commit(this.$$types.$WIDGETS_SET, { widgets }) 353 | } 354 | 355 | /** 356 | * update epage mode 357 | * @enum {String} edit | display 358 | */ 359 | updateMode (mode) { 360 | if (!include(modes(), mode)) { 361 | return console.warn(`mode must be one of ${modes().toString()}`) 362 | } 363 | 364 | this.$$store.commit(this.$$types.$MODE_CHANGE, { mode }) 365 | } 366 | 367 | /** 368 | * add widget when has registered widgets 369 | * @param {String|Schema} widget the unique name or schema 370 | */ 371 | addWidget (widget) { 372 | if (!isNotEmptyString(widget)) { 373 | if (isPlainObject(widget) && isNotEmptyString(widget.widget)) { 374 | this.$$store.commit(this.$$types.$WIDGET_ADD, { widget: null, schema: widget }) 375 | } else { 376 | return console.warn('widget should be a non-empty string or schema object') 377 | } 378 | } else { 379 | this.$$store.commit(this.$$types.$WIDGET_ADD, { widget, schema: null }) 380 | } 381 | } 382 | 383 | /** 384 | * dynamic add widget when has registered widgets 385 | * @param {String} key the unique key of widget 386 | */ 387 | dynamicAddWidget (key) { 388 | if (!isNotEmptyString(key)) { 389 | return console.warn('key should be a non-empty string') 390 | } 391 | 392 | this.$$store.commit(this.$$types.$WIDGET_DYNAMIC_ADD, { key }) 393 | } 394 | 395 | /** 396 | * dynamic remove widget when has registered widgets 397 | * @param {String} key the unique key of widget 398 | */ 399 | dynamicRemoveWidget (key, index) { 400 | if (!isNotEmptyString(key)) { 401 | return console.warn('key should be a non-empty string') 402 | } 403 | 404 | this.$$store.commit(this.$$types.$WIDGET_DYNAMIC_REMOVE, { key, index }) 405 | } 406 | 407 | /** 408 | * copy widget when has registered widgets 409 | * @param {String} key the unique key of widget 410 | */ 411 | copyWidget (key) { 412 | if (!isNotEmptyString(key)) { 413 | return 414 | } 415 | 416 | this.$$store.commit(this.$$types.$WIDGET_COPY, { key }) 417 | } 418 | 419 | /** 420 | * remove from specified widget 421 | * @param {String} key the unique key of widget 422 | */ 423 | removeWidget (key) { 424 | if (!isNotEmptyString(key)) { 425 | return 426 | } 427 | 428 | this.$$store.commit(this.$$types.$WIDGET_REMOVE, { key }) 429 | } 430 | 431 | /** 432 | * set widget to selected 433 | * @param {String} key the unique key of selected widget 434 | */ 435 | selectWidget (key) { 436 | if (!isNotEmptyString(key)) { 437 | return 438 | } 439 | 440 | this.$$store.commit(this.$$types.$WIDGET_SELECT, { key }) 441 | } 442 | 443 | /** 444 | * set widget to unselected 445 | * @param {String} key the unique key of widget 446 | */ 447 | deselectWidget () { 448 | this.$$store.commit(this.$$types.$WIDGET_DESELECT) 449 | } 450 | 451 | /** 452 | * update the return value type of widget 453 | * @param {String} key the unique key of widget 454 | * @param {String} type the new return value type of widget 455 | */ 456 | updateWidgetType (key, type) { 457 | if (!isNotEmptyString(key) || !isNotEmptyString(type)) { 458 | return 459 | } 460 | 461 | this.$$store.commit(this.$$types.$WIDGET_TYPE_UPDATE, { key, type }) 462 | } 463 | 464 | /** 465 | * update the custom props of widget 466 | * @param {String} key the unique key of widget 467 | * @param {Object} option should be updated props object 468 | */ 469 | updateWidgetOption (key, option) { 470 | if (!isNotEmptyString(key) || !option) { 471 | return 472 | } 473 | 474 | this.$$store.commit(this.$$types.$WIDGET_OPTION_UPDATE, { key, option }) 475 | } 476 | 477 | /** 478 | * update default value 479 | * @param {Object} defaults {[schema.key]: defaultValue} 480 | */ 481 | updateWidgetDefault (defaults, useName) { 482 | if (!isPlainObject(defaults)) { 483 | return 484 | } 485 | 486 | this.$$store.commit(this.$$types.$WIDGET_DEFAULT_UPDATE, { defaults, useName }) 487 | } 488 | 489 | /** 490 | * add child schema 491 | * @param {String} key the unique key of widget 492 | * @param {Number} index the index of new widget 493 | * @param {Object} child child element, example: { span: 12, list: [] } 494 | */ 495 | addWidgetChild (key, index, child) { 496 | if (!isNotEmptyString(key) || index < 0 || !child) { 497 | return 498 | } 499 | 500 | this.$$store.commit(this.$$types.$WIDGET_CHILD_ADD, { key, index, child }) 501 | } 502 | 503 | /** 504 | * remove child schema 505 | * @param {String} key the unique key of widget 506 | * @param {Number} index the index of widget 507 | */ 508 | removeWidgetChild (key, index) { 509 | if (!isNotEmptyString(key)) { 510 | return 511 | } 512 | 513 | this.$$store.commit(this.$$types.$WIDGET_CHILD_REMOVE, { key, index }) 514 | } 515 | 516 | /** 517 | * move child schema 518 | * @param {String} key the unique key of widget 519 | * @param {Number} preIndex the previous index of widget 520 | * @param {Number} index the index of widget 521 | */ 522 | moveWidgetChild (key, preIndex, index) { 523 | if (!isNotEmptyString(key) || preIndex < 0 || index < 0) { 524 | return 525 | } 526 | 527 | this.$$store.commit(this.$$types.$WIDGET_CHILD_MOVE, { key, preIndex, index }) 528 | } 529 | 530 | /** 531 | * trigger other widget props changes based on value changes 532 | * @param {Object} model the form data 533 | */ 534 | updateWidgetByModel (model, callback) { 535 | this.$$store.commit(this.$$types.$WIDGET_UPDATE_BY_VALUE_LOGIC, { model, callback }) 536 | } 537 | 538 | /** 539 | * trigger other widget props changes based on event triggered 540 | * @param {String} key the unique key of widget 541 | * @param {String} eventType event type, looks like click | change | blur and etc. 542 | */ 543 | updateWidgetByEvent (key, eventType, callback) { 544 | this.$$store.commit(this.$$types.$WIDGET_UPDATE_BY_EVENT_LOGIC, { key, eventType, callback }) 545 | } 546 | 547 | /** 548 | * initialize rule 549 | * @param {Object} rootSchema initialized root schema 550 | */ 551 | initRule (rootSchema) { 552 | this.$$store.commit(this.$$types.$RULE_INIT, { rootSchema }) 553 | } 554 | 555 | /** 556 | * add rule 557 | * @param {String} key the unique key of widget 558 | */ 559 | addRule (key) { 560 | if (!isNotEmptyString(key)) { 561 | return 562 | } 563 | 564 | this.$$store.commit(this.$$types.$RULE_ADD, { key }) 565 | } 566 | 567 | /** 568 | * remove rule 569 | * @param {String} key the unique key of widget 570 | * @param {Number} index the specified index rule 571 | */ 572 | removeRule (key, index) { 573 | if (!isNotEmptyString(key)) { 574 | return 575 | } 576 | 577 | this.$$store.commit(this.$$types.$RULE_REMOVE, { key, index }) 578 | } 579 | 580 | /** 581 | * update rule 582 | * @param {String} key the unique key of widget 583 | * @param {Number} index the specified index rule 584 | * @param {Object} rule rule detail, https://github.com/yiminghe/async-validator 585 | * @example 586 | * rule = { message: 'required', type: 'email' } 587 | */ 588 | updateRule (key, index, rule) { 589 | if (!isNotEmptyString(key) || typeof index !== 'number' || typeof rule !== 'object') { 590 | return 591 | } 592 | const { type, message } = rule 593 | 594 | // The first rule is to determine whether it is required 595 | if (index === 0) { 596 | this.$$store.commit(this.$$types.$RULE_REQUIRED_RULE_UPDATE, { key, rule }) 597 | } else { 598 | if (isNotEmptyString(type)) { 599 | this.$$store.commit(this.$$types.$RULE_TYPE_UPDATE, { key, index, type }) 600 | if (!isNotEmptyString(message)) { 601 | return 602 | } 603 | this.$$store.commit(this.$$types.$RULE_MESSAGE_UPDATE, { key, index, message }) 604 | } else { 605 | if (!isNotEmptyString(message)) { 606 | return 607 | } 608 | this.$$store.commit(this.$$types.$RULE_MESSAGE_UPDATE, { key, index, message }) 609 | } 610 | } 611 | } 612 | 613 | /** 614 | * add logic 615 | * @param {Object} logic logic object 616 | * @example 617 | * logic = { 618 | * type: "event", 619 | * key: "ktEAFSAXl", 620 | * action: "click", 621 | * effects: [{ 622 | * "key": "kDFnqbKSW", 623 | * "properties": [ 624 | * { key: "hidden", value: true }, 625 | * { key: "disabled", value: false } 626 | * ] 627 | * }] 628 | * } 629 | */ 630 | addLogic (logic) { 631 | if (!logic) { 632 | return 633 | } 634 | 635 | this.$$store.commit(this.$$types.$LOGIC_ADD, { logic }) 636 | } 637 | 638 | /** 639 | * update logic 640 | * @param {Number} index the specified index logic 641 | * @param {Object} logic logic object 642 | */ 643 | updateLogic (index, logic) { 644 | if (index < 0 || !logic) { 645 | return 646 | } 647 | 648 | this.$$store.commit(this.$$types.$LOGIC_UPDATE, { index, logic }) 649 | } 650 | 651 | /** 652 | * remove logic 653 | * @param {Number} index the specified index logic 654 | */ 655 | removeLogic (index) { 656 | if (index < 0) { 657 | return 658 | } 659 | 660 | this.$$store.commit(this.$$types.$LOGIC_DELETE, { index }) 661 | } 662 | 663 | /** 664 | * update style 665 | * @param {String|null} key schema key 666 | * @param {Obect} style style of css attribute 667 | */ 668 | updateStyle (key, style) { 669 | if (typeof style !== 'object') { 670 | return 671 | } 672 | 673 | this.$$store.commit(this.$$types.$STYLE_UPDATE, { key, style }) 674 | } 675 | } 676 | --------------------------------------------------------------------------------