ソースを参照

Merge remote-tracking branch 'origin/feature/formily'

luoyali 1 年間 前
コミット
ec783a86b5
51 ファイル変更5030 行追加77 行削除
  1. 5 1
      package.json
  2. 34 0
      src/components/FormCreateDesigner/DragBox.vue
  3. 136 0
      src/components/FormCreateDesigner/DragTool.vue
  4. 1076 0
      src/components/FormCreateDesigner/FcDesigner.vue
  5. 170 0
      src/components/FormCreateDesigner/Fetch.vue
  6. 16 0
      src/components/FormCreateDesigner/IconRefresh.vue
  7. 67 0
      src/components/FormCreateDesigner/Required.vue
  8. 124 0
      src/components/FormCreateDesigner/Struct.vue
  9. 78 0
      src/components/FormCreateDesigner/TableOptions.vue
  10. 236 0
      src/components/FormCreateDesigner/Validate.vue
  11. 21 1
      src/components/registerGlobComp.ts
  12. 89 0
      src/config/base/field.ts
  13. 62 0
      src/config/base/form.ts
  14. 9 0
      src/config/base/validate.ts
  15. 43 0
      src/config/menu.ts
  16. 64 0
      src/config/rule/alert.ts
  17. 76 0
      src/config/rule/button.ts
  18. 116 0
      src/config/rule/cascader.ts
  19. 57 0
      src/config/rule/checkbox.ts
  20. 27 0
      src/config/rule/col.ts
  21. 50 0
      src/config/rule/color.ts
  22. 108 0
      src/config/rule/date.ts
  23. 50 0
      src/config/rule/divider.ts
  24. 31 0
      src/config/rule/editor.ts
  25. 55 0
      src/config/rule/index.ts
  26. 98 0
      src/config/rule/input.ts
  27. 59 0
      src/config/rule/number.ts
  28. 50 0
      src/config/rule/radio.ts
  29. 56 0
      src/config/rule/rate.ts
  30. 64 0
      src/config/rule/row.ts
  31. 96 0
      src/config/rule/select.ts
  32. 64 0
      src/config/rule/slider.ts
  33. 42 0
      src/config/rule/space.ts
  34. 35 0
      src/config/rule/span.ts
  35. 55 0
      src/config/rule/switch.ts
  36. 50 0
      src/config/rule/tab.ts
  37. 36 0
      src/config/rule/tabPane.ts
  38. 77 0
      src/config/rule/time.ts
  39. 101 0
      src/config/rule/transfer.ts
  40. 86 0
      src/config/rule/tree.ts
  41. 86 0
      src/config/rule/upload.ts
  42. 408 0
      src/lang/en.ts
  43. 405 0
      src/lang/zh-cn.ts
  44. 1 0
      src/main.ts
  45. 133 0
      src/styles/fontIndex.scss
  46. BIN
      src/styles/fonts/fc-icons.woff
  47. 2 1
      src/styles/index.scss
  48. 9 0
      src/utils/form.ts
  49. 192 0
      src/utils/formCreateIndex.ts
  50. 22 0
      src/utils/locale.ts
  51. 3 74
      src/views/flow/create/components/FormDesign.vue

+ 5 - 1
package.json

@@ -37,7 +37,10 @@
     "vue-i18n": "^9.2.2",
     "vue-ls": "^4.2.0",
     "vue-router": "^4.2.1",
-    "vuedraggable": "^4.1.0"
+    "vuedraggable": "^4.1.0",
+		"@form-create/component-wangeditor": "^3.1",
+		"@form-create/element-ui": "^3.1.16",
+		"@form-create/utils": "^3.1.15"
   },
   "devDependencies": {
     "@types/js-md5": "^0.7.0",
@@ -50,6 +53,7 @@
     "@vitejs/plugin-vue": "^4.2.3",
     "@vitejs/plugin-vue-jsx": "^3.0.1",
 		"autoprefixer": "^10.4.16",
+		"codemirror": "^5.60.0",
     "eslint": "^8.40.0",
     "eslint-config-prettier": "^8.7.0",
     "eslint-plugin-prettier": "^4.2.1",

+ 34 - 0
src/components/FormCreateDesigner/DragBox.vue

@@ -0,0 +1,34 @@
+<script>
+import { h, defineComponent } from 'vue'
+import draggable from 'vuedraggable/src/vuedraggable'
+
+export default defineComponent({
+	name: 'DragBox',
+	props: ['rule', 'tag', 'formCreateInject'],
+	render(ctx) {
+		const subRule = { ...ctx.$props.rule.props, ...ctx.$attrs }
+		let _class = subRule.tag + '-drag drag-box'
+		if (!Object.keys(ctx.$slots).length) {
+			_class += ' ' + subRule.tag + '-holder'
+		}
+		subRule.class = _class
+		subRule.modelValue = [...this.$props.formCreateInject.children]
+
+		const keys = {}
+		if (ctx.$slots.default) {
+			const children = ctx.$slots.default()
+			children.forEach(v => {
+				if (v.key) {
+					keys[v.key] = v
+				}
+			})
+		}
+
+		return h(draggable, subRule, {
+			item: ({ element }) => {
+				return h('div', {}, keys[element.__fc__.key + 'fc'])
+			}
+		})
+	}
+})
+</script>

+ 136 - 0
src/components/FormCreateDesigner/DragTool.vue

@@ -0,0 +1,136 @@
+<template>
+	<div class="drag-tool" :class="{ active: state.active === id }" @click.stop="active">
+		<div v-if="mask" class="drag-mask"></div>
+		<div class="drag-l">
+			<div v-if="state.active === id && dragBtn !== false" class="drag-btn _fc-drag-btn" style="cursor: move">
+				<i class="fc-icon icon-move"></i>
+			</div>
+		</div>
+		<div class="drag-r">
+			<div class="drag-btn" @click="$emit('create')">
+				<i class="fc-icon icon-add"></i>
+			</div>
+			<div class="drag-btn" @click="$emit('copy')">
+				<i class="fc-icon icon-copy"></i>
+			</div>
+			<div v-if="children" class="drag-btn" @click="$emit('addChild')">
+				<i class="fc-icon icon-add-child"></i>
+			</div>
+			<div class="drag-btn drag-btn-danger" @click="$emit('delete')">
+				<i class="fc-icon icon-delete"></i>
+			</div>
+		</div>
+		<slot name="default"></slot>
+	</div>
+</template>
+
+<script>
+import { computed, inject, toRefs, defineComponent } from 'vue'
+
+let uni = 1
+export default defineComponent({
+	name: 'DragTool',
+	props: ['dragBtn', 'children', 'unique', 'mask'],
+	setup(props) {
+		const { unique } = toRefs(props)
+		const id = computed(() => unique.value || uni++)
+		const state = inject('fcx')
+		return {
+			id,
+			state
+		}
+	},
+	beforeUnmount() {
+		this.state = {}
+	},
+	methods: {
+		active() {
+			if (this.state.active === this.id) return
+			this.state.active = this.id
+			this.$emit('active')
+		}
+	}
+})
+</script>
+
+<style>
+.drag-tool {
+	position: relative;
+	display: flex;
+	min-height: 20px;
+	box-sizing: border-box;
+	padding: 2px;
+	outline: 1px dashed #2e73ff;
+	overflow: hidden;
+	word-wrap: break-word;
+	word-break: break-all;
+}
+
+.drag-tool .drag-tool {
+	margin: 5px;
+}
+
+.drag-tool + .drag-tool {
+	margin-top: 5px;
+}
+
+.drag-tool.active {
+	outline: 2px solid #2e73ff;
+}
+
+.drag-tool.active > div > .drag-btn {
+	display: flex;
+}
+
+.drag-tool .drag-btn {
+	display: none;
+}
+
+.drag-r {
+	position: absolute;
+	right: 2px;
+	bottom: 2px;
+	z-index: 1904;
+}
+
+.drag-l {
+	position: absolute;
+	top: 0;
+	left: 0;
+	z-index: 1904;
+}
+
+.drag-btn {
+	height: 18px;
+	width: 18px;
+	color: #fff;
+	background-color: #2e73ff;
+	text-align: center;
+	line-height: 20px;
+	padding-bottom: 1px;
+	float: left;
+	cursor: pointer;
+	justify-content: center;
+}
+
+.drag-btn + .drag-btn {
+	margin-left: 2px;
+}
+
+.drag-btn-danger {
+	background-color: #ff2e2e;
+}
+
+.drag-btn i {
+	font-size: 13px;
+}
+
+.drag-mask {
+	z-index: 1900;
+	position: absolute;
+	top: 0;
+	left: 0;
+	right: 0;
+	bottom: 0;
+}
+</style>

+ 1076 - 0
src/components/FormCreateDesigner/FcDesigner.vue

@@ -0,0 +1,1076 @@
+<style>
+._fc-designer {
+	height: 100%;
+	min-height: 500px;
+	overflow: hidden;
+	cursor: default;
+	position: relative;
+}
+
+._fc-designer > .el-main {
+	position: absolute;
+	top: 0;
+	bottom: 0;
+	left: 0;
+	right: 0;
+	padding: 0px;
+}
+
+._fc-m .form-create ._fc-l-item {
+	background: #2e73ff;
+	width: 100%;
+	height: 10px;
+	overflow: hidden;
+	transition: all 0.3s ease;
+}
+
+._fc-l,
+._fc-m,
+._fc-r {
+	border-top: 1px solid #ececec;
+	box-sizing: border-box;
+}
+
+._fc-l-group {
+	padding: 0 12px;
+}
+
+._fc-l-title {
+	font-weight: 600;
+	font-size: 14px;
+	margin: 18px 0px 5px;
+}
+
+._fc-l-item {
+	display: inline-block;
+	background: #fff;
+	color: #000;
+	min-width: 70px;
+	width: 33.33%;
+	height: 70px;
+	line-height: 1;
+	text-align: center;
+	transition: all 0.2s ease;
+	cursor: pointer;
+}
+
+._fc-l-item i {
+	font-size: 21px;
+	display: inline-block;
+}
+
+._fc-l-item ._fc-l-name {
+	font-size: 12px;
+}
+
+._fc-l-item ._fc-l-icon {
+	padding: 10px 5px 12px;
+}
+
+._fc-l-item:hover {
+	background: #2e73ff;
+	color: #fff;
+}
+
+._fc-m-tools {
+	height: 40px;
+	align-items: center;
+	display: flex;
+	justify-content: flex-end;
+	border: 1px solid #ececec;
+	border-top: 0 none;
+}
+
+._fc-m-tools button.el-button {
+	padding: 5px 14px;
+	display: flex;
+	align-items: center;
+}
+
+._fc-m-tools .fc-icon {
+	font-size: 14px;
+	margin-right: 2px;
+}
+
+._fc-r .el-tabs__nav-wrap::after {
+	height: 1px;
+	background-color: #ececec;
+}
+
+._fc-r ._fc-r-tabs {
+	display: flex;
+	padding: 0;
+	border-bottom: 1px solid #ececec;
+}
+
+._fc-r ._fc-r-tab {
+	height: 40px;
+	box-sizing: border-box;
+	line-height: 40px;
+	display: inline-block;
+	list-style: none;
+	font-size: 14px;
+	font-weight: 600;
+	color: #303133;
+	position: relative;
+	flex: 1;
+	text-align: center;
+}
+
+._fc-r ._fc-r-tab.active {
+	color: #409eff;
+	border-bottom: 2px solid #409eff;
+}
+
+.drag-box {
+	min-height: 60px;
+	width: 100%;
+}
+
+._fc-m-drag {
+	overflow: auto;
+	padding: 2px;
+	box-sizing: border-box;
+}
+
+._fc-m-drag,
+.draggable-drag {
+	background: #fff;
+	height: 100%;
+	position: relative;
+}
+
+._fc-m-drag > form,
+._fc-m-drag > form > .el-row {
+	height: 100%;
+}
+</style>
+
+<template>
+	<ElContainer class="_fc-designer" :style="'height:' + dragHeight">
+		<ElMain>
+			<ElContainer style="height: 100%">
+				<el-aside class="_fc-l" width="266px">
+					<template v-for="(item, index) in menuList" :key="index">
+						<div class="_fc-l-group">
+							<h4 class="_fc-l-title">{{ item.title }}</h4>
+							<draggable :group="{ name: 'default', pull: 'clone', put: false }" :sort="false" itemKey="name" :list="item.list">
+								<template #item="{ element }">
+									<div class="_fc-l-item">
+										<div class="_fc-l-icon">
+											<i class="fc-icon" :class="element.icon || 'icon-input'"></i>
+										</div>
+										<span class="_fc-l-name">{{ t('components.' + element.name + '.name') || element.label }}</span>
+									</div>
+								</template>
+							</draggable>
+						</div>
+					</template>
+				</el-aside>
+				<ElContainer class="_fc-m">
+					<el-header class="_fc-m-tools" height="45">
+						<slot name="handle"></slot>
+						<el-button size="small" type="primary">保存当前JSON</el-button>
+						<el-button type="primary" plain round size="small" @click="previewFc"
+							><i class="fc-icon icon-preview"></i> {{ t('designer.preview') }}
+						</el-button>
+						<el-popconfirm
+							:title="t('designer.clearConfirmTitle')"
+							width="200px"
+							:confirm-button-text="t('designer.clearConfirm')"
+							:cancel-button-text="t('designer.clearCancel')"
+							@confirm="clearDragRule"
+						>
+							<template #reference>
+								<el-button type="danger" plain round size="small"><i class="fc-icon icon-delete"></i>{{ t('designer.clear') }} </el-button>
+							</template>
+						</el-popconfirm>
+					</el-header>
+					<ElMain style="background: #f5f5f5; padding: 20px">
+						<div class="_fc-m-drag">
+							<DragForm :rule="dragForm.rule" :option="form.value" v-model:api="dragForm.api"></DragForm>
+						</div>
+					</ElMain>
+				</ElContainer>
+				<ElAside class="_fc-r" width="320px" v-if="!config || config.showConfig !== false">
+					<ElContainer style="height: 100%">
+						<el-header height="40px" class="_fc-r-tabs">
+							<div
+								class="_fc-r-tab"
+								:class="{ active: activeTab === 'props' }"
+								v-if="!!activeRule || (config && config.showFormConfig === false)"
+								@click="activeTab = 'props'"
+							>
+								{{ t('designer.config.component') }}
+							</div>
+							<div
+								class="_fc-r-tab"
+								v-if="!config || config.showFormConfig !== false"
+								:class="{ active: activeTab === 'form' && !!activeRule }"
+								@click="activeTab = 'form'"
+							>
+								{{ t('designer.config.form') }}
+							</div>
+						</el-header>
+						<ElMain v-show="activeTab === 'form'" v-if="!config || config.showFormConfig !== false">
+							<DragForm :rule="form.rule" :option="form.option" v-model="form.value.form" v-model:api="form.api"></DragForm>
+						</ElMain>
+						<ElMain v-show="activeTab === 'props'" style="padding: 0 20px" :key="activeRule ? activeRule._id : ''">
+							<div>
+								<ElDivider v-if="showBaseRule">{{ t('designer.config.rule') }}</ElDivider>
+								<DragForm
+									v-show="showBaseRule"
+									v-model:api="baseForm.api"
+									:rule="baseForm.rule"
+									:option="baseForm.options"
+									:modelValue="baseForm.value"
+									@change="baseChange"
+								></DragForm>
+								<ElDivider>{{ t('designer.config.props') }}</ElDivider>
+								<DragForm
+									v-model:api="propsForm.api"
+									:rule="propsForm.rule"
+									:option="propsForm.options"
+									:modelValue="propsForm.value"
+									@change="propChange"
+									@removeField="propRemoveField"
+								></DragForm>
+								<ElDivider v-if="showBaseRule">{{ t('designer.config.validate') }}</ElDivider>
+								<DragForm
+									v-show="showBaseRule"
+									v-model:api="validateForm.api"
+									:rule="validateForm.rule"
+									:option="validateForm.options"
+									:modelValue="validateForm.value"
+									@update:modelValue="validateChange"
+								></DragForm>
+							</div>
+						</ElMain>
+					</ElContainer>
+				</ElAside>
+				<ElDialog v-model="preview.state" width="800px" append-to-body>
+					<ViewForm :rule="preview.rule" :option="preview.option" v-if="preview.state"></ViewForm>
+				</ElDialog>
+			</ElContainer>
+		</ElMain>
+	</ElContainer>
+</template>
+<script>
+import form from '../../config/base/form'
+import field from '../../config/base/field'
+import validate from '../../config/base/validate'
+import { deepCopy } from '@form-create/utils/lib/deepextend'
+import is, { hasProperty } from '@form-create/utils/lib/type'
+import { lower } from '@form-create/utils/lib/tocase'
+import ruleList from '../../config/rule'
+import draggable from 'vuedraggable/src/vuedraggable'
+import createMenu from '../../config/menu'
+import { upper, useLocale } from '../../utils/formCreateIndex'
+import { designerForm } from '../../utils/form'
+import viewForm from '../../utils/form'
+import { t as globalT } from '../../utils/locale'
+import { computed, reactive, toRefs, toRef, ref, getCurrentInstance, provide, nextTick, watch, defineComponent, markRaw } from 'vue'
+
+export default defineComponent({
+	name: 'FcDesigner',
+	components: {
+		draggable,
+		DragForm: designerForm.$form(),
+		ViewForm: viewForm.$form()
+	},
+	props: ['menu', 'height', 'config', 'mask', 'locale'],
+	setup(props) {
+		const { menu, height, mask, locale } = toRefs(props)
+		const vm = getCurrentInstance()
+		provide('fcx', ref({ active: null }))
+		provide('designer', vm)
+
+		const config = toRef(props, 'config', {})
+		const baseRule = toRef(config.value, 'baseRule', null)
+		const componentRule = toRef(config.value, 'componentRule', {})
+		const validateRule = toRef(config.value, 'validateRule', null)
+		const formRule = toRef(config.value, 'formRule', null)
+		const dragHeight = computed(() => {
+			const h = height.value
+			if (!h) return '100%'
+			return is.Number(h) ? `${h}px` : h
+		})
+		let _t = globalT
+		if (locale.value) {
+			_t = useLocale(locale).t
+		}
+		const t = (...args) => _t(...args)
+
+		const tidyRuleConfig = (orgRule, configRule, ...args) => {
+			if (configRule) {
+				if (is.Function(configRule)) {
+					return configRule(...args)
+				}
+				if (configRule.rule) {
+					let rule = configRule.rule(...args)
+					if (configRule.append) {
+						rule = [...rule, ...orgRule(...args)]
+					}
+					return rule
+				}
+			}
+			return orgRule(...args)
+		}
+
+		const data = reactive({
+			cacheProps: {},
+			moveRule: null,
+			addRule: null,
+			added: null,
+			activeTab: 'form',
+			activeRule: null,
+			children: ref([]),
+			menuList: menu.value || createMenu({ t }),
+			showBaseRule: false,
+			visible: {
+				preview: false
+			},
+			t,
+			preview: {
+				state: false,
+				rule: [],
+				option: {}
+			},
+			dragForm: ref({
+				rule: [],
+				api: {}
+			}),
+			form: {
+				rule: tidyRuleConfig(form, formRule.value, { t }),
+				api: {},
+				option: {
+					form: {
+						labelPosition: 'top',
+						size: 'small'
+					},
+					submitBtn: false
+				},
+				value: {
+					form: {
+						inline: false,
+						hideRequiredAsterisk: false,
+						labelPosition: 'right',
+						size: 'small',
+						labelWidth: '125px',
+						formCreateSubmitBtn: true,
+						formCreateResetBtn: false
+					},
+					submitBtn: false
+				}
+			},
+			baseForm: {
+				rule: tidyRuleConfig(field, baseRule.value, { t }),
+				api: {},
+				value: {},
+				options: {
+					form: {
+						labelPosition: 'top',
+						size: 'small'
+					},
+					submitBtn: false,
+					mounted: fapi => {
+						fapi.activeRule = data.activeRule
+						fapi.setValue(fapi.options.formData || {})
+					}
+				}
+			},
+			validateForm: {
+				rule: tidyRuleConfig(validate, validateRule.value, { t }),
+				api: {},
+				value: [],
+				options: {
+					form: {
+						labelPosition: 'top',
+						size: 'small'
+					},
+					submitBtn: false,
+					mounted: fapi => {
+						fapi.activeRule = data.activeRule
+						fapi.setValue(fapi.options.formData || {})
+					}
+				}
+			},
+			propsForm: {
+				rule: [],
+				api: {},
+				value: {},
+				options: {
+					form: {
+						labelPosition: 'top',
+						size: 'small'
+					},
+					submitBtn: false,
+					mounted: fapi => {
+						fapi.activeRule = data.activeRule
+						fapi.setValue(fapi.options.formData || {})
+					}
+				}
+			}
+		})
+
+		watch(
+			() => data.preview.state,
+			function (n) {
+				if (!n) {
+					nextTick(() => {
+						data.preview.rule = data.preview.option = null
+					})
+				}
+			}
+		)
+
+		let unWatchActiveRule = null
+
+		watch(
+			() => locale.value,
+			n => {
+				_t = n ? useLocale(locale).t : globalT
+				const formVal = data.form.api.formData && data.form.api.formData()
+				const baseFormVal = data.baseForm.api.formData && data.baseForm.api.formData()
+				const validateFormVal = data.validateForm.api.formData && data.validateForm.api.formData()
+				data.validateForm.rule = tidyRuleConfig(validate, validateRule.value, { t })
+				data.baseForm.rule = tidyRuleConfig(field, baseRule.value, { t })
+				data.form.rule = tidyRuleConfig(form, formRule.value, { t })
+				data.cacheProps = {}
+				const rule = data.activeRule
+				let propsVal = null
+				if (rule) {
+					propsVal = data.propsForm.api.formData && data.propsForm.api.formData()
+					data.propsForm.rule = data.cacheProps[rule._id] = tidyRuleConfig(
+						rule.config.config.props,
+						componentRule.value && componentRule.value[rule.config.config.name],
+						rule,
+						{ t, api: data.dragForm.api }
+					)
+				}
+				nextTick(() => {
+					formVal && data.form.api.setValue(formVal)
+					baseFormVal && data.baseForm.api.setValue(baseFormVal)
+					validateFormVal && data.validateForm.api.setValue(validateFormVal)
+					propsVal && data.propsForm.api.setValue(propsVal)
+				})
+			}
+		)
+
+		const methods = {
+			unWatchActiveRule() {
+				unWatchActiveRule && unWatchActiveRule()
+				unWatchActiveRule = null
+			},
+			watchActiveRule() {
+				methods.unWatchActiveRule()
+				unWatchActiveRule = watch(
+					() => data.activeRule,
+					function (n) {
+						n && methods.updateRuleFormData()
+					},
+					{ deep: true, flush: 'post' }
+				)
+			},
+			makeChildren(children) {
+				return reactive({ children }).children
+			},
+			addMenu(config) {
+				if (!config.name || !config.list) return
+				let flag = true
+				data.menuList.forEach((v, i) => {
+					if (v.name === config.name) {
+						data.menuList[i] = config
+						flag = false
+					}
+				})
+				if (flag) {
+					data.menuList.push(config)
+				}
+			},
+			removeMenu(name) {
+				;[...data.menuList].forEach((v, i) => {
+					if (v.name === name) {
+						data.menuList.splice(i, 1)
+					}
+				})
+			},
+			setMenuItem(name, list) {
+				data.menuList.forEach(v => {
+					if (v.name === name) {
+						v.list = list
+					}
+				})
+			},
+			appendMenuItem(name, item) {
+				data.menuList.forEach(v => {
+					if (v.name === name) {
+						v.list.push(...(Array.isArray(item) ? item : [item]))
+					}
+				})
+			},
+			removeMenuItem(item) {
+				data.menuList.forEach(v => {
+					let idx
+					if (is.String(item)) {
+						;[...v.list].forEach((menu, idx) => {
+							if (menu.name === item) {
+								v.list.splice(idx, 1)
+							}
+						})
+					} else {
+						if ((idx = v.list.indexOf(item)) > -1) {
+							v.list.splice(idx, 1)
+						}
+					}
+				})
+			},
+			addComponent(component) {
+				if (Array.isArray(component)) {
+					component.forEach(v => {
+						ruleList[v.name] = v
+					})
+				} else {
+					ruleList[component.name] = component
+				}
+			},
+			getParent(rule) {
+				let parent = rule.__fc__.parent.rule
+				const config = parent.config
+				if (config && config.config.inside) {
+					rule = parent
+					parent = parent.__fc__.parent.rule
+				}
+				return { root: parent, parent: rule }
+			},
+			makeDrag(group, tag, children, on) {
+				return {
+					type: 'DragBox',
+					wrap: {
+						show: false
+					},
+					col: {
+						show: false
+					},
+					inject: true,
+					props: {
+						rule: {
+							props: {
+								tag: 'el-col',
+								group: group === true ? 'default' : group,
+								ghostClass: 'ghost',
+								animation: 150,
+								handle: '._fc-drag-btn',
+								emptyInsertThreshold: 0,
+								direction: 'vertical',
+								itemKey: 'type'
+							}
+						},
+						tag
+					},
+					children,
+					on
+				}
+			},
+			clearDragRule() {
+				methods.setRule([])
+			},
+			makeDragRule(children) {
+				return methods.makeChildren([
+					methods.makeDrag(true, 'draggable', children, {
+						add: (inject, evt) => methods.dragAdd(children, evt),
+						end: (inject, evt) => methods.dragEnd(children, evt),
+						start: (inject, evt) => methods.dragStart(children, evt),
+						unchoose: (inject, evt) => methods.dragUnchoose(children, evt)
+					})
+				])
+			},
+			previewFc() {
+				data.preview.state = true
+				data.preview.rule = methods.getRule()
+				data.preview.option = methods.getOption()
+			},
+			getRule() {
+				return methods.parseRule(deepCopy(data.dragForm.api.rule[0].children))
+			},
+			getJson() {
+				return designerForm.toJson(methods.getRule())
+			},
+			getOption() {
+				const option = deepCopy(data.form.value)
+				option.submitBtn = option._submitBtn
+				option.resetBtn = option._resetBtn
+				if (typeof option.submitBtn === 'object') {
+					option.submitBtn.show = option.form.formCreateSubmitBtn
+				} else {
+					option.submitBtn = {
+						show: option.form.formCreateSubmitBtn,
+						innerText: t('form.submit')
+					}
+				}
+				if (typeof option.resetBtn === 'object') {
+					option.resetBtn.show = option.form.formCreateResetBtn
+				} else {
+					option.resetBtn = {
+						show: option.form.formCreateResetBtn,
+						innerText: t('form.reset')
+					}
+				}
+				delete option.form.formCreateSubmitBtn
+				delete option.form.formCreateResetBtn
+				delete option._submitBtn
+				delete option._resetBtn
+				return option
+			},
+			getOptions() {
+				methods.getOption()
+			},
+			setRule(rules) {
+				if (!rules) {
+					rules = []
+				}
+				data.children = methods.makeChildren(methods.loadRule(is.String(rules) ? designerForm.parseJson(rules) : deepCopy(rules)))
+				methods.clearActiveRule()
+				data.dragForm.rule = methods.makeDragRule(data.children)
+			},
+			setBaseRuleConfig(rule, append) {
+				baseRule.value = { rule, append }
+				data.baseForm.rule = tidyRuleConfig(field, baseRule.value, { t })
+			},
+			setComponentRuleConfig(name, rule, append) {
+				componentRule.value[name] = { rule, append }
+				data.cacheProps = {}
+				const activeRule = data.activeRule
+				if (activeRule) {
+					const propsVal = data.propsForm.api.formData && data.propsForm.api.formData()
+					data.propsForm.rule = data.cacheProps[activeRule._id] = tidyRuleConfig(
+						activeRule.config.config.props,
+						componentRule.value && componentRule.value[activeRule.config.config.name],
+						activeRule,
+						{ t, api: data.dragForm.api }
+					)
+					nextTick(() => {
+						propsVal && data.propsForm.api.setValue(propsVal)
+					})
+				}
+			},
+			setValidateRuleConfig(rule, append) {
+				validateRule.value = { rule, append }
+				data.validateForm.rule = tidyRuleConfig(field, validateRule.value, { t })
+			},
+			setFormRuleConfig(rule, append) {
+				formRule.value = { rule, append }
+				data.form.rule = tidyRuleConfig(field, formRule.value, { t })
+			},
+			clearActiveRule() {
+				data.activeRule = null
+				data.activeTab = 'form'
+			},
+			setOption(opt) {
+				let option = { ...opt }
+				option.form.formCreateSubmitBtn =
+					typeof option.submitBtn === 'object' ? (option.submitBtn.show === undefined ? true : !!option.submitBtn.show) : !!option.submitBtn
+				option.form.formCreateResetBtn = typeof option.resetBtn === 'object' ? !!option.resetBtn.show : !!option.resetBtn
+				option._resetBtn = option.resetBtn
+				option.resetBtn = false
+				option._submitBtn = option.submitBtn
+				option.submitBtn = false
+				data.form.value = option
+			},
+			setOptions(opt) {
+				methods.setOption(opt)
+			},
+			loadRule(rules) {
+				const loadRule = []
+				rules.forEach(rule => {
+					if (is.String(rule)) {
+						return loadRule.push(rule)
+					}
+					const config = ruleList[rule._fc_drag_tag] || ruleList[rule.type]
+					const _children = rule.children
+					rule.children = []
+					if (rule.control) {
+						rule._control = rule.control
+						delete rule.control
+					}
+					if (config) {
+						rule = methods.makeRule(config, rule)
+						if (_children) {
+							let children = rule.children[0].children
+
+							if (config.drag) {
+								children = children[0].children
+							}
+							children.push(...methods.loadRule(_children))
+						}
+					} else if (_children) {
+						rule.children = methods.loadRule(_children)
+					}
+					loadRule.push(rule)
+				})
+				return loadRule
+			},
+			parseRule(children) {
+				return [...children].reduce((initial, rule) => {
+					if (is.String(rule)) {
+						initial.push(rule)
+						return initial
+					} else if (rule.type === 'DragBox') {
+						initial.push(...methods.parseRule(rule.children))
+						return initial
+					} else if (rule.type === 'DragTool') {
+						rule = rule.children[0]
+						if (rule.type === 'DragBox') {
+							initial.push(...methods.parseRule(rule.children))
+							return initial
+						}
+					}
+					if (!rule) return initial
+					rule = { ...rule }
+					if (rule.children.length) {
+						rule.children = methods.parseRule(rule.children)
+					}
+
+					delete rule._id
+					delete rule.key
+					delete rule.component
+					if (rule.config) {
+						delete rule.config.config
+					}
+					if (rule.effect) {
+						delete rule.effect._fc
+						delete rule.effect._fc_tool
+					}
+					if (rule._control) {
+						rule.control = rule._control
+						delete rule._control
+					}
+					Object.keys(rule)
+						.filter(k => (Array.isArray(rule[k]) && rule[k].length === 0) || (is.Object(rule[k]) && Object.keys(rule[k]).length === 0))
+						.forEach(k => {
+							delete rule[k]
+						})
+					initial.push(rule)
+					return initial
+				}, [])
+			},
+			baseChange(field, value, _, fapi) {
+				if (data.activeRule && fapi[data.activeRule._id] === data.activeRule) {
+					methods.unWatchActiveRule()
+					data.activeRule[field] = value
+					methods.watchActiveRule()
+					data.activeRule.config.config?.watch?.['$' + field]?.({
+						field,
+						value,
+						api: fapi,
+						rule: data.activeRule
+					})
+				}
+			},
+			propRemoveField(field, _, fapi) {
+				if (data.activeRule && fapi[data.activeRule._id] === data.activeRule) {
+					methods.unWatchActiveRule()
+					const org = field
+					data.dragForm.api.sync(data.activeRule)
+					if (field.indexOf('formCreate') === 0) {
+						field = field.replace('formCreate', '')
+						if (!field) return
+						field = lower(field)
+						if (field.indexOf('effect') === 0 && field.indexOf('>') > -1) {
+							delete data.activeRule.effect[field.split('>')[1]]
+						} else if (field.indexOf('props') === 0 && field.indexOf('>') > -1) {
+							delete data.activeRule.props[field.split('>')[1]]
+						} else if (field.indexOf('attrs') === 0 && field.indexOf('>') > -1) {
+							data.activeRule.attrs[field.split('>')[1]] = value
+						} else if (field === 'child') {
+							delete data.activeRule.children[0]
+						} else if (field) {
+							data.activeRule[field] = undefined
+						}
+					} else {
+						delete data.activeRule.props[field]
+					}
+					methods.watchActiveRule()
+					data.activeRule.config.config?.watch?.[org]?.({
+						field: org,
+						value: undefined,
+						api: fapi,
+						rule: data.activeRule
+					})
+				}
+			},
+			propChange(field, value, _, fapi) {
+				if (data.activeRule && fapi[data.activeRule._id] === data.activeRule) {
+					methods.unWatchActiveRule()
+					const org = field
+					if (field.indexOf('formCreate') === 0) {
+						field = field.replace('formCreate', '')
+						if (!field) return
+						field = lower(field)
+						if (field.indexOf('effect') === 0 && field.indexOf('>') > -1) {
+							data.activeRule.effect[field.split('>')[1]] = value
+						} else if (field.indexOf('props') === 0 && field.indexOf('>') > -1) {
+							data.activeRule.props[field.split('>')[1]] = value
+						} else if (field.indexOf('attrs') === 0 && field.indexOf('>') > -1) {
+							data.activeRule.attrs[field.split('>')[1]] = value
+						} else if (field === 'child') {
+							data.activeRule.children[0] = value
+						} else {
+							data.activeRule[field] = value
+						}
+					} else {
+						data.activeRule.props[field] = value
+					}
+					methods.watchActiveRule()
+					data.activeRule.config.config?.watch?.[org]?.({
+						field: org,
+						value,
+						api: fapi,
+						rule: data.activeRule
+					})
+				}
+			},
+			validateChange(formData) {
+				if (!data.activeRule || data.validateForm.api[data.activeRule._id] !== data.activeRule) return
+				data.activeRule.validate = formData.validate || []
+				data.dragForm.api.refreshValidate()
+				data.dragForm.api.nextTick(() => {
+					data.dragForm.api.clearValidateState(data.activeRule.__fc__.id)
+				})
+			},
+			toolActive(rule) {
+				methods.unWatchActiveRule()
+				if (data.activeRule) {
+					delete data.propsForm.api[data.activeRule._id]
+					delete data.baseForm.api[data.activeRule._id]
+					delete data.validateForm.api[data.activeRule._id]
+					delete data.dragForm.api.activeRule
+				}
+				data.activeRule = rule
+				data.dragForm.api.activeRule = rule
+
+				nextTick(() => {
+					data.activeTab = 'props'
+					nextTick(() => {
+						data.propsForm.api[data.activeRule._id] = data.activeRule
+						data.baseForm.api[data.activeRule._id] = data.activeRule
+						data.validateForm.api[data.activeRule._id] = data.activeRule
+					})
+				})
+				if (!data.cacheProps[rule._id]) {
+					data.cacheProps[rule._id] = tidyRuleConfig(
+						rule.config.config.props,
+						componentRule.value && componentRule.value[rule.config.config.name],
+						rule,
+						{ t, api: data.dragForm.api }
+					) // rule.config.config.props(rule, {t, api: data.dragForm.api});
+				}
+
+				data.propsForm.rule = data.cacheProps[rule._id]
+				methods.updateRuleFormData()
+				methods.watchActiveRule()
+			},
+			updateRuleFormData() {
+				const rule = data.activeRule
+				const formData = { ...rule.props, formCreateChild: deepCopy(rule.children[0]) }
+				Object.keys(rule).forEach(k => {
+					if (['effect', 'config', 'payload', 'id', 'type'].indexOf(k) < 0) formData['formCreate' + upper(k)] = deepCopy(rule[k])
+				})
+				;['props', 'effect', 'attrs'].forEach(name => {
+					rule[name] &&
+						Object.keys(rule[name]).forEach(k => {
+							formData['formCreate' + upper(name) + '>' + k] = deepCopy(rule[name][k])
+						})
+				})
+				data.propsForm.value = formData
+
+				data.showBaseRule = hasProperty(rule, 'field') && rule.input !== false && (!config.value || config.value.showBaseForm !== false)
+
+				if (data.showBaseRule) {
+					data.baseForm.value = {
+						field: rule.field,
+						title: rule.title || '',
+						info: rule.info,
+						_control: rule._control
+					}
+					data.validateForm.value = { validate: rule.validate ? [...rule.validate] : [] }
+					data.dragForm.api.refreshValidate()
+					data.dragForm.api.nextTick(() => {
+						data.dragForm.api.clearValidateState(rule.__fc__.id)
+					})
+				}
+			},
+			dragStart(children) {
+				data.moveRule = children
+				data.added = false
+			},
+			dragUnchoose(children, evt) {
+				data.addRule = {
+					children,
+					oldIndex: evt.oldIndex
+				}
+			},
+			dragAdd(children, evt) {
+				const newIndex = evt.newIndex
+				const menu = evt.item._underlying_vm_
+				if (!menu || menu.__fc__) {
+					if (data.addRule) {
+						const rule = data.addRule.children.splice(data.addRule.oldIndex, 1)
+						children.splice(newIndex, 0, rule[0])
+					}
+				} else {
+					const rule = methods.makeRule(ruleList[menu.name])
+					children.splice(newIndex, 0, rule)
+				}
+				data.added = true
+				// data.dragForm.api.refresh();
+			},
+			dragEnd(children, { newIndex, oldIndex }) {
+				if (!data.added && !(data.moveRule === children && newIndex === oldIndex)) {
+					const rule = data.moveRule.splice(oldIndex, 1)
+					children.splice(newIndex, 0, rule[0])
+				}
+				data.moveRule = null
+				data.addRule = null
+				data.added = false
+				// data.dragForm.api.refresh();
+			},
+			makeRule(config, _rule) {
+				const rule = _rule || config.rule({ t })
+				rule.config = { config }
+				if (config.component) {
+					rule.component = markRaw(config.component)
+				}
+				if (!rule.effect) rule.effect = {}
+				rule.effect._fc = true
+				rule._fc_drag_tag = config.name
+
+				let drag
+
+				if (config.drag) {
+					rule.children.push(
+						(drag = methods.makeDrag(config.drag, rule.type, methods.makeChildren([]), {
+							end: (inject, evt) => methods.dragEnd(inject.self.children, evt),
+							add: (inject, evt) => methods.dragAdd(inject.self.children, evt),
+							start: (inject, evt) => methods.dragStart(inject.self.children, evt),
+							unchoose: (inject, evt) => methods.dragUnchoose(inject.self.children, evt)
+						}))
+					)
+				}
+
+				if (config.children && !_rule) {
+					for (let i = 0; i < (config.childrenLen || 1); i++) {
+						const child = methods.makeRule(ruleList[config.children])
+						;(drag || rule).children.push(child)
+					}
+				}
+
+				const dragMask = mask.value !== undefined ? mask.value !== false : config.mask !== false
+
+				if (config.inside) {
+					rule.children = methods.makeChildren([
+						{
+							type: 'DragTool',
+							props: {
+								dragBtn: config.dragBtn !== false,
+								children: config.children,
+								mask: dragMask
+							},
+							effect: {
+								_fc_tool: true
+							},
+							inject: true,
+							on: {
+								delete: ({ self }) => {
+									const parent = methods.getParent(self).parent
+									parent.__fc__.rm()
+									vm.emit('delete', parent)
+									methods.clearActiveRule()
+								},
+								create: ({ self }) => {
+									const top = methods.getParent(self)
+									vm.emit('create', top.parent)
+									top.root.children.splice(top.root.children.indexOf(top.parent) + 1, 0, methods.makeRule(top.parent.config.config))
+								},
+								addChild: ({ self }) => {
+									const top = methods.getParent(self)
+									const config = top.parent.config.config
+									const item = ruleList[config.children]
+									if (!item) return
+									;(!config.drag ? top.parent : top.parent.children[0]).children[0].children.push(methods.makeRule(item))
+								},
+								copy: ({ self }) => {
+									const top = methods.getParent(self)
+									vm.emit('copy', top.parent)
+									top.root.children.splice(top.root.children.indexOf(top.parent) + 1, 0, designerForm.copyRule(top.parent))
+								},
+								active: ({ self }) => {
+									const top = methods.getParent(self)
+									vm.emit('active', top.parent)
+									methods.toolActive(top.parent)
+								}
+							},
+							children: rule.children
+						}
+					])
+					return rule
+				} else {
+					return {
+						type: 'DragTool',
+						props: {
+							dragBtn: config.dragBtn !== false,
+							children: config.children,
+							mask: dragMask
+						},
+						effect: {
+							_fc_tool: true
+						},
+						inject: true,
+						on: {
+							delete: ({ self }) => {
+								vm.emit('delete', self.children[0])
+								self.__fc__.rm()
+								methods.clearActiveRule()
+							},
+							create: ({ self }) => {
+								vm.emit('create', self.children[0])
+								const top = methods.getParent(self)
+								top.root.children.splice(top.root.children.indexOf(top.parent) + 1, 0, methods.makeRule(self.children[0].config.config))
+							},
+							addChild: ({ self }) => {
+								const config = self.children[0].config.config
+								const item = ruleList[config.children]
+								if (!item) return
+								;(!config.drag ? self : self.children[0]).children[0].children.push(methods.makeRule(item))
+							},
+							copy: ({ self }) => {
+								vm.emit('copy', self.children[0])
+								const top = methods.getParent(self)
+								top.root.children.splice(top.root.children.indexOf(top.parent) + 1, 0, designerForm.copyRule(top.parent))
+							},
+							active: ({ self }) => {
+								vm.emit('active', self.children[0])
+								methods.toolActive(self.children[0])
+							}
+						},
+						children: methods.makeChildren([rule])
+					}
+				}
+			}
+		}
+		data.dragForm.rule = methods.makeDragRule(methods.makeChildren(data.children))
+		return {
+			...toRefs(data),
+			...methods,
+			dragHeight,
+			t
+		}
+	},
+	created() {
+		document.body.ondrop = e => {
+			e.preventDefault()
+			e.stopPropagation()
+		}
+	}
+})
+</script>

+ 170 - 0
src/components/FormCreateDesigner/Fetch.vue

@@ -0,0 +1,170 @@
+<template>
+	<div class="_fc_fetch">
+		<DragForm v-model:api="api" :model-value="formValue" :rule="rule" :option="option" @change="input" />
+	</div>
+</template>
+
+<script>
+import debounce from '@form-create/utils/lib/debounce'
+import is from '@form-create/utils/lib/type'
+import { designerForm } from '../../utils/form'
+import { defineComponent } from 'vue'
+
+export default defineComponent({
+	name: 'Fetch',
+	props: {
+		modelValue: [Object, String],
+		to: String
+	},
+	components: {
+		DragForm: designerForm.$form()
+	},
+	inject: ['designer'],
+	data() {
+		const t = this.designer.setupState.t
+		return {
+			api: {},
+			fetch: {},
+			t,
+			option: {
+				form: {
+					labelPosition: 'right',
+					size: 'small',
+					labelWidth: '90px'
+				},
+				submitBtn: false
+			},
+			rule: [
+				{
+					type: 'input',
+					field: 'action',
+					title: t('fetch.action') + ': ',
+					validate: [{ required: true, message: t('fetch.actionRequired') }]
+				},
+				{
+					type: 'select',
+					field: 'method',
+					title: t('fetch.method') + ': ',
+					value: 'GET',
+					options: [
+						{ label: 'GET', value: 'GET' },
+						{ label: 'POST', value: 'POST' }
+					],
+					control: [
+						{
+							value: 'POST',
+							rule: [
+								{
+									type: 'select',
+									field: 'dataType',
+									title: t('fetch.dataType') + ': ',
+									value: 'FormData',
+									options: [
+										{ label: 'FormData', value: 'FormData' },
+										{ label: 'JSON', value: 'JSON' }
+									]
+								}
+							]
+						}
+					]
+				},
+				{
+					type: 'Struct',
+					field: 'data',
+					title: t('fetch.data') + ': ',
+					value: {},
+					props: {
+						defaultValue: {}
+					}
+				},
+				{
+					type: 'Struct',
+					field: 'headers',
+					title: t('fetch.headers') + ': ',
+					value: {},
+					props: {
+						defaultValue: {}
+					}
+				},
+				{
+					type: 'Struct',
+					field: 'parse',
+					title: t('fetch.parse') + ': ',
+					info: t('fetch.parseInfo'),
+					value: null,
+					props: {
+						defaultValue: function parse(res) {
+							return res
+						}
+					}
+				}
+				// {
+				//     type: 'input',
+				//     field: '_parse',
+				//     title: t('fetch.parse') + ': ',
+				//     info: t('fetch.parseInfo'),
+				//     value: 'function (res){\n   return res.data;\n}',
+				//     props: {
+				//         type: 'textarea',
+				//         rows: 8,
+				//     },
+				//     validate: [{
+				//         validator: (_, v, cb) => {
+				//             if (!v) return cb();
+				//             try {
+				//                 this.parseFn(v);
+				//             } catch (e) {
+				//                 return cb(false);
+				//             }
+				//             cb();
+				//         }, message: t('fetch.parseValidate')
+				//     }]
+				// },
+			]
+		}
+	},
+	computed: {
+		formValue() {
+			const val = this.modelValue
+			if (!val) return {}
+			if (is.String(val)) {
+				return {
+					action: val
+				}
+			}
+			return val
+		}
+	},
+	methods: {
+		parseFn(v) {
+			return new Function('return ' + v)()
+		},
+		_input() {
+			this.api.submit(formData => {
+				formData.to = this.to || 'options'
+				// if (formData._parse) formData.parse = this.parseFn(formData._parse);
+				this.$emit('update:modelValue', formData)
+			})
+		},
+		input: debounce(function () {
+			this._input()
+		}, 1000)
+	},
+	mounted() {
+		this._input()
+	}
+})
+</script>
+<style>
+._fc_fetch .el-form-item__label {
+	float: left;
+	display: inline-block;
+	text-align: right;
+	padding-right: 5px;
+}
+
+._fc_fetch {
+	background-color: #bfdaf7;
+	padding: 10px;
+}
+</style>

+ 16 - 0
src/components/FormCreateDesigner/IconRefresh.vue

@@ -0,0 +1,16 @@
+<template>
+	<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" data-v-029747aa="">
+		<path
+			fill="currentColor"
+			d="M771.776 794.88A384 384 0 0 1 128 512h64a320 320 0 0 0 555.712 216.448H654.72a32 32 0 1 1 0-64h149.056a32 32 0 0 1 32 32v148.928a32 32 0 1 1-64 0v-50.56zM276.288 295.616h92.992a32 32 0 0 1 0 64H220.16a32 32 0 0 1-32-32V178.56a32 32 0 0 1 64 0v50.56A384 384 0 0 1 896.128 512h-64a320 320 0 0 0-555.776-216.384z"
+		></path>
+	</svg>
+</template>
+
+<script>
+import { defineComponent } from 'vue'
+
+export default defineComponent({
+	name: 'IconRefresh'
+})
+</script>

+ 67 - 0
src/components/FormCreateDesigner/Required.vue

@@ -0,0 +1,67 @@
+<template>
+	<div class="_fc-required">
+		<ElSwitch v-model="required"></ElSwitch>
+		<ElInput v-if="required" v-model="requiredMsg" :placeholder="t('validate.requiredPlaceholder')"></ElInput>
+	</div>
+</template>
+
+<script>
+import is from '@form-create/utils/lib/type'
+import { defineComponent } from 'vue'
+
+export default defineComponent({
+	name: 'Required',
+	inject: ['designer'],
+	props: {
+		modelValue: {}
+	},
+	data() {
+		const flag = is.String(this.modelValue)
+		const t = this.designer.setupState.t
+		return {
+			t,
+			required: this.modelValue === undefined ? false : flag ? true : !!this.modelValue,
+			requiredMsg: flag ? this.modelValue : ''
+		}
+	},
+	watch: {
+		required() {
+			this.update()
+		},
+		requiredMsg() {
+			this.update()
+		},
+		modelValue(n) {
+			const flag = is.String(n)
+			this.required = n === undefined ? false : flag ? true : !!n
+			this.requiredMsg = flag ? n : ''
+		}
+	},
+	methods: {
+		update() {
+			let val
+			if (this.required === false) {
+				val = false
+			} else {
+				val = this.requiredMsg || true
+			}
+			this.$emit('update:modelValue', val)
+		}
+	}
+})
+</script>
+
+<style>
+._fc-required {
+	display: flex;
+	align-items: center;
+}
+
+._fc-required .el-input {
+	margin-left: 15px;
+}
+
+._fc-required .el-switch {
+	height: 28px;
+}
+</style>

+ 124 - 0
src/components/FormCreateDesigner/Struct.vue

@@ -0,0 +1,124 @@
+<template>
+	<div class="_fc_struct">
+		<ElButton style="width: 100%" @click="visible = true">{{ title || t('struct.title') }}</ElButton>
+		<ElDialog v-model="visible" :title="title || t('struct.title')" :close-on-click-modal="false" append-to-body>
+			<div v-if="visible" ref="editor"></div>
+			<template #footer>
+				<span class="dialog-footer">
+					<span v-if="err" class="_fc_err"> {{ t('struct.error') }}{{ err !== true ? err : '' }}</span>
+					<ElButton size="small" @click="visible = false">{{ t('struct.cancel') }}</ElButton>
+					<ElButton type="primary" size="small" @click="onOk">{{ t('struct.submit') }}</ElButton>
+				</span>
+			</template>
+		</ElDialog>
+	</div>
+</template>
+
+<script>
+import 'codemirror/lib/codemirror.css'
+import CodeMirror from 'codemirror/lib/codemirror'
+import 'codemirror/mode/javascript/javascript'
+import { deepParseFn, toJSON } from '../../utils/formCreateIndex'
+import { deepCopy } from '@form-create/utils/lib/deepextend'
+import { defineComponent } from 'vue'
+
+export default defineComponent({
+	name: 'Struct',
+	inject: ['designer'],
+	props: {
+		modelValue: [Object, Array, Function],
+		title: String,
+		defaultValue: {
+			require: false
+		},
+		validate: Function
+	},
+	data() {
+		return {
+			editor: null,
+			visible: false,
+			err: false,
+			oldVal: null,
+			t: this.designer.setupState.t
+		}
+	},
+	watch: {
+		modelValue() {
+			this.load()
+		},
+		visible(n) {
+			if (n) {
+				this.load()
+			} else {
+				this.err = false
+			}
+		}
+	},
+	methods: {
+		load() {
+			const val = toJSON(deepParseFn(this.modelValue ? deepCopy(this.modelValue) : this.defaultValue))
+			this.oldVal = val
+			this.$nextTick(() => {
+				this.editor = CodeMirror(this.$refs.editor, {
+					lineNumbers: true,
+					mode: 'javascript',
+					gutters: ['CodeMirror-lint-markers'],
+					lint: true,
+					line: true,
+					tabSize: 2,
+					lineWrapping: true,
+					value: val || ''
+				})
+			})
+		},
+		onOk() {
+			const str = this.editor.getValue()
+			let val
+			try {
+				val = new Function('return ' + str)()
+			} catch (e) {
+				this.err = ` (${e})`
+				return
+			}
+			if (this.validate && false === this.validate(val)) {
+				this.err = true
+				return
+			}
+			this.visible = false
+			if (toJSON(val, null, 2) !== this.oldVal) {
+				this.$emit('update:modelValue', val)
+			}
+		}
+	}
+})
+</script>
+
+<style>
+._fc_struct {
+	width: 100%;
+}
+
+._fc_struct .CodeMirror {
+	height: 450px;
+}
+
+._fc_struct .CodeMirror-line {
+	line-height: 16px !important;
+	font-size: 13px !important;
+}
+
+.CodeMirror-lint-tooltip {
+	z-index: 2021 !important;
+}
+
+._fc_struct .el-dialog__body {
+	padding: 0px 20px;
+}
+
+._fc_err {
+	color: red;
+	float: left;
+	text-align: left;
+	width: 65%;
+}
+</style>

+ 78 - 0
src/components/FormCreateDesigner/TableOptions.vue

@@ -0,0 +1,78 @@
+<template>
+	<div class="_fc_table_opt">
+		<el-table :data="modelValue" border size="small" style="width: 100%">
+			<template v-for="(col, idx) in column" :key="col.label + idx">
+				<el-table-column :label="col.label">
+					<template #default="scope">
+						<el-input
+							size="small"
+							:model-value="scope.row[col.key] || ''"
+							@Update:modelValue="n => ((scope.row[col.key] = n), onInput(scope.row))"
+						></el-input>
+					</template>
+				</el-table-column>
+			</template>
+			<el-table-column min-width="50" align="center" fixed="right" :label="t('tableOptions.handle')">
+				<template #default="scope">
+					<i class="fc-icon icon-delete" @click="del(scope.$index)"></i>
+				</template>
+			</el-table-column>
+		</el-table>
+		<el-button link type="primary" @click="add"> <i class="fc-icon icon-add"></i> {{ t('tableOptions.add') }} </el-button>
+	</div>
+</template>
+
+<script>
+import { defineComponent } from 'vue'
+
+export default defineComponent({
+	name: 'TableOptions',
+	inject: ['designer'],
+	inheritAttrs: false,
+	props: {
+		modelValue: [Object, Array, String]
+	},
+	data() {
+		return {
+			column: [
+				{ label: 'label', key: 'label' },
+				{ label: 'value', key: 'value' }
+			],
+			t: this.designer.setupState.t
+		}
+	},
+	created() {
+		if (!Array.isArray(this.modelValue)) {
+			this.$emit('input', [])
+		}
+	},
+	methods: {
+		onInput(item) {
+			if (item.label && item.value) {
+				this.input()
+			}
+		},
+		input() {
+			this.$emit('update:modelValue', this.modelValue)
+		},
+		add() {
+			this.modelValue.push(
+				this.column.reduce((initial, v) => {
+					initial[v.key] = ''
+					return initial
+				}, {})
+			)
+		},
+		del(idx) {
+			this.modelValue.splice(idx, 1)
+			this.input(this.modelValue)
+		}
+	}
+})
+</script>
+
+<style scoped>
+._fc_table_opt {
+	width: 100%;
+}
+</style>

+ 236 - 0
src/components/FormCreateDesigner/Validate.vue

@@ -0,0 +1,236 @@
+<template>
+	<DragForm class="_fc-validate" :rule="rule" :option="option" :model-value="formValue" @update:modelValue="onInput"></DragForm>
+</template>
+
+<script>
+import { designerForm } from '../../utils/form'
+import { defineComponent } from 'vue'
+import { deepCopy } from '@form-create/utils/lib/deepextend'
+
+export default defineComponent({
+	name: 'Validate',
+	inject: ['designer'],
+	props: {
+		modelValue: Array
+	},
+	components: {
+		DragForm: designerForm.$form()
+	},
+	data() {
+		const t = this.designer.setupState.t
+		return {
+			formValue: {},
+			t,
+			option: {
+				form: {
+					labelPosition: 'top',
+					size: 'small',
+					labelWidth: '90px'
+				},
+				submitBtn: false,
+				appendValue: true,
+				formData: this.parseValue(this.modelValue)
+			},
+			rule: [
+				{
+					type: 'select',
+					field: 'type',
+					value: '',
+					title: t('validate.type'),
+					options: [
+						{ value: '', label: t('validate.typePlaceholder') },
+						{ value: 'string', label: 'String' },
+						{ value: 'array', label: 'Array' },
+						{ value: 'number', label: 'Number' },
+						{ value: 'integer', label: 'Integer' },
+						{ value: 'float', label: 'Float' },
+						{ value: 'object', label: 'Object' },
+						{ value: 'date', label: 'Date' },
+						{ value: 'url', label: 'url' },
+						{ value: 'hex', label: 'hex' },
+						{ value: 'email', label: 'email' }
+					],
+					control: [
+						{
+							handle: v => {
+								return !!v
+							},
+							rule: [
+								{
+									type: 'group',
+									field: 'validate',
+									props: {
+										expand: 1,
+										sortBtn: false,
+										rule: [
+											{
+												type: 'select',
+												title: t('validate.trigger'),
+												field: 'trigger',
+												value: 'change',
+												options: [
+													{ label: 'change', value: 'change' },
+													{ label: 'submit', value: 'submit' },
+													{ label: 'blur', value: 'blur' }
+												]
+											},
+											{
+												type: 'hidden',
+												field: 'validator',
+												value: undefined
+											},
+											{
+												type: 'select',
+												title: t('validate.mode'),
+												field: 'mode',
+												options: [
+													{ value: 'required', label: t('validate.modes.required') },
+													{ value: 'pattern', label: t('validate.modes.pattern') },
+													{ value: 'min', label: t('validate.modes.min') },
+													{ value: 'max', label: t('validate.modes.max') },
+													{ value: 'len', label: t('validate.modes.len') }
+												],
+												value: 'required',
+												control: [
+													{
+														value: 'required',
+														rule: [
+															{
+																type: 'hidden',
+																field: 'required',
+																value: true
+															}
+														]
+													},
+													{
+														value: 'pattern',
+														rule: [
+															{
+																type: 'input',
+																field: 'pattern',
+																title: t('validate.modes.pattern')
+															}
+														]
+													},
+													{
+														value: 'min',
+														rule: [
+															{
+																type: 'inputNumber',
+																field: 'min',
+																title: t('validate.modes.min')
+															}
+														]
+													},
+													{
+														value: 'max',
+														rule: [
+															{
+																type: 'inputNumber',
+																field: 'max',
+																title: t('validate.modes.max')
+															}
+														]
+													},
+													{
+														value: 'len',
+														rule: [
+															{
+																type: 'inputNumber',
+																field: 'len',
+																title: t('validate.modes.len')
+															}
+														]
+													}
+												]
+											},
+											{
+												type: 'input',
+												title: t('validate.message'),
+												field: 'message',
+												value: '',
+												children: [
+													{
+														type: 'span',
+														slot: 'append',
+														inject: true,
+														class: 'append-msg',
+														on: {
+															click: inject => {
+																const title = this.designer.setupState.activeRule.title
+																if (this.designer.setupState.activeRule) {
+																	inject.api.setValue(
+																		'message',
+																		t(inject.api.form.mode !== 'required' ? 'validate.autoMode' : 'validate.autoRequired', { title })
+																	)
+																}
+															}
+														},
+														children: [t('validate.auto')]
+													}
+												]
+											}
+										]
+									},
+									value: []
+								}
+							]
+						}
+					]
+				}
+			]
+		}
+	},
+	watch: {
+		modelValue(n) {
+			this.formValue = this.parseValue(n)
+		}
+	},
+	methods: {
+		onInput: function (formData) {
+			let val = []
+			const { validate, type } = deepCopy(formData)
+			if (type && (!validate || !validate.length)) {
+				return
+			} else if (type) {
+				validate.forEach(v => {
+					v.type = type
+				})
+				val = [...validate]
+			}
+			this.$emit('update:modelValue', val)
+		},
+		parseValue(n) {
+			let val = {
+				validate: n ? [...n] : [],
+				type: n.length ? n[0].type || 'string' : undefined
+			}
+			val.validate.forEach(v => {
+				if (!v.mode) {
+					Object.keys(v).forEach(k => {
+						if (['message', 'type', 'trigger', 'mode'].indexOf(k) < 0) {
+							v.mode = k
+						}
+					})
+				}
+			})
+
+			return val
+		}
+	}
+})
+</script>
+
+<style>
+._fc-validate .form-create .el-form-item {
+	margin-bottom: 22px !important;
+}
+
+._fc-validate .append-msg {
+	cursor: pointer;
+}
+
+._fc-validate .el-input-group__append {
+	padding: 0 10px;
+}
+</style>

+ 21 - 1
src/components/registerGlobComp.ts

@@ -15,6 +15,17 @@ import Icon from '@/components/Icon.vue'
 import Select from '@/components/Select/index.vue'
 import Chart from '@/components/Chart.vue'
 
+// 表单设计器
+import DragTool from '@/components/FormCreateDesigner/DragTool.vue'
+import Struct from '@/components/FormCreateDesigner/Struct.vue'
+import Fetch from '@/components/FormCreateDesigner/Fetch.vue'
+import Validate from '@/components/FormCreateDesigner/Validate.vue'
+import DragBox from '@/components/FormCreateDesigner/DragBox.vue'
+import Required from '@/components/FormCreateDesigner/Required.vue'
+import TableOptions from '@/components/FormCreateDesigner/TableOptions.vue'
+import FcEditor from '@form-create/component-wangeditor'
+import draggable from 'vuedraggable/src/vuedraggable'
+
 // you want register components
 const compList = [
 	SvgIcon,
@@ -29,7 +40,16 @@ const compList = [
 	NoData,
 	Icon,
 	Select,
-	Chart
+	Chart,
+	DragTool,
+	Struct,
+	Fetch,
+	Validate,
+	DragBox,
+	Required,
+	TableOptions,
+	FcEditor,
+	draggable
 ]
 
 export function registerGlobComp(app: App) {

+ 89 - 0
src/config/base/field.ts

@@ -0,0 +1,89 @@
+import IconRefresh from '../../components/FormCreateDesigner/IconRefresh.vue'
+import { markRaw } from 'vue'
+
+export default function field({ t }) {
+	return [
+		{
+			type: 'input',
+			field: 'field',
+			value: '',
+			title: t('form.field')
+		},
+		{
+			type: 'input',
+			field: 'title',
+			value: '',
+			title: t('form.title')
+		},
+		{
+			type: 'input',
+			field: 'info',
+			value: '',
+			title: t('form.info')
+		},
+		{
+			type: 'Struct',
+			field: '_control',
+			value: [],
+			title: t('form.control'),
+			props: {
+				defaultValue: [],
+				validate(val) {
+					if (!Array.isArray(val)) return false
+					if (!val.length) return true
+					return !val.some(({ rule }) => {
+						return !Array.isArray(rule)
+					})
+				}
+			}
+		},
+		{
+			type: 'col',
+			props: {
+				span: 24
+			},
+			children: [
+				{
+					type: 'el-button',
+					props: {
+						type: 'primary',
+						size: 'small'
+					},
+					inject: true,
+					on: {
+						click({ $f }) {
+							const rule = $f.activeRule
+							if (rule) {
+								rule.__fc__.updateKey()
+								rule.value = undefined
+								rule.__fc__.$api.sync(rule)
+							}
+						}
+					},
+					native: true,
+					children: [{ type: 'i', class: 'fc-icon icon-delete' }, t('form.clear')]
+				},
+				{
+					type: 'el-button',
+					props: {
+						type: 'success',
+						size: 'small',
+						icon: markRaw(IconRefresh)
+					},
+					inject: true,
+					on: {
+						click({ $f }) {
+							const rule = $f.activeRule
+							if (rule) {
+								rule.__fc__.updateKey(true)
+								rule.__fc__.$api.sync(rule)
+							}
+						}
+					},
+					native: true,
+					children: [t('form.refresh')]
+				}
+			]
+		}
+	]
+}

+ 62 - 0
src/config/base/form.ts

@@ -0,0 +1,62 @@
+export default function form({ t }) {
+	return [
+		{
+			type: 'radio',
+			field: 'labelPosition',
+			value: 'left',
+			title: t('form.labelPosition'),
+			options: [
+				{ value: 'right', label: 'right' },
+				{ value: 'left', label: 'left' },
+				{ value: 'top', label: 'top' }
+			]
+		},
+		{
+			type: 'radio',
+			field: 'size',
+			value: 'small',
+			title: t('form.size'),
+			options: [
+				{ value: 'large', label: 'large' },
+				{ value: 'default', label: 'default' },
+				{ value: 'small', label: 'small' }
+			]
+		},
+		{
+			type: 'input',
+			field: 'labelWidth',
+			value: '125px',
+			title: t('form.labelWidth')
+		},
+		{
+			type: 'switch',
+			field: 'hideRequiredAsterisk',
+			value: false,
+			title: t('form.hideRequiredAsterisk')
+		},
+		{
+			type: 'switch',
+			field: 'showMessage',
+			value: true,
+			title: t('form.showMessage')
+		},
+		{
+			type: 'switch',
+			field: 'inlineMessage',
+			value: false,
+			title: t('form.inlineMessage')
+		},
+		{
+			type: 'switch',
+			field: 'formCreateSubmitBtn',
+			value: true,
+			title: t('form.submitBtn')
+		},
+		{
+			type: 'switch',
+			field: 'formCreateResetBtn',
+			value: false,
+			title: t('form.resetBtn')
+		}
+	]
+}

+ 9 - 0
src/config/base/validate.ts

@@ -0,0 +1,9 @@
+export default function validate() {
+	return [
+		{
+			type: 'validate',
+			field: 'validate',
+			value: []
+		}
+	]
+}

+ 43 - 0
src/config/menu.ts

@@ -0,0 +1,43 @@
+import radio from './rule/radio'
+import checkbox from './rule/checkbox'
+import input from './rule/input'
+import number from './rule/number'
+import select from './rule/select'
+import _switch from './rule/switch'
+import slider from './rule/slider'
+import time from './rule/time'
+import date from './rule/date'
+import rate from './rule/rate'
+import color from './rule/color'
+import row from './rule/row'
+import divider from './rule/divider'
+import cascader from './rule/cascader'
+import upload from './rule/upload'
+import transfer from './rule/transfer'
+import tree from './rule/tree'
+import alert from './rule/alert'
+import span from './rule/span'
+import space from './rule/space'
+import button from './rule/button'
+import editor from './rule/editor'
+import tab from './rule/tab'
+
+export default function createMenu({ t }) {
+	return [
+		{
+			name: 'main',
+			title: t('menu.main'),
+			list: [input, number, radio, checkbox, select, _switch, time, date, slider, rate, color, cascader, upload, transfer, tree, editor]
+		},
+		{
+			name: 'aide',
+			title: t('menu.aide'),
+			list: [alert, button, span, divider]
+		},
+		{
+			name: 'layout',
+			title: t('menu.layout'),
+			list: [row, tab, space]
+		}
+	]
+}

+ 64 - 0
src/config/rule/alert.ts

@@ -0,0 +1,64 @@
+import { localeProps } from '../../utils/formCreateIndex'
+
+const label = '提示'
+const name = 'el-alert'
+
+export default {
+	icon: 'icon-alert',
+	label,
+	name,
+	rule({ t }) {
+		return {
+			type: name,
+			props: {
+				title: t('components.el-alert.name'),
+				description: t('components.el-alert.description'),
+				type: 'success',
+				effect: 'dark'
+			},
+			children: []
+		}
+	},
+	props(_, { t }) {
+		return localeProps(t, name + '.props', [
+			{ type: 'input', field: 'title', title: '标题' },
+			{
+				type: 'select',
+				field: 'type',
+				title: '主题',
+				options: [
+					{ label: 'success', value: 'success' },
+					{ label: 'warning', value: 'warning' },
+					{
+						label: 'info',
+						value: 'info'
+					},
+					{ label: 'error', value: 'error' }
+				]
+			},
+			{ type: 'input', field: 'description', title: '辅助性文字' },
+			{
+				type: 'switch',
+				field: 'closable',
+				title: '是否可关闭',
+				value: true
+			},
+			{ type: 'switch', field: 'center', title: '文字是否居中', value: true },
+			{
+				type: 'input',
+				field: 'closeText',
+				title: '关闭按钮自定义文本'
+			},
+			{ type: 'switch', field: 'showIcon', title: '是否显示图标' },
+			{
+				type: 'select',
+				field: 'effect',
+				title: '选择提供的主题',
+				options: [
+					{ label: 'light', value: 'light' },
+					{ label: 'dark', value: 'dark' }
+				]
+			}
+		])
+	}
+}

+ 76 - 0
src/config/rule/button.ts

@@ -0,0 +1,76 @@
+import { localeProps } from '../../utils/formCreateIndex'
+
+const label = '按钮'
+const name = 'el-button'
+
+export default {
+	icon: 'icon-button',
+	label,
+	name,
+	mask: false,
+	rule({ t }) {
+		return {
+			type: name,
+			props: {},
+			children: [t('components.el-button.name')]
+		}
+	},
+	props(_, { t }) {
+		return localeProps(t, name + '.props', [
+			{
+				type: 'input',
+				field: 'formCreateChild',
+				title: '内容'
+			},
+			{
+				type: 'select',
+				field: 'size',
+				title: '尺寸',
+				options: [
+					{ label: 'large', value: 'large' },
+					{ label: 'default', value: 'default' },
+					{
+						label: 'small',
+						value: 'small'
+					}
+				]
+			},
+			{
+				type: 'select',
+				field: 'type',
+				title: '类型',
+				options: [
+					{ label: 'primary', value: 'primary' },
+					{
+						label: 'success',
+						value: 'success'
+					},
+					{ label: 'warning', value: 'warning' },
+					{ label: 'danger', value: 'danger' },
+					{
+						label: 'info',
+						value: 'info'
+					}
+				]
+			},
+			{ type: 'switch', field: 'plain', title: '是否朴素按钮' },
+			{
+				type: 'switch',
+				field: 'round',
+				title: '是否圆角按钮'
+			},
+			{ type: 'switch', field: 'circle', title: '是否圆形按钮' },
+			{
+				type: 'switch',
+				field: 'loading',
+				title: '是否加载中状态'
+			},
+			{ type: 'switch', field: 'disabled', title: '是否禁用状态' },
+			{
+				type: 'input',
+				field: 'icon',
+				title: '图标类名'
+			}
+		])
+	}
+}

+ 116 - 0
src/config/rule/cascader.ts

@@ -0,0 +1,116 @@
+import uniqueId from '@form-create/utils/lib/unique'
+import { localeProps, makeOptionsRule, makeRequiredRule } from '../../utils/formCreateIndex'
+
+const label = '级联选择器'
+const name = 'cascader'
+
+export default {
+	icon: 'icon-cascader',
+	label,
+	name,
+	rule({ t }) {
+		const opt = t('props.option')
+		return {
+			type: name,
+			field: uniqueId(),
+			title: t('components.cascader.name'),
+			info: '',
+			effect: {
+				fetch: ''
+			},
+			$required: false,
+			props: {
+				options: [1, 2].map(value => {
+					return {
+						label: opt + value,
+						value,
+						children: []
+					}
+				})
+			}
+		}
+	},
+	props(_, { t }) {
+		return localeProps(t, name + '.props', [
+			makeRequiredRule(),
+			makeOptionsRule(t, 'props.options', false),
+			{
+				type: 'Object',
+				field: 'props',
+				title: '配置选项',
+				props: {
+					rule: localeProps(t, name + '.propsOpt', [
+						{
+							type: 'select',
+							field: 'expandTrigger',
+							title: '次级菜单的展开方式',
+							options: [
+								{ label: 'click', value: 'click' },
+								{ label: 'hover', value: 'hover' }
+							]
+						},
+						{ type: 'switch', field: 'multiple', title: '是否多选' },
+						{
+							type: 'switch',
+							field: 'checkStrictly',
+							title: '是否严格的遵守父子节点不互相关联'
+						},
+						{
+							type: 'switch',
+							field: 'emitPath',
+							title: '在选中节点改变时,是否返回由该节点所在的各级菜单的值所组成的数组,若设置 false,则只返回该节点的值',
+							value: true
+						},
+						{ type: 'input', field: 'value', title: '指定选项的值为选项对象的某个属性值', value: 'value' },
+						{
+							type: 'input',
+							field: 'label',
+							title: '指定选项标签为选项对象的某个属性值',
+							value: 'label'
+						},
+						{ type: 'input', field: 'children', title: '指定选项的子选项为选项对象的某个属性值', value: 'children' },
+						{
+							type: 'input',
+							field: 'disabled',
+							title: '指定选项的禁用为选项对象的某个属性值',
+							value: 'disabled'
+						},
+						{ type: 'input', field: 'leaf', title: '指定选项的叶子节点的标志位为选项对象的某个属性值' }
+					])
+				}
+			},
+			{
+				type: 'select',
+				field: 'size',
+				title: '尺寸',
+				options: [
+					{ label: 'large', value: 'large' },
+					{ label: 'default', value: 'default' },
+					{
+						label: 'small',
+						value: 'small'
+					}
+				]
+			},
+			{ type: 'input', field: 'placeholder', title: '输入框占位文本' },
+			{
+				type: 'switch',
+				field: 'disabled',
+				title: '是否禁用'
+			},
+			{ type: 'switch', field: 'clearable', title: '是否支持清空选项' },
+			{
+				type: 'switch',
+				field: 'showAllLevels',
+				title: '输入框中是否显示选中值的完整路径',
+				value: true
+			},
+			{ type: 'switch', field: 'collapseTags', title: '多选模式下是否折叠Tag' },
+			{
+				type: 'input',
+				field: 'separator',
+				title: '选项分隔符'
+			}
+		])
+	}
+}

+ 57 - 0
src/config/rule/checkbox.ts

@@ -0,0 +1,57 @@
+import uniqueId from '@form-create/utils/lib/unique'
+import { localeProps, makeOptionsRule, makeRequiredRule } from '../../utils/formCreateIndex'
+
+const label = '多选框'
+const name = 'checkbox'
+
+export default {
+	icon: 'icon-checkbox',
+	label,
+	name,
+	rule({ t }) {
+		const opt = t('props.option')
+		return {
+			type: name,
+			field: uniqueId(),
+			title: t('components.checkbox.name'),
+			info: '',
+			effect: {
+				fetch: ''
+			},
+			$required: false,
+			props: {},
+			options: [1, 2].map(value => {
+				return {
+					label: opt + value,
+					value
+				}
+			})
+		}
+	},
+	props(_, { t }) {
+		return localeProps(t, name + '.props', [
+			makeRequiredRule(),
+			makeOptionsRule(t, 'options'),
+			{
+				type: 'switch',
+				field: 'type',
+				title: '按钮类型',
+				props: { activeValue: 'button', inactiveValue: 'default' }
+			},
+			{ type: 'switch', field: 'disabled', title: '是否禁用' },
+			{
+				type: 'inputNumber',
+				field: 'min',
+				title: '可被勾选的 checkbox 的最小数量',
+				props: { min: 0 }
+			},
+			{ type: 'inputNumber', field: 'max', title: '可被勾选的 checkbox 的最大数量', props: { min: 0 } },
+			{
+				type: 'input',
+				field: 'textColor',
+				title: '按钮形式的 Checkbox 激活时的文本颜色'
+			},
+			{ type: 'input', field: 'fill', title: '按钮形式的 Checkbox 激活时的填充色和边框色' }
+		])
+	}
+}

+ 27 - 0
src/config/rule/col.ts

@@ -0,0 +1,27 @@
+import { localeProps } from '../../utils/formCreateIndex'
+
+const name = 'col'
+
+export default {
+	name,
+	label: '格子',
+	drag: true,
+	dragBtn: false,
+	inside: true,
+	mask: false,
+	rule() {
+		return {
+			type: name,
+			props: { span: 12 },
+			children: []
+		}
+	},
+	props(_, { t }) {
+		return localeProps(t, name + '.props', [
+			{ type: 'slider', field: 'span', title: '栅格占据的列数', value: 12, props: { min: 0, max: 24 } },
+			{ type: 'slider', field: 'offset', title: '栅格左侧的间隔格数', props: { min: 0, max: 24 } },
+			{ type: 'slider', field: 'push', title: '栅格向右移动格数', props: { min: 0, max: 24 } },
+			{ type: 'slider', field: 'pull', title: '栅格向左移动格数', props: { min: 0, max: 24 } }
+		])
+	}
+}

+ 50 - 0
src/config/rule/color.ts

@@ -0,0 +1,50 @@
+import uniqueId from '@form-create/utils/lib/unique'
+import { localeProps, makeRequiredRule } from '../../utils/formCreateIndex'
+
+const label = '颜色选择器'
+const name = 'colorPicker'
+
+export default {
+	icon: 'icon-color',
+	label,
+	name,
+	rule({ t }) {
+		return {
+			type: name,
+			field: uniqueId(),
+			title: t('components.colorPicker.name'),
+			info: '',
+			$required: false,
+			props: {}
+		}
+	},
+	props(_, { t }) {
+		return localeProps(t, name + '.props', [
+			makeRequiredRule(),
+			{
+				type: 'switch',
+				field: 'disabled',
+				title: '是否禁用'
+			},
+			{
+				type: 'switch',
+				field: 'showAlpha',
+				title: '是否支持透明度选择'
+			},
+			{
+				type: 'select',
+				field: 'colorFormat',
+				title: '颜色的格式',
+				options: [
+					{ label: 'hsl', value: 'hsl' },
+					{ label: 'hsv', value: 'hsv' },
+					{
+						label: 'hex',
+						value: 'hex'
+					},
+					{ label: 'rgb', value: 'rgb' }
+				]
+			}
+		])
+	}
+}

+ 108 - 0
src/config/rule/date.ts

@@ -0,0 +1,108 @@
+import uniqueId from '@form-create/utils/lib/unique'
+import { localeProps, makeRequiredRule } from '../../utils/formCreateIndex'
+
+const label = '日期选择器'
+const name = 'datePicker'
+
+export default {
+	icon: 'icon-date',
+	label,
+	name,
+	rule({ t }) {
+		return {
+			type: name,
+			field: uniqueId(),
+			title: t('components.datePicker.name'),
+			info: '',
+			$required: false,
+			props: {}
+		}
+	},
+	props(_, { t }) {
+		return localeProps(t, name + '.props', [
+			makeRequiredRule(),
+			{
+				type: 'Struct',
+				field: 'pickerOptions',
+				title: '当前时间日期选择器特有的选项',
+				props: { defaultValue: {} }
+			},
+			{ type: 'switch', field: 'readonly', title: '完全只读' },
+			{
+				type: 'switch',
+				field: 'disabled',
+				title: '禁用'
+			},
+			{
+				type: 'select',
+				field: 'type',
+				title: '显示类型',
+				options: [
+					{ label: 'year', value: 'year' },
+					{ label: 'month', value: 'month' },
+					{
+						label: 'date',
+						value: 'date'
+					},
+					{ label: 'dates', value: 'dates' },
+					{ label: 'week', value: 'week' },
+					{
+						label: 'datetime',
+						value: 'datetime'
+					},
+					{ label: 'datetimerange', value: 'datetimerange' },
+					{
+						label: 'daterange',
+						value: 'daterange'
+					},
+					{ label: 'monthrange', value: 'monthrange' }
+				]
+			},
+			{ type: 'switch', field: 'editable', title: '文本框可输入', value: true },
+			{
+				type: 'switch',
+				field: 'clearable',
+				title: '是否显示清除按钮',
+				value: true
+			},
+			{ type: 'input', field: 'placeholder', title: '非范围选择时的占位内容' },
+			{
+				type: 'input',
+				field: 'startPlaceholder',
+				title: '范围选择时开始日期的占位内容'
+			},
+			{ type: 'input', field: 'endPlaceholder', title: '范围选择时结束日期的占位内容' },
+			{
+				type: 'input',
+				field: 'format',
+				title: '显示在输入框中的格式'
+			},
+			{
+				type: 'select',
+				field: 'align',
+				title: '对齐方式',
+				options: [
+					{ label: 'left', value: 'left' },
+					{ label: 'center', value: 'center' },
+					{
+						label: 'right',
+						value: 'right'
+					},
+					{ label: 'left', value: 'left' }
+				]
+			},
+			{ type: 'input', field: 'rangeSeparator', title: '选择范围时的分隔符' },
+			{
+				type: 'switch',
+				field: 'unlinkPanels',
+				title: '在范围选择器里取消两个日期面板之间的联动'
+			},
+			{ type: 'input', field: 'prefixIcon', title: '自定义头部图标的类名' },
+			{
+				type: 'input',
+				field: 'clearIcon',
+				title: '自定义清空图标的类名'
+			}
+		])
+	}
+}

+ 50 - 0
src/config/rule/divider.ts

@@ -0,0 +1,50 @@
+import { localeProps } from '../../utils/formCreateIndex'
+
+const label = '分割线'
+const name = 'el-divider'
+
+export default {
+	icon: 'icon-divider',
+	label,
+	name,
+	rule() {
+		return {
+			type: name,
+			props: {},
+			wrap: { show: false },
+			native: false,
+			children: ['']
+		}
+	},
+	props(_, { t }) {
+		return localeProps(t, name + '.props', [
+			{
+				type: 'select',
+				field: 'direction',
+				title: '设置分割线方向',
+				options: [
+					{ label: 'horizontal', value: 'horizontal' },
+					{ label: 'vertical', value: 'vertical' }
+				]
+			},
+			{
+				type: 'input',
+				field: 'formCreateChild',
+				title: '设置分割线文案'
+			},
+			{
+				type: 'select',
+				field: 'contentPosition',
+				title: '设置分割线文案的位置',
+				options: [
+					{ label: 'left', value: 'left' },
+					{ label: 'right', value: 'right' },
+					{
+						label: 'center',
+						value: 'center'
+					}
+				]
+			}
+		])
+	}
+}

+ 31 - 0
src/config/rule/editor.ts

@@ -0,0 +1,31 @@
+import uniqueId from '@form-create/utils/lib/unique'
+import { localeProps, makeRequiredRule } from '../../utils/formCreateIndex'
+
+const label = '富文本框'
+const name = 'fc-editor'
+
+export default {
+	icon: 'icon-editor',
+	label,
+	name,
+	rule({ t }) {
+		return {
+			type: name,
+			field: uniqueId(),
+			title: t('components.fc-editor.name'),
+			info: '',
+			$required: false,
+			props: {}
+		}
+	},
+	props(_, { t }) {
+		return localeProps(t, name + '.props', [
+			makeRequiredRule(),
+			{
+				type: 'switch',
+				field: 'disabled',
+				title: '是否禁用'
+			}
+		])
+	}
+}

+ 55 - 0
src/config/rule/index.ts

@@ -0,0 +1,55 @@
+import radio from './radio'
+import checkbox from './checkbox.js'
+import input from './input'
+import number from './number'
+import select from './select'
+import _switch from './switch'
+import slider from './slider'
+import time from './time'
+import date from './date.js'
+import rate from './rate'
+import color from './color.js'
+import row from './row'
+import col from './col.js'
+import tabPane from './tabPane'
+import divider from './divider.js'
+import cascader from './cascader.js'
+import upload from './upload'
+import transfer from './transfer'
+import tree from './tree'
+import alert from './alert.js'
+import span from './span'
+import space from './space'
+import tab from './tab'
+import button from './button.js'
+import editor from './editor.js'
+
+const ruleList = {
+	[radio.name]: radio,
+	[checkbox.name]: checkbox,
+	[input.name]: input,
+	[number.name]: number,
+	[select.name]: select,
+	[_switch.name]: _switch,
+	[slider.name]: slider,
+	[time.name]: time,
+	[date.name]: date,
+	[rate.name]: rate,
+	[color.name]: color,
+	[row.name]: row,
+	[col.name]: col,
+	[tab.name]: tab,
+	[tabPane.name]: tabPane,
+	[divider.name]: divider,
+	[cascader.name]: cascader,
+	[upload.name]: upload,
+	[transfer.name]: transfer,
+	[tree.name]: tree,
+	[alert.name]: alert,
+	[span.name]: span,
+	[space.name]: space,
+	[button.name]: button,
+	[editor.name]: editor
+}
+
+export default ruleList

+ 98 - 0
src/config/rule/input.ts

@@ -0,0 +1,98 @@
+import uniqueId from '@form-create/utils/lib/unique'
+import { localeProps, makeRequiredRule } from '../../utils/formCreateIndex'
+
+const label = '输入框'
+const name = 'input'
+
+export default {
+	icon: 'icon-input',
+	label,
+	name,
+	rule({ t }) {
+		return {
+			type: name,
+			field: uniqueId(),
+			title: t('components.input.name'),
+			info: '',
+			$required: false,
+			props: {}
+		}
+	},
+	props(_, { t }) {
+		return localeProps(t, name + '.props', [
+			makeRequiredRule(),
+			{
+				type: 'select',
+				field: 'type',
+				title: '类型',
+				options: [
+					{ label: 'text', value: 'text' },
+					{
+						label: 'textarea',
+						value: 'textarea'
+					},
+					{ label: 'number', value: 'number' },
+					{ label: 'password', value: 'password' }
+				]
+			},
+			{ type: 'inputNumber', field: 'maxlength', title: '最大输入长度', props: { min: 0 } },
+			{
+				type: 'inputNumber',
+				field: 'minlength',
+				title: '最小输入长度',
+				props: { min: 0 }
+			},
+			{ type: 'switch', field: 'showWordLimit', title: '是否显示输入字数统计' },
+			{
+				type: 'input',
+				field: 'placeholder',
+				title: '输入框占位文本'
+			},
+			{ type: 'switch', field: 'clearable', title: '是否可清空' },
+			{
+				type: 'switch',
+				field: 'showPassword',
+				title: '是否显示切换密码图标'
+			},
+			{ type: 'switch', field: 'disabled', title: '禁用' },
+			{
+				type: 'input',
+				field: 'prefixIcon',
+				title: '输入框头部图标'
+			},
+			{ type: 'input', field: 'suffixIcon', title: '输入框尾部图标' },
+			{
+				type: 'inputNumber',
+				field: 'rows',
+				info: t('components.input.props.rowsInfo'),
+				title: '输入框行数',
+				props: { min: 0 }
+			},
+			{
+				type: 'select',
+				field: 'autocomplete',
+				title: '自动补全',
+				options: [
+					{ label: 'on', value: 'on' },
+					{ label: 'off', value: 'off' }
+				]
+			},
+			{ type: 'switch', field: 'readonly', title: '是否只读' },
+			{
+				type: 'select',
+				field: 'resize',
+				title: '控制是否能被用户缩放',
+				options: [
+					{ label: 'none', value: 'none' },
+					{ label: 'both', value: 'both' },
+					{
+						label: 'horizontal',
+						value: 'horizontal'
+					},
+					{ label: 'vertical', value: 'vertical' }
+				]
+			},
+			{ type: 'switch', field: 'autofocus', title: '自动获取焦点' }
+		])
+	}
+}

+ 59 - 0
src/config/rule/number.ts

@@ -0,0 +1,59 @@
+import uniqueId from '@form-create/utils/lib/unique'
+import { localeProps, makeRequiredRule } from '../../utils/formCreateIndex'
+
+const label = '计数器'
+const name = 'inputNumber'
+
+export default {
+	icon: 'icon-number',
+	label,
+	name,
+	rule({ t }) {
+		return {
+			type: name,
+			field: uniqueId(),
+			title: t('components.inputNumber.name'),
+			info: '',
+			$required: false,
+			props: {}
+		}
+	},
+	props(_, { t }) {
+		return localeProps(t, name + '.props', [
+			makeRequiredRule(),
+			{
+				type: 'inputNumber',
+				field: 'min',
+				title: '设置计数器允许的最小值'
+			},
+			{
+				type: 'inputNumber',
+				field: 'max',
+				title: '设置计数器允许的最大值'
+			},
+			{ type: 'inputNumber', field: 'step', title: '计数器步长', props: { min: 0 } },
+			{
+				type: 'switch',
+				field: 'stepStrictly',
+				title: '是否只能输入 step 的倍数'
+			},
+			{ type: 'switch', field: 'disabled', title: '是否禁用计数器' },
+			{
+				type: 'switch',
+				field: 'controls',
+				title: '是否使用控制按钮',
+				value: true
+			},
+			{
+				type: 'select',
+				field: 'controlsPosition',
+				title: '控制按钮位置',
+				options: [
+					{ label: 'default', value: '' },
+					{ label: 'right', value: 'right' }
+				]
+			},
+			{ type: 'input', field: 'placeholder', title: '输入框默认 placeholder' }
+		])
+	}
+}

+ 50 - 0
src/config/rule/radio.ts

@@ -0,0 +1,50 @@
+import uniqueId from '@form-create/utils/lib/unique'
+import { localeProps, makeOptionsRule, makeRequiredRule } from '../../utils/formCreateIndex'
+
+const label = '单选框'
+const name = 'radio'
+
+export default {
+	icon: 'icon-radio',
+	label,
+	name,
+	rule({ t }) {
+		const opt = t('props.option')
+		return {
+			type: name,
+			field: uniqueId(),
+			title: t('components.radio.name'),
+			info: '',
+			effect: {
+				fetch: ''
+			},
+			$required: false,
+			props: {},
+			options: [1, 2].map(value => {
+				return {
+					label: opt + value,
+					value
+				}
+			})
+		}
+	},
+	props(_, { t }) {
+		return localeProps(t, name + '.props', [
+			makeRequiredRule(),
+			makeOptionsRule(t, 'options'),
+			{ type: 'switch', field: 'disabled', title: '是否禁用' },
+			{
+				type: 'switch',
+				field: 'type',
+				title: '按钮形式',
+				props: { activeValue: 'button', inactiveValue: 'default' }
+			},
+			{ type: 'input', field: 'textColor', title: '按钮形式的 Radio 激活时的文本颜色' },
+			{
+				type: 'input',
+				field: 'fill',
+				title: '按钮形式的 Radio 激活时的填充色和边框色'
+			}
+		])
+	}
+}

+ 56 - 0
src/config/rule/rate.ts

@@ -0,0 +1,56 @@
+import uniqueId from '@form-create/utils/lib/unique'
+import { localeProps, makeRequiredRule } from '../../utils/formCreateIndex'
+
+const label = '评分'
+const name = 'rate'
+
+export default {
+	icon: 'icon-rate',
+	label,
+	name,
+	rule({ t }) {
+		return {
+			type: name,
+			field: uniqueId(),
+			title: t('components.rate.name'),
+			info: '',
+			$required: false,
+			props: {}
+		}
+	},
+	props(_, { t }) {
+		return localeProps(t, name + '.props', [
+			makeRequiredRule(),
+			{ type: 'inputNumber', field: 'max', title: '最大分值', props: { min: 0 } },
+			{
+				type: 'switch',
+				field: 'disabled',
+				title: '是否为只读'
+			},
+			{ type: 'switch', field: 'allowHalf', title: '是否允许半选' },
+			{
+				type: 'input',
+				field: 'voidColor',
+				title: '未选中 icon 的颜色'
+			},
+			{ type: 'input', field: 'disabledVoidColor', title: '只读时未选中 icon 的颜色' },
+			{
+				type: 'input',
+				field: 'voidIconClass',
+				title: '未选中 icon 的类名'
+			},
+			{ type: 'input', field: 'disabledVoidIconClass', title: '只读时未选中 icon 的类名' },
+			{
+				type: 'switch',
+				field: 'showScore',
+				title: '是否显示当前分数,show-score 和 show-text 不能同时为真'
+			},
+			{ type: 'input', field: 'textColor', title: '辅助文字的颜色' },
+			{
+				type: 'input',
+				field: 'scoreTemplate',
+				title: '分数显示模板'
+			}
+		])
+	}
+}

+ 64 - 0
src/config/rule/row.ts

@@ -0,0 +1,64 @@
+import { localeProps } from '../../utils/formCreateIndex'
+
+const label = '栅格布局'
+const name = 'row'
+
+export default {
+	icon: 'icon-row',
+	label,
+	name,
+	mask: false,
+	rule() {
+		return {
+			type: 'FcRow',
+			props: {},
+			children: []
+		}
+	},
+	children: 'col',
+	childrenLen: 2,
+	props(_, { t }) {
+		return localeProps(t, name + '.props', [
+			{
+				type: 'inputNumber',
+				field: 'gutter',
+				title: '栅格间隔',
+				props: { min: 0 }
+			},
+			{
+				type: 'switch',
+				field: 'type',
+				title: 'flex布局模式',
+				props: { activeValue: 'flex', inactiveValue: 'default' }
+			},
+			{
+				type: 'select',
+				field: 'justify',
+				title: 'flex 布局下的水平排列方式',
+				options: [
+					{ label: 'start', value: 'start' },
+					{ label: 'end', value: 'end' },
+					{
+						label: 'center',
+						value: 'center'
+					},
+					{ label: 'space-around', value: 'space-around' },
+					{ label: 'space-between', value: 'space-between' }
+				]
+			},
+			{
+				type: 'select',
+				field: 'align',
+				title: 'flex 布局下的垂直排列方式',
+				options: [
+					{ label: 'top', value: 'top' },
+					{ label: 'middle', value: 'middle' },
+					{
+						label: 'bottom',
+						value: 'bottom'
+					}
+				]
+			}
+		])
+	}
+}

+ 96 - 0
src/config/rule/select.ts

@@ -0,0 +1,96 @@
+import uniqueId from '@form-create/utils/lib/unique'
+import { localeProps, makeOptionsRule, makeRequiredRule } from '../../utils/formCreateIndex'
+
+const label = '选择器'
+const name = 'select'
+
+export default {
+	icon: 'icon-select',
+	label,
+	name,
+	rule({ t }) {
+		const opt = t('props.option')
+		return {
+			type: name,
+			field: uniqueId(),
+			title: t('components.select.name'),
+			info: '',
+			effect: {
+				fetch: ''
+			},
+			$required: false,
+			props: {},
+			options: [1, 2].map(value => {
+				return {
+					label: opt + value,
+					value
+				}
+			})
+		}
+	},
+	watch: {
+		multiple({ rule }) {
+			rule.key = uniqueId()
+		}
+	},
+	props(_, { t }) {
+		return localeProps(t, name + '.props', [
+			makeRequiredRule(),
+			makeOptionsRule(t, 'options'),
+			{ type: 'switch', field: 'multiple', title: '是否多选' },
+			{
+				type: 'switch',
+				field: 'disabled',
+				title: '是否禁用'
+			},
+			{ type: 'switch', field: 'clearable', title: '是否可以清空选项' },
+			{
+				type: 'switch',
+				field: 'collapseTags',
+				title: '多选时是否将选中值按文字的形式展示'
+			},
+			{ type: 'inputNumber', field: 'multipleLimit', title: '多选时用户最多可以选择的项目数,为 0 则不限制', props: { min: 0 } },
+			{
+				type: 'input',
+				field: 'autocomplete',
+				title: 'autocomplete 属性'
+			},
+			{ type: 'input', field: 'placeholder', title: '占位符' },
+			{
+				type: 'switch',
+				field: 'filterable',
+				title: '是否可搜索'
+			},
+			{ type: 'switch', field: 'allowCreate', title: '是否允许用户创建新条目' },
+			{
+				type: 'input',
+				field: 'noMatchText',
+				title: '搜索条件无匹配时显示的文字'
+			},
+			{
+				type: 'switch',
+				field: 'remote',
+				title: '其中的选项是否从服务器远程加载'
+			},
+			{
+				type: 'Struct',
+				field: 'remoteMethod',
+				title: '自定义远程搜索方法'
+			},
+			{ type: 'input', field: 'noDataText', title: '选项为空时显示的文字' },
+			{
+				type: 'switch',
+				field: 'reserveKeyword',
+				title: '多选且可搜索时,是否在选中一个选项后保留当前的搜索关键词'
+			},
+			{ type: 'switch', field: 'defaultFirstOption', title: '在输入框按下回车,选择第一个匹配项' },
+			{
+				type: 'switch',
+				field: 'popperAppendToBody',
+				title: '是否将弹出框插入至 body 元素',
+				value: true
+			},
+			{ type: 'switch', field: 'automaticDropdown', title: '对于不可搜索的 Select,是否在输入框获得焦点后自动弹出选项菜单' }
+		])
+	}
+}

+ 64 - 0
src/config/rule/slider.ts

@@ -0,0 +1,64 @@
+import uniqueId from '@form-create/utils/lib/unique'
+import { localeProps, makeRequiredRule } from '../../utils/formCreateIndex'
+
+const label = '滑块'
+const name = 'slider'
+
+export default {
+	icon: 'icon-slider',
+	label,
+	name,
+	rule({ t }) {
+		return {
+			type: name,
+			field: uniqueId(),
+			title: t('components.slider.name'),
+			info: '',
+			$required: false,
+			props: {}
+		}
+	},
+	props(_, { t }) {
+		return localeProps(t, name + '.props', [
+			makeRequiredRule(),
+			{
+				type: 'inputNumber',
+				field: 'min',
+				title: '最小值',
+				props: { min: 0 }
+			},
+			{
+				type: 'inputNumber',
+				field: 'max',
+				title: '最大值',
+				props: { min: 0 }
+			},
+			{ type: 'switch', field: 'disabled', title: '是否禁用' },
+			{
+				type: 'inputNumber',
+				field: 'step',
+				title: '步长',
+				props: { min: 0 }
+			},
+			{ type: 'switch', field: 'showInput', title: '是否显示输入框,仅在非范围选择时有效' },
+			{
+				type: 'switch',
+				field: 'showInputControls',
+				title: '在显示输入框的情况下,是否显示输入框的控制按钮',
+				value: true
+			},
+			{ type: 'switch', field: 'showStops', title: '是否显示间断点' },
+			{
+				type: 'switch',
+				field: 'range',
+				title: '是否为范围选择'
+			},
+			{ type: 'switch', field: 'vertical', title: '是否竖向模式' },
+			{
+				type: 'input',
+				field: 'height',
+				title: 'Slider 高度,竖向模式时必填'
+			}
+		])
+	}
+}

+ 42 - 0
src/config/rule/space.ts

@@ -0,0 +1,42 @@
+import { localeProps } from '../../utils/formCreateIndex'
+
+const label = '间距'
+const name = 'div'
+
+export default {
+	icon: 'icon-space',
+	label,
+	name,
+	rule() {
+		return {
+			type: name,
+			wrap: {
+				show: false
+			},
+			native: false,
+			style: {
+				width: '100%',
+				height: '20px'
+			},
+			children: []
+		}
+	},
+	props(_, { t }) {
+		return [
+			{
+				type: 'object',
+				field: 'formCreateStyle',
+				native: true,
+				props: {
+					rule: localeProps(t, name + '.props', [
+						{
+							type: 'input',
+							field: 'height',
+							title: 'height'
+						}
+					])
+				}
+			}
+		]
+	}
+}

+ 35 - 0
src/config/rule/span.ts

@@ -0,0 +1,35 @@
+import { localeProps } from '../../utils/formCreateIndex'
+
+const label = '文字'
+const name = 'span'
+
+export default {
+	icon: 'icon-span',
+	label,
+	name,
+	rule({ t }) {
+		return {
+			type: name,
+			title: t('components.span.name'),
+			native: false,
+			children: [t('components.span.name')]
+		}
+	},
+	props(_, { t }) {
+		return localeProps(t, name + '.props', [
+			{
+				type: 'input',
+				field: 'formCreateTitle',
+				title: 'title'
+			},
+			{
+				type: 'input',
+				field: 'formCreateChild',
+				title: '内容',
+				props: {
+					type: 'textarea'
+				}
+			}
+		])
+	}
+}

+ 55 - 0
src/config/rule/switch.ts

@@ -0,0 +1,55 @@
+import uniqueId from '@form-create/utils/lib/unique'
+import { localeProps, makeRequiredRule } from '../../utils/formCreateIndex'
+
+const label = '开关'
+const name = 'switch'
+
+export default {
+	icon: 'icon-switch',
+	label,
+	name,
+	rule({ t }) {
+		return {
+			type: name,
+			field: uniqueId(),
+			title: t('components.switch.name'),
+			info: '',
+			$required: false,
+			props: {}
+		}
+	},
+	props(_, { t }) {
+		return localeProps(t, name + '.props', [
+			makeRequiredRule(),
+			{
+				type: 'switch',
+				field: 'disabled',
+				title: '是否禁用'
+			},
+			{
+				type: 'inputNumber',
+				field: 'width',
+				title: '宽度(px)',
+				props: { min: 0 }
+			},
+			{ type: 'input', field: 'activeText', title: 'switch 打开时的文字描述' },
+			{
+				type: 'input',
+				field: 'inactiveText',
+				title: 'switch 关闭时的文字描述'
+			},
+			{ type: 'input', field: 'activeValue', title: 'switch 打开时的值' },
+			{
+				type: 'input',
+				field: 'inactiveValue',
+				title: 'switch 关闭时的值'
+			},
+			{ type: 'input', field: 'activeColor', title: 'switch 打开时的背景色' },
+			{
+				type: 'input',
+				field: 'inactiveColor',
+				title: 'switch 关闭时的背景色'
+			}
+		])
+	}
+}

+ 50 - 0
src/config/rule/tab.ts

@@ -0,0 +1,50 @@
+import { localeProps } from '../../utils/formCreateIndex'
+
+const label = '标签页'
+const name = 'tab'
+
+export default {
+	icon: 'icon-tab',
+	label,
+	name,
+	children: 'tab-pane',
+	mask: false,
+	rule() {
+		return {
+			type: 'el-tabs',
+			style: 'width:100%;',
+			children: []
+		}
+	},
+	props(_, { t }) {
+		return localeProps(t, name + '.props', [
+			{
+				type: 'select',
+				field: 'type',
+				title: '风格类型',
+				options: [
+					{
+						label: 'card',
+						value: 'card'
+					},
+					{ label: 'border-card', value: 'border-card' }
+				]
+			},
+			{ type: 'switch', field: 'closable', title: '标签是否可关闭' },
+			{
+				type: 'select',
+				field: 'tabPosition',
+				title: '选项卡所在位置',
+				options: [
+					{ label: 'top', value: 'top' },
+					{ label: 'right', value: 'right' },
+					{
+						label: 'left',
+						value: 'left'
+					}
+				]
+			},
+			{ type: 'switch', field: 'stretch', title: '标签的宽度是否自撑开' }
+		])
+	}
+}

+ 36 - 0
src/config/rule/tabPane.ts

@@ -0,0 +1,36 @@
+import { localeProps } from '../../utils/formCreateIndex'
+
+const label = '标签页'
+const name = 'tab-pane'
+
+export default {
+	label,
+	name,
+	inside: true,
+	drag: true,
+	dragBtn: false,
+	mask: false,
+	rule({ t }) {
+		return {
+			type: 'el-tab-pane',
+			props: { label: t('components.el-transfer.name') },
+			children: []
+		}
+	},
+	props(_, { t }) {
+		return localeProps(t, name + '.props', [
+			{ type: 'input', field: 'label', title: '选项卡标题' },
+			{
+				type: 'switch',
+				field: 'disabled',
+				title: '是否禁用'
+			},
+			{ type: 'input', field: 'name', title: '与选项卡绑定值 value 对应的标识符,表示选项卡别名' },
+			{
+				type: 'switch',
+				field: 'lazy',
+				title: '标签是否延迟渲染'
+			}
+		])
+	}
+}

+ 77 - 0
src/config/rule/time.ts

@@ -0,0 +1,77 @@
+import uniqueId from '@form-create/utils/lib/unique'
+import { localeProps, makeRequiredRule } from '../../utils/formCreateIndex'
+
+const label = '时间选择器'
+const name = 'timePicker'
+
+export default {
+	icon: 'icon-time',
+	label,
+	name,
+	rule({ t }) {
+		return {
+			type: name,
+			field: uniqueId(),
+			title: t('components.timePicker.name'),
+			info: '',
+			$required: false,
+			props: {}
+		}
+	},
+	props(_, { t }) {
+		return localeProps(t, name + '.props', [
+			makeRequiredRule(),
+			{
+				type: 'Struct',
+				field: 'pickerOptions',
+				title: '当前时间日期选择器特有的选项',
+				props: { defaultValue: {} }
+			},
+			{ type: 'switch', field: 'readonly', title: '完全只读' },
+			{
+				type: 'switch',
+				field: 'disabled',
+				title: '禁用'
+			},
+			{ type: 'switch', field: 'editable', title: '文本框可输入', value: true },
+			{
+				type: 'switch',
+				field: 'clearable',
+				title: '是否显示清除按钮',
+				value: true
+			},
+			{ type: 'input', field: 'placeholder', title: '非范围选择时的占位内容' },
+			{
+				type: 'input',
+				field: 'startPlaceholder',
+				title: '范围选择时开始日期的占位内容'
+			},
+			{ type: 'input', field: 'endPlaceholder', title: '范围选择时开始日期的占位内容' },
+			{
+				type: 'switch',
+				field: 'isRange',
+				title: '是否为时间范围选择'
+			},
+			{ type: 'switch', field: 'arrowControl', title: '是否使用箭头进行时间选择' },
+			{
+				type: 'select',
+				field: 'align',
+				title: '对齐方式',
+				options: [
+					{ label: 'left', value: 'left' },
+					{ label: 'center', value: 'center' },
+					{
+						label: 'right',
+						value: 'right'
+					}
+				]
+			},
+			{ type: 'input', field: 'prefixIcon', title: '自定义头部图标的类名' },
+			{
+				type: 'input',
+				field: 'clearIcon',
+				title: '自定义清空图标的类名'
+			}
+		])
+	}
+}

+ 101 - 0
src/config/rule/transfer.ts

@@ -0,0 +1,101 @@
+import uniqueId from '@form-create/utils/lib/unique'
+import { localeProps } from '../../utils/formCreateIndex'
+
+const label = '穿梭框'
+const name = 'el-transfer'
+
+const generateData = () => {
+	const data = []
+	for (let i = 1; i <= 15; i++) {
+		data.push({
+			key: i,
+			label: `备选项 ${i}`,
+			disabled: i % 4 === 0
+		})
+	}
+	return data
+}
+
+export default {
+	icon: 'icon-transfer',
+	label,
+	name,
+	rule({ t }) {
+		return {
+			type: name,
+			field: uniqueId(),
+			title: t('components.el-transfer.name'),
+			info: '',
+			$required: false,
+			props: {
+				data: generateData()
+			}
+		}
+	},
+	props(_, { t }) {
+		return localeProps(t, name + '.props', [
+			{
+				type: 'Struct',
+				field: 'data',
+				title: 'Transfer 的数据源',
+				props: { defaultValue: [] }
+			},
+			{ type: 'switch', field: 'filterable', title: '是否可搜索' },
+			{
+				type: 'input',
+				field: 'filterPlaceholder',
+				title: '搜索框占位符'
+			},
+			{
+				type: 'select',
+				field: 'targetOrder',
+				title: '右侧列表元素的排序策略',
+				info: '若为 original,则保持与数据源相同的顺序;若为 push,则新加入的元素排在最后;若为 unshift,则新加入的元素排在最前',
+				options: [
+					{ label: 'original', value: 'original' },
+					{
+						label: 'push',
+						value: 'push'
+					},
+					{ label: 'unshift', value: 'unshift' }
+				]
+			},
+			{
+				type: 'Struct',
+				field: 'titles',
+				title: '自定义列表标题',
+				props: { defaultValue: [] }
+			},
+			{
+				type: 'Struct',
+				field: 'buttonTexts',
+				title: '自定义按钮文案',
+				props: { defaultValue: [] }
+			},
+			{
+				type: 'Struct',
+				field: 'format',
+				title: '列表顶部勾选状态文案',
+				props: { defaultValue: {} }
+			},
+			{
+				type: 'Struct',
+				field: 'props',
+				title: '数据源的字段别名',
+				props: { defaultValue: {} }
+			},
+			{
+				type: 'Struct',
+				field: 'leftDefaultChecked',
+				title: '初始状态下左侧列表的已勾选项的 key 数组',
+				props: { defaultValue: [] }
+			},
+			{
+				type: 'Struct',
+				field: 'rightDefaultChecked',
+				title: '初始状态下右侧列表的已勾选项的 key 数组',
+				props: { defaultValue: [] }
+			}
+		])
+	}
+}

+ 86 - 0
src/config/rule/tree.ts

@@ -0,0 +1,86 @@
+import uniqueId from '@form-create/utils/lib/unique'
+import { localeProps, makeOptionsRule, makeRequiredRule } from '../../utils/formCreateIndex'
+
+const label = '树形控件'
+const name = 'tree'
+
+export default {
+	icon: 'icon-tree',
+	label,
+	name,
+	rule({ t }) {
+		const opt = t('props.option')
+		return {
+			type: name,
+			field: uniqueId(),
+			title: t('components.tree.name'),
+			info: '',
+			effect: {
+				fetch: ''
+			},
+			$required: false,
+			props: {
+				props: {
+					label: 'label'
+				},
+				showCheckbox: true,
+				nodeKey: 'id',
+				data: [1, 2].map(value => {
+					return {
+						label: opt + value,
+						id: value,
+						children: []
+					}
+				})
+			}
+		}
+	},
+	props(_, { t }) {
+		return localeProps(t, name + '.props', [
+			makeRequiredRule(),
+			makeOptionsRule(t, 'props.data', false),
+			{ type: 'input', field: 'emptyText', title: '内容为空的时候展示的文本' },
+			{
+				type: 'Struct',
+				field: 'props',
+				title: '配置选项,具体看下表',
+				props: { defaultValue: {} }
+			},
+			{ type: 'switch', field: 'renderAfterExpand', title: '是否在第一次展开某个树节点后才渲染其子节点', value: true },
+			{
+				type: 'switch',
+				field: 'defaultExpandAll',
+				title: '是否默认展开所有节点'
+			},
+			{
+				type: 'switch',
+				field: 'expandOnClickNode',
+				title: '是否在点击节点的时候展开或者收缩节点, 默认值为 true,如果为 false,则只有点箭头图标的时候才会展开或者收缩节点。',
+				value: true
+			},
+			{
+				type: 'switch',
+				field: 'checkOnClickNode',
+				title: '是否在点击节点的时候选中节点,默认值为 false,即只有在点击复选框时才会选中节点。'
+			},
+			{ type: 'switch', field: 'autoExpandParent', title: '展开子节点的时候是否自动展开父节点', value: true },
+			{
+				type: 'switch',
+				field: 'checkStrictly',
+				title: '在显示复选框的情况下,是否严格的遵循父子不互相关联的做法,默认为 false'
+			},
+			{ type: 'switch', field: 'accordion', title: '是否每次只打开一个同级树节点展开' },
+			{
+				type: 'inputNumber',
+				field: 'indent',
+				title: '相邻级节点间的水平缩进,单位为像素'
+			},
+			{ type: 'input', field: 'iconClass', title: '自定义树节点的图标' },
+			{
+				type: 'input',
+				field: 'nodeKey',
+				title: '每个树节点用来作为唯一标识的属性,整棵树应该是唯一的'
+			}
+		])
+	}
+}

+ 86 - 0
src/config/rule/upload.ts

@@ -0,0 +1,86 @@
+import uniqueId from '@form-create/utils/lib/unique'
+import { localeProps, makeRequiredRule } from '../../utils/formCreateIndex'
+
+const label = '上传'
+const name = 'upload'
+
+export default {
+	icon: 'icon-upload',
+	label,
+	name,
+	rule({ t }) {
+		return {
+			type: name,
+			field: uniqueId(),
+			title: t('components.upload.name'),
+			info: '',
+			$required: false,
+			props: {
+				action: '',
+				onSuccess(res, file) {
+					file.url = res.data.url
+				}
+			}
+		}
+	},
+	props(_, { t }) {
+		return localeProps(t, name + '.props', [
+			makeRequiredRule(),
+			{
+				type: 'select',
+				field: 'list-type',
+				title: '上传类型',
+				value: 'text',
+				options: [
+					{ label: '文字', value: 'text' },
+					{
+						label: '图片',
+						value: 'picture'
+					},
+					{
+						label: '卡片',
+						value: 'picture-card'
+					}
+				]
+			},
+			{ type: 'input', field: 'action', title: '上传的地址(必填)' },
+			{
+				type: 'Struct',
+				field: 'headers',
+				title: '设置上传的请求头部',
+				props: { defaultValue: {} }
+			},
+			{ type: 'switch', field: 'multiple', title: '是否支持多选文件' },
+			{
+				type: 'Struct',
+				field: 'data',
+				title: '上传时附带的额外参数',
+				props: { defaultValue: {} }
+			},
+			{ type: 'input', field: 'name', title: '上传的文件字段名' },
+			{
+				type: 'switch',
+				field: 'withCredentials',
+				title: '支持发送 cookie 凭证信息'
+			},
+			{ type: 'input', field: 'accept', title: '接受上传的文件类型(thumbnail-mode 模式下此参数无效)' },
+			{
+				type: 'switch',
+				field: 'autoUpload',
+				title: '是否在选取文件后立即进行上传',
+				value: true
+			},
+			{
+				type: 'switch',
+				field: 'disabled',
+				title: '是否禁用'
+			},
+			{
+				type: 'inputNumber',
+				field: 'limit',
+				title: '最大允许上传个数',
+				props: { min: 0 }
+			}
+		])
+	}
+}

+ 408 - 0
src/lang/en.ts

@@ -58,5 +58,413 @@ export default {
 		message: {
 			editSuccess: 'Edit Success'
 		}
+	},
+	// 新增的 表单设计器 start
+	form: {
+		field: 'Field',
+		title: 'Title',
+		info: 'Info',
+		control: 'Control',
+		clear: 'Clear',
+		refresh: 'Refresh',
+		labelPosition: 'Label position',
+		size: 'Form size',
+		labelWidth: 'Label width',
+		hideRequiredAsterisk: 'Hide the red asterisk next to the label for required fields',
+		showMessage: 'Display verification error message',
+		inlineMessage: 'Display validation information inline',
+		submitBtn: 'Whether to display the form submit button',
+		resetBtn: 'Whether to display the form reset button',
+		submit: 'Submit',
+		reset: 'Reset'
+	},
+	validate: {
+		type: 'Value type',
+		typePlaceholder: 'Please select',
+		trigger: 'Trigger mode',
+		mode: 'Verification method',
+		modes: {
+			required: 'required',
+			pattern: 'regular expression',
+			min: 'minimum value',
+			max: 'maximum value',
+			len: 'length'
+		},
+		message: 'Error message',
+		auto: 'Get automatically',
+		autoRequired: 'Please enter {title}',
+		autoMode: 'Please enter the correct {title}',
+		requiredPlaceholder: 'Please enter a prompt'
+	},
+	tableOptions: {
+		handle: 'Delete',
+		add: 'Add'
+	},
+	struct: {
+		title: 'Edit Data',
+		submit: 'OK',
+		cancel: 'Cancel',
+		error: 'The format of the input content is incorrect'
+	},
+	fetch: {
+		action: 'Action',
+		actionRequired: 'Please data action',
+		method: 'Method',
+		dataType: 'Date type',
+		data: 'Attached data',
+		headers: 'Headers',
+		parse: 'Analysis function',
+		parseInfo: 'Parse the interface data and return the data structure required by the component',
+		parseValidate: 'Please enter the correct parsing function'
+	},
+	designer: {
+		preview: 'Preview',
+		clear: 'Clear',
+		clearConfirm: 'Clear',
+		clearCancel: 'Cancel',
+		clearConfirmTitle: 'It will not be restored after cleared, are you sure you want to clear it? ',
+		config: {
+			component: 'Component',
+			form: 'Form',
+			rule: 'Basic',
+			props: 'Property',
+			validate: 'Verify'
+		}
+	},
+	menu: {
+		main: 'Form',
+		aide: 'Auxiliary',
+		layout: 'Layout'
+	},
+	props: {
+		required: 'Is it required',
+		options: 'Option data',
+		option: 'Option',
+		optionsType: {
+			json: 'JSON',
+			fetch: 'Fetch',
+			struct: 'Struct'
+		}
+	},
+	components: {
+		radio: {
+			name: 'Radio',
+			props: {
+				disabled: 'Whether to disable',
+				type: 'Button form',
+				textColor: 'The text color of the radio when the button is activated',
+				fill: 'The fill color and border color when the radio in the button form is activated'
+			}
+		},
+		checkbox: {
+			name: 'Checkbox',
+			props: {
+				type: 'Button type',
+				disabled: 'Whether to disable',
+				min: 'The minimum number of checkboxes that can be checked',
+				max: 'The maximum number of checkboxes that can be checked',
+				textColor: 'The text color of the checkbox in the button form when it is activated',
+				fill: 'The fill color and border color of the checkbox in the button form when it is activated'
+			}
+		},
+		input: {
+			name: 'Input',
+			props: {
+				type: 'Type',
+				maxlength: 'Maximum input length',
+				minlength: 'Minimum input length',
+				showWordLimit: 'Whether to display input word count statistics',
+				placeholder: 'Input box placeholder text',
+				clearable: 'Whether it can be cleared',
+				showPassword: 'Whether to display the switch password icon',
+				disabled: 'Disabled',
+				prefixIcon: 'Input box head icon',
+				suffixIcon: 'Input box tail icon',
+				rowsInfo: "Only works when type is 'textarea'",
+				rows: 'Number of input box rows',
+				autocomplete: 'Autocomplete',
+				readonly: 'Whether read-only',
+				resize: 'Whether the control can be zoomed by the user',
+				autofocus: 'Automatically acquire focus'
+			}
+		},
+		inputNumber: {
+			name: 'Number',
+			props: {
+				min: 'Set the minimum value allowed by the counter',
+				max: 'Set the maximum value allowed by the counter',
+				step: 'Counter step size',
+				stepStrictly: 'Whether only multiples of step can be entered',
+				disabled: 'Whether to disable the counter',
+				controls: 'Whether to use the control button',
+				controlsPosition: 'Control button position',
+				placeholder: 'Input box default placeholder'
+			}
+		},
+		select: {
+			name: 'Select',
+			props: {
+				multiple: 'Whether to choose multiple',
+				disabled: 'Whether to disable',
+				clearable: 'Is it possible to clear the options',
+				collapseTags: 'Whether to display the selected value in the form of text when multiple selection',
+				multipleLimit: 'The maximum number of items that the user can select in multi-selection, if it is 0, there is no limit',
+				autocomplete: 'Autocomplete attribute',
+				placeholder: 'Placeholder',
+				filterable: 'Whether it is searchable',
+				allowCreate: 'Whether to allow users to create new entries',
+				noMatchText: 'The text displayed when the search condition has no match',
+				noDataText: 'The text displayed when the option is empty',
+				reserveKeyword: 'When multi-choice and searchable, whether to retain the current search keyword after selecting an option',
+				defaultFirstOption: 'Press Enter in the input box to select the first match',
+				popperAppendToBody: 'Whether to insert the popup box into the body element',
+				automaticDropdown: 'For non-searchable Select, whether to automatically pop up the option menu after the input box gets focus'
+			}
+		},
+		switch: {
+			name: 'Switch',
+			props: {
+				disabled: 'Whether to disable',
+				width: 'Width (px)',
+				activeText: 'Text description when the switch is open',
+				inactiveText: 'Text description when switch is closed',
+				activeValue: 'The value when the switch is open',
+				inactiveValue: 'Value when switch is closed',
+				activeColor: 'Background color when the switch is open',
+				inactiveColor: 'Background color when switch is closed'
+			}
+		},
+		slider: {
+			name: 'Slider',
+			props: {
+				min: 'Minimum value',
+				max: 'Maximum value',
+				disabled: 'Whether to disable',
+				step: 'Step size',
+				showInput: 'Whether to display the input box, only valid for non-range selection',
+				showInputControls: 'Whether to display the control buttons of the input box when the input box is displayed',
+				showStops: 'Whether to display the break point',
+				range: 'Whether it is a range selection',
+				vertical: 'Whether vertical mode',
+				height: 'Slider height, required in vertical mode'
+			}
+		},
+		timePicker: {
+			name: 'TimePicker',
+			props: {
+				pickerOptions: 'Options specific to the current time and date picker',
+				readonly: 'Completely read-only',
+				disabled: 'Disabled',
+				editable: 'The text box can be entered',
+				clearable: 'Whether to display the clear button',
+				placeholder: 'Placeholder content when non-range selection',
+				startPlaceholder: 'The placeholder content of the start date when the range is selected',
+				endPlaceholder: 'The placeholder content of the start date when the range is selected',
+				isRange: 'Whether to select for the time range',
+				arrowControl: 'Whether to use the arrow for time selection',
+				align: 'Alignment',
+				prefixIcon: 'Class name of custom header icon',
+				clearIcon: 'The class name of the custom clear icon'
+			}
+		},
+		datePicker: {
+			name: 'DatePicker',
+			props: {
+				pickerOptions: 'Options specific to the current time and date picker',
+				readonly: 'Completely read-only',
+				disabled: 'Disabled',
+				type: 'Display type',
+				editable: 'The text box can be entered',
+				clearable: 'Whether to display the clear button',
+				placeholder: 'Placeholder content when non-range selection',
+				startPlaceholder: 'The placeholder content of the start date when the range is selected',
+				endPlaceholder: 'The placeholder content of the end date when the range is selected',
+				format: 'The format displayed in the input box',
+				align: 'Alignment',
+				rangeSeparator: 'Separator when selecting a range',
+				unlinkPanels: 'Unlink the linkage between two date panels in the range selector',
+				prefixIcon: 'Class name of custom header icon',
+				clearIcon: 'The class name of the custom clear icon'
+			}
+		},
+		rate: {
+			name: 'Rate',
+			props: {
+				max: 'Maximum score',
+				disabled: 'Is it read-only',
+				allowHalf: 'Whether half selection is allowed',
+				voidColor: 'The color of the unselected icon',
+				disabledVoidColor: 'The color of the unselected icon when it is read-only',
+				voidIconClass: 'The class name of the unselected icon',
+				disabledVoidIconClass: 'The class name of the unselected icon when it is read-only',
+				showScore: 'Whether to display the current score, show-score and show-text cannot be true at the same time',
+				textColor: 'The color of the auxiliary text',
+				scoreTemplate: 'Score display template'
+			}
+		},
+		colorPicker: {
+			name: 'ColorPicker',
+			props: {
+				disabled: 'Whether to disable',
+				showAlpha: 'Whether to support transparency selection',
+				colorFormat: 'Color format'
+			}
+		},
+		row: {
+			name: 'Grid Layout',
+			props: {
+				gutter: 'Grid Interval',
+				type: 'Flex layout mode',
+				justify: 'Horizontal arrangement under flex layout',
+				align: 'Vertical alignment under flex layout'
+			}
+		},
+		col: {
+			name: 'Grid',
+			props: {
+				span: 'The number of columns occupied by the grid',
+				offset: 'Number of grids on the left side of the grid',
+				push: 'The grid moves to the right by the number of grids',
+				pull: 'The grid moves to the left by the number of grids'
+			}
+		},
+		tab: {
+			name: 'Tab',
+			props: {
+				type: 'Style type',
+				closable: 'Whether the label can be closed',
+				tabPosition: 'Tab position',
+				stretch: 'Whether the width of the label is self-supporting'
+			}
+		},
+		'tab-pane': {
+			name: 'TabPane',
+			props: {
+				label: 'Tab Title',
+				disabled: 'Whether to disable',
+				name: 'Identifier corresponding to the tab binding value value, indicating the tab alias',
+				lazy: 'Whether the label is rendered late'
+			}
+		},
+		'el-divider': {
+			name: 'Divider',
+			props: {
+				direction: 'Set the dividing line direction',
+				formCreateChild: 'Set the dividing line text',
+				contentPosition: 'Set the position of the dividing line text'
+			}
+		},
+		cascader: {
+			name: 'Cascader',
+			props: {
+				props: 'Configuration options',
+				size: 'Size',
+				placeholder: 'Input box placeholder text',
+				disabled: 'Whether to disable',
+				clearable: 'Whether to support the clear option',
+				showAllLevels: 'Whether to display the full path of the selected value in the input box',
+				collapseTags: 'Whether to collapse Tags in multi-select mode',
+				separator: 'Option separator'
+			}
+		},
+		upload: {
+			name: 'Upload',
+			props: {
+				uploadType: 'Upload type',
+				action: 'Address to upload (required)',
+				headers: 'Set the upload request header',
+				multiple: 'Whether to support multiple selection files',
+				data: 'Additional parameters attached to upload',
+				name: 'Uploaded file field name',
+				withCredentials: 'Support sending cookie credential information',
+				accept: 'Accept the uploaded file type (this parameter is invalid in thumbnail-mode mode)',
+				autoUpload: 'Whether to upload immediately after selecting the file',
+				disabled: 'Whether to disable',
+				limit: 'Maximum number of uploads allowed'
+			}
+		},
+		'el-transfer': {
+			name: 'Transfer',
+			props: {
+				data: "Transfer's data source",
+				filterable: 'Whether it is searchable',
+				filterPlaceholder: 'Search box placeholder',
+				targetOrder: 'Sorting strategy for the right list elements',
+				titles: 'Custom list title',
+				buttonTexts: 'Custom button text',
+				format: 'Check the status copy at the top of the list',
+				props: 'Field alias of data source',
+				leftDefaultChecked: 'The key array of checked items in the left list in the initial state',
+				rightDefaultChecked: 'The key array of checked items in the right list in the initial state'
+			}
+		},
+		tree: {
+			name: 'Tree',
+			props: {
+				emptyText: 'The text displayed when the content is empty',
+				props: 'Configuration options, see the table below',
+				renderAfterExpand: 'Whether to render its child nodes after the first expansion of a tree node',
+				defaultExpandAll: 'Whether to expand all nodes by default',
+				expandOnClickNode:
+					'Whether to expand or contract the node when the node is clicked, the default value is true, if it is false, the node will only be expanded or contracted when the arrow icon is clicked. ',
+				checkOnClickNode:
+					'Whether to select the node when the node is clicked, the default value is false, that is, the node will be selected only when the check box is clicked. ',
+				autoExpandParent: 'Whether to automatically expand the parent node when expanding the child node',
+				checkStrictly:
+					'In the case of displaying the check box, whether to strictly follow the practice that the parent and child are not related to each other, the default is false',
+				accordion: 'Whether to open only one sibling tree node each time',
+				indent: 'Horizontal indentation between adjacent level nodes, in pixels',
+				iconClass: 'Custom tree node icon',
+				nodeKey: 'Each tree node is used as a unique identification attribute, and the whole tree should be unique'
+			}
+		},
+		'el-alert': {
+			name: 'Alert',
+			description: 'Description',
+			props: {
+				title: 'Title',
+				type: 'Theme',
+				description: 'Auxiliary text',
+				closable: 'Whether it can be closed',
+				center: 'Whether the text is centered',
+				closeText: 'Close button custom text',
+				showIcon: 'Whether to display the icon',
+				effect: 'Select a provided theme'
+			}
+		},
+		span: {
+			name: 'Text',
+			props: {
+				formCreateTitle: 'Title',
+				formCreateChild: 'Content'
+			}
+		},
+		div: {
+			name: 'Space',
+			props: {
+				height: 'height'
+			}
+		},
+		'el-button': {
+			name: 'Button',
+			props: {
+				formCreateChild: 'Content',
+				size: 'Size',
+				type: 'Type',
+				plain: 'Is it a plain button',
+				round: 'Whether round button',
+				circle: 'Whether a circular button',
+				loading: 'Whether loading status',
+				disabled: 'Whether to disable the state',
+				icon: 'Icon class name'
+			}
+		},
+		'fc-editor': {
+			name: 'Editor',
+			props: {
+				disabled: 'Whether to disable'
+			}
+		}
 	}
+	// 新增的 表单设计器 end
 }

+ 405 - 0
src/lang/zh-cn.ts

@@ -57,5 +57,410 @@ export default {
 		message: {
 			editSuccess: '编辑成功'
 		}
+	},
+	// 新增的 表单设计器 start
+	form: {
+		field: '字段 ID',
+		title: '字段名称',
+		info: '提示信息',
+		control: '联动数据',
+		clear: '清空值',
+		refresh: '刷新',
+		labelPosition: '标签位置',
+		size: '表单尺寸',
+		labelWidth: '标签宽度',
+		hideRequiredAsterisk: '隐藏必填字段的标签旁边的红色星号',
+		showMessage: '显示校验错误信息',
+		inlineMessage: '以行内形式展示校验信息',
+		submitBtn: '是否显示表单提交按钮',
+		resetBtn: '是否显示表单重置按钮',
+		submit: '提交',
+		reset: '重置'
+	},
+	validate: {
+		type: '字段类型',
+		typePlaceholder: '请选择',
+		trigger: '触发方式',
+		mode: '验证方式',
+		modes: {
+			required: '必填',
+			pattern: '正则表达式',
+			min: '最小值',
+			max: '最大值',
+			len: '长度'
+		},
+		message: '错误信息',
+		auto: '自动获取',
+		autoRequired: '请输入{title}',
+		autoMode: '请输入正确的{title}',
+		requiredPlaceholder: '请输入提示语'
+	},
+	tableOptions: {
+		handle: '操作',
+		add: '添加'
+	},
+	struct: {
+		title: '编辑数据',
+		submit: '确 定',
+		cancel: '取 消',
+		error: '输入内容格式有误'
+	},
+	fetch: {
+		action: '接口',
+		actionRequired: '请数据接口',
+		method: '请求方式',
+		dataType: '提交方式',
+		data: '附带数据',
+		headers: 'header信息',
+		parse: '解析函数',
+		parseInfo: '解析接口数据,返回组件所需的数据结构',
+		parseValidate: '请输入正确的解析函数'
+	},
+	designer: {
+		preview: '预 览',
+		clear: '清 空',
+		clearConfirm: '清空',
+		clearCancel: '取消',
+		clearConfirmTitle: '清空后将不能恢复,确定要清空吗?',
+		config: {
+			component: '组件配置',
+			form: '表单配置',
+			rule: '基础配置',
+			props: '属性配置',
+			validate: '验证配置'
+		}
+	},
+	menu: {
+		main: '表单组件',
+		aide: '辅助组件',
+		layout: '布局组件'
+	},
+	props: {
+		required: '是否必填',
+		options: '选项数据',
+		option: '选项',
+		optionsType: {
+			json: 'JSON数据',
+			fetch: '接口数据',
+			struct: '静态数据'
+		}
+	},
+	components: {
+		radio: {
+			name: '单选框',
+			props: {
+				disabled: '是否禁用',
+				type: '按钮形式',
+				textColor: '按钮形式的 Radio 激活时的文本颜色',
+				fill: '按钮形式的 Radio 激活时的填充色和边框色'
+			}
+		},
+		checkbox: {
+			name: '多选框',
+			props: {
+				type: '按钮类型',
+				disabled: '是否禁用',
+				min: '可被勾选的 checkbox 的最小数量',
+				max: '可被勾选的 checkbox 的最大数量',
+				textColor: '按钮形式的 Checkbox 激活时的文本颜色',
+				fill: '按钮形式的 Checkbox 激活时的填充色和边框色'
+			}
+		},
+		input: {
+			name: '输入框',
+			props: {
+				type: '类型',
+				maxlength: '最大输入长度',
+				minlength: '最小输入长度',
+				showWordLimit: '是否显示输入字数统计',
+				placeholder: '输入框占位文本',
+				clearable: '是否可清空',
+				showPassword: '是否显示切换密码图标',
+				disabled: '禁用',
+				prefixIcon: '输入框头部图标',
+				suffixIcon: '输入框尾部图标',
+				rowInfo: '只对 type="textarea" 有效',
+				rows: '输入框行数',
+				autocomplete: '自动补全',
+				readonly: '是否只读',
+				resize: '控制是否能被用户缩放',
+				autofocus: '自动获取焦点'
+			}
+		},
+		inputNumber: {
+			name: '计数器',
+			props: {
+				min: '设置计数器允许的最小值',
+				max: '设置计数器允许的最大值',
+				step: '计数器步长',
+				stepStrictly: '是否只能输入 step 的倍数',
+				disabled: '是否禁用计数器',
+				controls: '是否使用控制按钮',
+				controlsPosition: '控制按钮位置',
+				placeholder: '输入框默认 placeholder'
+			}
+		},
+		select: {
+			name: '选择器',
+			props: {
+				multiple: '是否多选',
+				disabled: '是否禁用',
+				clearable: '是否可以清空选项',
+				collapseTags: '多选时是否将选中值按文字的形式展示',
+				multipleLimit: '多选时用户最多可以选择的项目数,为 0 则不限制',
+				autocomplete: 'autocomplete 属性',
+				placeholder: '占位符',
+				filterable: '是否可搜索',
+				allowCreate: '是否允许用户创建新条目',
+				noMatchText: '搜索条件无匹配时显示的文字',
+				noDataText: '选项为空时显示的文字',
+				reserveKeyword: '多选且可搜索时,是否在选中一个选项后保留当前的搜索关键词',
+				defaultFirstOption: '在输入框按下回车,选择第一个匹配项',
+				popperAppendToBody: '是否将弹出框插入至 body 元素',
+				automaticDropdown: '对于不可搜索的 Select,是否在输入框获得焦点后自动弹出选项菜单'
+			}
+		},
+		switch: {
+			name: '开关',
+			props: {
+				disabled: '是否禁用',
+				width: '宽度(px)',
+				activeText: 'switch 打开时的文字描述',
+				inactiveText: 'switch 关闭时的文字描述',
+				activeValue: 'switch 打开时的值',
+				inactiveValue: 'switch 关闭时的值',
+				activeColor: 'switch 打开时的背景色',
+				inactiveColor: 'switch 关闭时的背景色'
+			}
+		},
+		slider: {
+			name: '滑块',
+			props: {
+				min: '最小值',
+				max: '最大值',
+				disabled: '是否禁用',
+				step: '步长',
+				showInput: '是否显示输入框,仅在非范围选择时有效',
+				showInputControls: '在显示输入框的情况下,是否显示输入框的控制按钮',
+				showStops: '是否显示间断点',
+				range: '是否为范围选择',
+				vertical: '是否竖向模式',
+				height: 'Slider 高度,竖向模式时必填'
+			}
+		},
+		timePicker: {
+			name: '时间选择器',
+			props: {
+				pickerOptions: '当前时间日期选择器特有的选项',
+				readonly: '完全只读',
+				disabled: '禁用',
+				editable: '文本框可输入',
+				clearable: '是否显示清除按钮',
+				placeholder: '非范围选择时的占位内容',
+				startPlaceholder: '范围选择时开始日期的占位内容',
+				endPlaceholder: '范围选择时开始日期的占位内容',
+				isRange: '是否为时间范围选择',
+				arrowControl: '是否使用箭头进行时间选择',
+				align: '对齐方式',
+				prefixIcon: '自定义头部图标的类名',
+				clearIcon: '自定义清空图标的类名'
+			}
+		},
+		datePicker: {
+			name: '日期选择器',
+			props: {
+				pickerOptions: '当前时间日期选择器特有的选项',
+				readonly: '完全只读',
+				disabled: '禁用',
+				type: '显示类型',
+				editable: '文本框可输入',
+				clearable: '是否显示清除按钮',
+				placeholder: '非范围选择时的占位内容',
+				startPlaceholder: '范围选择时开始日期的占位内容',
+				endPlaceholder: '范围选择时结束日期的占位内容',
+				format: '显示在输入框中的格式',
+				align: '对齐方式',
+				rangeSeparator: '选择范围时的分隔符',
+				unlinkPanels: '在范围选择器里取消两个日期面板之间的联动',
+				prefixIcon: '自定义头部图标的类名',
+				clearIcon: '自定义清空图标的类名'
+			}
+		},
+		rate: {
+			name: '评分',
+			props: {
+				max: '最大分值',
+				disabled: '是否为只读',
+				allowHalf: '是否允许半选',
+				voidColor: '未选中 icon 的颜色',
+				disabledVoidColor: '只读时未选中 icon 的颜色',
+				voidIconClass: '未选中 icon 的类名',
+				disabledVoidIconClass: '只读时未选中 icon 的类名',
+				showScore: '是否显示当前分数,show-score 和 show-text 不能同时为真',
+				textColor: '辅助文字的颜色',
+				scoreTemplate: '分数显示模板'
+			}
+		},
+		colorPicker: {
+			name: '颜色选择器',
+			props: {
+				disabled: '是否禁用',
+				showAlpha: '是否支持透明度选择',
+				colorFormat: '颜色的格式'
+			}
+		},
+		row: {
+			name: '栅格布局',
+			props: {
+				gutter: '栅格间隔',
+				type: 'flex布局模式',
+				justify: 'flex 布局下的水平排列方式',
+				align: 'flex 布局下的垂直排列方式'
+			}
+		},
+		col: {
+			name: '格子',
+			props: {
+				span: '栅格占据的列数',
+				offset: '栅格左侧的间隔格数',
+				push: '栅格向右移动格数',
+				pull: '栅格向左移动格数'
+			}
+		},
+		tab: {
+			name: '标签页',
+			props: {
+				type: '风格类型',
+				closable: '标签是否可关闭',
+				tabPosition: '选项卡所在位置',
+				stretch: '标签的宽度是否自撑开'
+			}
+		},
+		'tab-pane': {
+			name: '标签页',
+			props: {
+				label: '选项卡标题',
+				disabled: '是否禁用',
+				name: '与选项卡绑定值 value 对应的标识符,表示选项卡别名',
+				lazy: '标签是否延迟渲染'
+			}
+		},
+		'el-divider': {
+			name: '分割线',
+			props: {
+				direction: '设置分割线方向',
+				formCreateChild: '设置分割线文案',
+				contentPosition: '设置分割线文案的位置'
+			}
+		},
+		cascader: {
+			name: '级联选择器',
+			props: {
+				props: '配置选项',
+				size: '尺寸',
+				placeholder: '输入框占位文本',
+				disabled: '是否禁用',
+				clearable: '是否支持清空选项',
+				showAllLevels: '输入框中是否显示选中值的完整路径',
+				collapseTags: '多选模式下是否折叠Tag',
+				separator: '选项分隔符'
+			}
+		},
+		upload: {
+			name: '上传',
+			props: {
+				uploadType: '上传类型',
+				action: '上传的地址(必填)',
+				headers: '设置上传的请求头部',
+				multiple: '是否支持多选文件',
+				data: '上传时附带的额外参数',
+				name: '上传的文件字段名',
+				withCredentials: '支持发送 cookie 凭证信息',
+				accept: '接受上传的文件类型(thumbnail-mode 模式下此参数无效)',
+				autoUpload: '是否在选取文件后立即进行上传',
+				disabled: '是否禁用',
+				limit: '最大允许上传个数'
+			}
+		},
+		'el-transfer': {
+			name: '穿梭框',
+			props: {
+				data: 'Transfer 的数据源',
+				filterable: '是否可搜索',
+				filterPlaceholder: '搜索框占位符',
+				targetOrder: '右侧列表元素的排序策略',
+				titles: '自定义列表标题',
+				buttonTexts: '自定义按钮文案',
+				format: '列表顶部勾选状态文案',
+				props: '数据源的字段别名',
+				leftDefaultChecked: '初始状态下左侧列表的已勾选项的 key 数组',
+				rightDefaultChecked: '初始状态下右侧列表的已勾选项的 key 数组'
+			}
+		},
+		tree: {
+			name: '树形控件',
+			props: {
+				emptyText: '内容为空的时候展示的文本',
+				props: '配置选项,具体看下表',
+				renderAfterExpand: '是否在第一次展开某个树节点后才渲染其子节点',
+				defaultExpandAll: '是否默认展开所有节点',
+				expandOnClickNode: '是否在点击节点的时候展开或者收缩节点, 默认值为 true,如果为 false,则只有点箭头图标的时候才会展开或者收缩节点。',
+				checkOnClickNode: '是否在点击节点的时候选中节点,默认值为 false,即只有在点击复选框时才会选中节点。',
+				autoExpandParent: '展开子节点的时候是否自动展开父节点',
+				checkStrictly: '在显示复选框的情况下,是否严格的遵循父子不互相关联的做法,默认为 false',
+				accordion: '是否每次只打开一个同级树节点展开',
+				indent: '相邻级节点间的水平缩进,单位为像素',
+				iconClass: '自定义树节点的图标',
+				nodeKey: '每个树节点用来作为唯一标识的属性,整棵树应该是唯一的'
+			}
+		},
+		'el-alert': {
+			name: '提示',
+			description: 'description',
+			props: {
+				title: '标题',
+				type: '主题',
+				description: '辅助性文字',
+				closable: '是否可关闭',
+				center: '文字是否居中',
+				closeText: '关闭按钮自定义文本',
+				showIcon: '是否显示图标',
+				effect: '选择提供的主题'
+			}
+		},
+		span: {
+			name: '文字',
+			props: {
+				formCreateTitle: '标题',
+				formCreateChild: '内容'
+			}
+		},
+		div: {
+			name: '间距',
+			props: {
+				height: '高度'
+			}
+		},
+		'el-button': {
+			name: '按钮',
+			props: {
+				formCreateChild: '内容',
+				size: '尺寸',
+				type: '类型',
+				plain: '是否朴素按钮',
+				round: '是否圆角按钮',
+				circle: '是否圆形按钮',
+				loading: '是否加载中状态',
+				disabled: '是否禁用状态',
+				icon: '图标类名'
+			}
+		},
+		'fc-editor': {
+			name: '富文本框',
+			props: {
+				disabled: '是否禁用'
+			}
+		}
 	}
+	// 新增的 表单设计器 end
 }

+ 1 - 0
src/main.ts

@@ -10,6 +10,7 @@ import 'element-plus/theme-chalk/dark/css-vars.css'
 import '@/styles/index.scss'
 // 注册全局组件
 import { registerGlobComp } from '@/components/registerGlobComp'
+
 // 注册全局directive
 import { setupGlobDirectives } from '@/directive'
 import '@/permission'

+ 133 - 0
src/styles/fontIndex.scss

@@ -0,0 +1,133 @@
+@font-face {
+	font-family: "fc-icon";
+	src: url(fonts/fc-icons.woff) format('woff');
+}
+
+.fc-icon {
+	font-family: "fc-icon" !important;
+	font-size: 16px;
+	font-style: normal;
+	-webkit-font-smoothing: antialiased;
+	-moz-osx-font-smoothing: grayscale;
+}
+
+.icon-add-child:before {
+	content: "\e789";
+}
+
+.icon-switch:before {
+	content: "\e77c";
+}
+
+.icon-tab:before {
+	content: "\e77b";
+}
+
+.icon-button:before {
+	content: "\e77e";
+}
+
+.icon-input:before {
+	content: "\e77f";
+}
+
+.icon-checkbox:before {
+	content: "\e780";
+}
+
+.icon-radio:before {
+	content: "\e781";
+}
+
+.icon-rate:before {
+	content: "\e782";
+}
+
+.icon-number:before {
+	content: "\e783";
+}
+
+.icon-upload:before {
+	content: "\e784";
+}
+
+.icon-cascader:before {
+	content: "\e785";
+}
+
+.icon-space:before {
+	content: "\e786";
+}
+
+.icon-color:before {
+	content: "\e787";
+}
+
+.icon-span:before {
+	content: "\e788";
+}
+
+.icon-alert:before {
+	content: "\e78a";
+}
+
+.icon-row:before {
+	content: "\e78b";
+}
+
+.icon-divider:before {
+	content: "\e78d";
+}
+
+.icon-select:before {
+	content: "\e78e";
+}
+
+.icon-transfer:before {
+	content: "\e78f";
+}
+
+.icon-editor:before {
+	content: "\e790";
+}
+
+.icon-slider:before {
+	content: "\e791";
+}
+
+.icon-tree:before {
+	content: "\e792";
+}
+
+.icon-date:before {
+	content: "\e793";
+}
+
+.icon-time:before {
+	content: "\e794";
+}
+
+.icon-delete:before {
+	content: "\e770";
+}
+
+.icon-copy:before {
+	content: "\e771";
+}
+
+.icon-import:before {
+	content: "\e773";
+}
+
+.icon-add:before {
+	content: "\e774";
+}
+
+.icon-preview:before {
+	content: "\e776";
+}
+
+.icon-move:before {
+	content: "\e777";
+}
+

BIN
src/styles/fonts/fc-icons.woff


+ 2 - 1
src/styles/index.scss

@@ -1,5 +1,6 @@
 @import 'src/styles/variables.module';
 @import 'src/styles/element-plus-dark';
+@import "./fontIndex.scss";
 @import './mixin.scss';
 @import './transition.scss';
 @import 'src/styles/element-plus';
@@ -139,4 +140,4 @@ div:focus {
 .le-hover-effect--bg {
 	cursor: pointer;
 	@include hover-bg-opacity();
-}
+}

+ 9 - 0
src/utils/form.ts

@@ -0,0 +1,9 @@
+import formCreate from '@form-create/element-ui'
+
+const viewForm = formCreate
+
+const designerForm = formCreate.factory()
+
+export default viewForm
+
+export { designerForm }

+ 192 - 0
src/utils/formCreateIndex.ts

@@ -0,0 +1,192 @@
+import is, { hasProperty } from '@form-create/utils/lib/type'
+import { parseFn } from '@form-create/utils/lib/json'
+import toCase from '@form-create/utils/lib/tocase'
+import { computed, isRef, unref, ref } from 'vue'
+import ZhCn from '../lang/zh-cn'
+
+export function makeRequiredRule() {
+	return {
+		type: 'Required',
+		field: 'formCreate$required',
+		title: '是否必填'
+	}
+}
+
+export function makeOptionsRule(t, to, flag) {
+	const options = [
+		{ label: t('props.optionsType.json'), value: 0 },
+		{ label: t('props.optionsType.fetch'), value: 1 }
+	]
+
+	const control = [
+		{
+			value: 0,
+			rule: [
+				{
+					type: 'Struct',
+					field: 'formCreate' + upper(to).replace('.', '>'),
+					props: { defaultValue: [] }
+				}
+			]
+		},
+		{
+			value: 1,
+			rule: [
+				{
+					type: 'Fetch',
+					field: 'formCreateEffect>fetch',
+					props: {
+						to
+					}
+				}
+			]
+		}
+	]
+
+	if (flag !== false) {
+		options.splice(0, 0, { label: t('props.optionsType.struct'), value: 2 })
+		control.push({
+			value: 2,
+			rule: [
+				{
+					type: 'TableOptions',
+					field: 'formCreate' + upper(to).replace('.', '>'),
+					props: { defaultValue: [] }
+				}
+			]
+		})
+	}
+
+	return {
+		type: 'radio',
+		title: t('props.options'),
+		field: '_optionType',
+		value: flag !== false ? 2 : 0,
+		options,
+		props: {
+			type: 'button'
+		},
+		control
+	}
+}
+
+export function upper(str) {
+	return str.replace(str[0], str[0].toLocaleUpperCase())
+}
+
+export const toJSON = function (val) {
+	const type = /object ([a-zA-Z]*)/.exec(Object.prototype.toString.call(val))
+	if (type && _toJSON[type[1].toLowerCase()]) {
+		return _toJSON[type[1].toLowerCase()](val)
+	} else {
+		return val
+	}
+}
+
+const _toJSON = {
+	object: function (val) {
+		const json = []
+		for (const i in val) {
+			if (!hasProperty(val, i)) continue
+			json.push(toJSON(i) + ': ' + (val[i] != null ? toJSON(val[i]) : 'null'))
+		}
+		return '{\n ' + json.join(',\n ') + '\n}'
+	},
+	function: function (val) {
+		val = '' + val
+		const exec = /^ *([\w]+) *\(/.exec(val)
+		if (exec && exec[1] !== 'function') {
+			return 'function ' + val
+		}
+		return val
+	},
+	array: function (val) {
+		for (var i = 0, json = []; i < val.length; i++) json[i] = val[i] != null ? toJSON(val[i]) : 'null'
+		return '[' + json.join(', ') + ']'
+	},
+	string: function (val) {
+		const tmp = val.split('')
+		for (let i = 0; i < tmp.length; i++) {
+			let c = tmp[i]
+			c >= ' '
+				? c === '\\'
+					? (tmp[i] = '\\\\')
+					: c === '"'
+					? (tmp[i] = '\\"')
+					: 0
+				: (tmp[i] =
+						c === '\n'
+							? '\\n'
+							: c === '\r'
+							? '\\r'
+							: c === '\t'
+							? '\\t'
+							: c === '\b'
+							? '\\b'
+							: c === '\f'
+							? '\\f'
+							: ((c = c.charCodeAt()), '\\u00' + (c > 15 ? 1 : 0) + (c % 16)))
+		}
+		return '"' + tmp.join('') + '"'
+	}
+}
+
+export const deepParseFn = function (target) {
+	if (target && typeof target === 'object') {
+		for (const key in target) {
+			if (Object.prototype.hasOwnProperty.call(target, key)) {
+				const data = target[key]
+				if (Array.isArray(data) || is.Object(data)) {
+					deepParseFn(data)
+				}
+				if (is.String(data)) {
+					target[key] = parseFn(data)
+				}
+			}
+		}
+	}
+	return target
+}
+
+function get(object, path, defaultValue) {
+	path = (path || '').split('.')
+
+	let index = 0,
+		length = path.length
+
+	while (object != null && index < length) {
+		object = object[path[index++]]
+	}
+	return index && index === length ? (object !== undefined ? object : defaultValue) : defaultValue
+}
+
+export const buildTranslator = locale => (path, option) => translate(path, option, unref(locale))
+
+export const translate = (path, option, locale) => get(locale, path, '').replace(/\{(\w+)\}/g, (_, key) => `${option?.[key] ?? `{${key}}`}`)
+
+export const buildLocaleContext = locale => {
+	const lang = computed(() => unref(locale).name)
+	const name = computed(() => upper(toCase(lang.value || '')))
+	const localeRef = isRef(locale) ? locale : ref(locale)
+	return {
+		lang,
+		name,
+		locale: localeRef,
+		t: buildTranslator(locale)
+	}
+}
+
+export const useLocale = locale => {
+	return buildLocaleContext(computed(() => locale.value || ZhCn))
+}
+
+export const localeProps = (t, prefix, rules) => {
+	return rules.map(rule => {
+		if (rule.field === 'formCreate$required') {
+			rule.title = t('props.required') || rule.title
+		} else if (rule.field && rule.field !== '_optionType') {
+			rule.title = t('components.' + prefix + '.' + rule.field) || rule.title
+		}
+		return rule
+	})
+}

+ 22 - 0
src/utils/locale.ts

@@ -0,0 +1,22 @@
+import { useLocale } from './formCreateIndex'
+import { ref } from 'vue'
+
+let _t = null
+const _locale = ref(null)
+
+function t(...args) {
+	return _t(...args)
+}
+
+const globalUseLocale = locale => {
+	_locale.value = locale
+	const data = useLocale(_locale)
+	_t = data.t
+	return data
+}
+
+globalUseLocale()
+
+export default globalUseLocale
+
+export { t }

+ 3 - 74
src/views/flow/create/components/FormDesign.vue

@@ -1,80 +1,9 @@
 <script setup name="FormDesign">
-defineProps({
-	label: {
-		type: String
-	},
-	name: {
-		type: String
-	}
-})
-import draggable from 'vuedraggable'
+import FcDesigner from '@/components/FormCreateDesigner/FcDesigner.vue'
 </script>
 
 <template>
-	<h1>123</h1>
+	<fc-designer ref="designer" />
 </template>
 
-<style scoped lang="scss">
-.form-design-wrap {
-}
-.leftItem {
-	padding-left: 50px;
-}
-
-.zj {
-	display: inline-block;
-	width: 140px;
-	margin: 5px;
-}
-
-$f22_width: 400px;
-
-$center_width: 360px;
-.drag-content {
-	min-height: 640px;
-	//border: 1px solid;
-	width: $center_width;
-	border-radius: 20px;
-	padding: 30px 10px;
-	background-color: white;
-	margin-left: calc(50% - ($center_width) / 2);
-	box-shadow: 0 0 10px grey;
-}
-
-.drag-content-inner {
-	background-color: var(--el-bg-color-page);
-	border-radius: 5px;
-	padding: 5px;
-}
-
-.f11 {
-	width: calc(100% - $f22_width);
-}
-
-.f22 {
-	width: $f22_width;
-}
-
-.okcomponent {
-	padding: 5px;
-	border-radius: 0px;
-	margin-bottom: 10px;
-	background-color: white;
-	border: 1px solid white;
-}
-
-.active-component {
-	border: 1px solid var(--el-color-primary);
-}
-
-.deleteIcon {
-	position: absolute;
-	margin-left: calc($center_width - 60px);
-	z-index: 20;
-}
-
-.deleteIcon:hover {
-	cursor: pointer;
-	color: palevioletred;
-}
-</style>
+<style scoped lang="scss"></style>