Browse Source

feat: 增加表单设计器

main
xingyu 2 years ago
parent
commit
c6a53accfa
  1. 1
      package.json
  2. 16
      pnpm-lock.yaml
  3. 4
      src/components/FormDesign/index.ts
  4. 71
      src/components/FormDesign/src/components/VFormCreate/components/FormRender.vue
  5. 139
      src/components/FormDesign/src/components/VFormCreate/index.vue
  6. 79
      src/components/FormDesign/src/components/VFormDesign/components/CodeModal.vue
  7. 219
      src/components/FormDesign/src/components/VFormDesign/components/ComponentProps.vue
  8. 64
      src/components/FormDesign/src/components/VFormDesign/components/FormItemColumnProps.vue
  9. 132
      src/components/FormDesign/src/components/VFormDesign/components/FormItemProps.vue
  10. 49
      src/components/FormDesign/src/components/VFormDesign/components/FormNode.vue
  11. 69
      src/components/FormDesign/src/components/VFormDesign/components/FormNodeOperate.vue
  12. 111
      src/components/FormDesign/src/components/VFormDesign/components/FormOptions.vue
  13. 94
      src/components/FormDesign/src/components/VFormDesign/components/FormProps.vue
  14. 130
      src/components/FormDesign/src/components/VFormDesign/components/ImportJsonModal.vue
  15. 64
      src/components/FormDesign/src/components/VFormDesign/components/JsonModal.vue
  16. 117
      src/components/FormDesign/src/components/VFormDesign/components/LayoutItem.vue
  17. 91
      src/components/FormDesign/src/components/VFormDesign/components/PreviewCode.vue
  18. 277
      src/components/FormDesign/src/components/VFormDesign/components/RuleProps.vue
  19. 1148
      src/components/FormDesign/src/components/VFormDesign/config/componentPropsConfig.ts
  20. 349
      src/components/FormDesign/src/components/VFormDesign/config/formItemPropsConfig.ts
  21. 335
      src/components/FormDesign/src/components/VFormDesign/index.vue
  22. 102
      src/components/FormDesign/src/components/VFormDesign/modules/CollapseItem.vue
  23. 155
      src/components/FormDesign/src/components/VFormDesign/modules/FormComponentPanel.vue
  24. 96
      src/components/FormDesign/src/components/VFormDesign/modules/PropsPanel.vue
  25. 136
      src/components/FormDesign/src/components/VFormDesign/modules/Toolbar.vue
  26. 226
      src/components/FormDesign/src/components/VFormDesign/styles/drag.less
  27. 15
      src/components/FormDesign/src/components/VFormDesign/styles/variable.less
  28. 213
      src/components/FormDesign/src/components/VFormItem/index.vue
  29. 68
      src/components/FormDesign/src/components/VFormItem/vFormItem.vue
  30. 96
      src/components/FormDesign/src/components/VFormPreview/index.vue
  31. 71
      src/components/FormDesign/src/components/VFormPreview/useForm.vue
  32. 69
      src/components/FormDesign/src/components/index.ts
  33. 420
      src/components/FormDesign/src/core/formItemConfig.ts
  34. 739
      src/components/FormDesign/src/core/iconConfig.ts
  35. 18
      src/components/FormDesign/src/hooks/useFormDesignState.ts
  36. 56
      src/components/FormDesign/src/hooks/useFormInstanceMethods.ts
  37. 188
      src/components/FormDesign/src/hooks/useVFormMethods.ts
  38. 10
      src/components/FormDesign/src/typings/base-type.ts
  39. 48
      src/components/FormDesign/src/typings/form-type.ts
  40. 344
      src/components/FormDesign/src/typings/v-form-component.ts
  41. 195
      src/components/FormDesign/src/utils/index.ts
  42. 18
      src/components/FormDesign/src/utils/message.ts
  43. 9
      src/views/infra/build/index.vue

1
package.json

@ -63,6 +63,7 @@
"vue-json-pretty": "^2.2.4",
"vue-router": "^4.1.6",
"vue-types": "^5.0.2",
"vuedraggable": "^4.1.0",
"xlsx": "^0.18.5"
},
"devDependencies": {

16
pnpm-lock.yaml

@ -91,6 +91,9 @@ dependencies:
vue-types:
specifier: ^5.0.2
version: 5.0.2(vue@3.2.47)
vuedraggable:
specifier: ^4.1.0
version: 4.1.0(vue@3.2.47)
xlsx:
specifier: ^0.18.5
version: 0.18.5
@ -7582,6 +7585,10 @@ packages:
- supports-color
dev: true
/sortablejs@1.14.0:
resolution: {integrity: sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==}
dev: false
/sortablejs@1.15.0:
resolution: {integrity: sha512-bv9qgVMjUMf89wAvM6AxVvS/4MX3sPeN0+agqShejLU5z5GX4C75ow1O2e5k4L6XItUyAK3gH6AxSbXrOM5e8w==}
dev: false
@ -8771,6 +8778,15 @@ packages:
'@vue/server-renderer': 3.2.47(vue@3.2.47)
'@vue/shared': 3.2.47
/vuedraggable@4.1.0(vue@3.2.47):
resolution: {integrity: sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==}
peerDependencies:
vue: ^3.0.1
dependencies:
sortablejs: 1.14.0
vue: 3.2.47
dev: false
/warning@4.0.3:
resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==}
dependencies:

4
src/components/FormDesign/index.ts

@ -0,0 +1,4 @@
import VFormDesign from './src/components/VFormDesign/index.vue'
import VFormCreate from './src/components/VFormCreate/index.vue'
export { VFormDesign, VFormCreate }

71
src/components/FormDesign/src/components/VFormCreate/components/FormRender.vue

@ -0,0 +1,71 @@
<template>
<template v-if="['Grid'].includes(schema.component)">
<Row class="grid-row">
<Col class="grid-col" v-for="(colItem, index) in schema.columns" :key="index" :span="colItem.span">
<FormRender
v-for="(item, k) in colItem.children"
:key="k"
:schema="item"
:formData="formData"
:formConfig="formConfig"
:setFormModel="setFormModel"
/>
</Col>
</Row>
</template>
<VFormItem
v-else
:formConfig="formConfig"
:schema="schema"
:formData="formData"
:setFormModel="setFormModel"
@change="$emit('change', { schema: schema, value: $event })"
@submit="$emit('submit', schema)"
@reset="$emit('reset')"
>
<template v-if="schema.componentProps && schema.componentProps.slotName" #[schema.componentProps!.slotName]>
<slot :name="schema.componentProps!.slotName"></slot>
</template>
</VFormItem>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue'
import { IVFormComponent, IFormConfig } from '../../../typings/v-form-component'
import VFormItem from '../../VFormItem/index.vue'
import { Row, Col } from 'ant-design-vue'
export default defineComponent({
name: 'FormRender',
components: {
VFormItem,
Row,
Col
},
props: {
formData: {
type: Object,
default: () => ({})
},
schema: {
type: Object as PropType<IVFormComponent>,
default: () => ({})
},
formConfig: {
type: Object as PropType<IFormConfig>,
default: () => [] as IFormConfig[]
},
setFormModel: {
type: Function as PropType<(key: string, value: any) => void>,
default: null
}
},
emits: ['change', 'submit', 'reset'],
setup(_props) {}
})
</script>
<style>
.v-form-render-item {
overflow: hidden;
}
</style>

139
src/components/FormDesign/src/components/VFormCreate/index.vue

@ -0,0 +1,139 @@
<!--
* @Description: 表单渲染器根据json生成表单
-->
<template>
<div class="v-form-container">
<Form class="v-form-model" ref="eFormModel" :model="formModel" v-bind="formModelProps">
<Row>
<FormRender
v-for="(schema, index) of noHiddenList"
:key="index"
:schema="schema"
:formConfig="formConfig"
:formData="formModelNew"
@change="handleChange"
:setFormModel="setFormModel"
@submit="handleSubmit"
@reset="resetFields"
>
<template v-if="schema && schema.componentProps" #[`schema.componentProps!.slotName`]>
<slot :name="schema.componentProps!.slotName" v-bind="{ formModel: formModel, field: schema.field, schema }"></slot>
</template>
</FormRender>
</Row>
</Form>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, PropType, provide, ref, unref } from 'vue'
import FormRender from './components/FormRender.vue'
import { IFormConfig, AForm } from '../../typings/v-form-component'
import { Form, Row, Col } from 'ant-design-vue'
import { useFormInstanceMethods } from '../../hooks/useFormInstanceMethods'
import { IProps, IVFormMethods, useVFormMethods } from '../../hooks/useVFormMethods'
import { useVModel } from '@vueuse/core'
import { omit } from 'lodash-es'
export default defineComponent({
name: 'VFormCreate',
components: {
FormRender,
Form,
Row
},
props: {
fApi: {
type: Object
},
formModel: {
type: Object,
default: () => ({})
},
formConfig: {
type: Object as PropType<IFormConfig>,
required: true
}
},
emits: ['submit', 'change', 'update:fApi', 'update:formModel'],
setup(props, context) {
const wrapperComp = props.formConfig.layout == 'vertical' ? Col : Row
const { emit } = context
const eFormModel = ref<AForm | null>(null)
const formModelNew = computed({
get: () => props.formModel,
set: (value) => emit('update:formModel', value)
})
const noHiddenList = computed(() => {
return props.formConfig.schemas && props.formConfig.schemas.filter((item) => item.hidden !== true)
})
const fApi = useVModel(props, 'fApi', emit)
const { submit, validate, clearValidate, resetFields, validateField } = useFormInstanceMethods(props, formModelNew, context, eFormModel)
const { linkOn, ...methods } = useVFormMethods(
{ formConfig: props.formConfig, formData: props.formModel } as unknown as IProps,
context,
eFormModel,
{
submit,
validate,
validateField,
resetFields,
clearValidate
}
)
fApi.value = methods
const handleChange = (_event) => {
const { schema, value } = _event
const { field } = unref(schema)
linkOn[field!]?.forEach((formItem) => {
formItem.update?.(value, formItem, fApi.value as IVFormMethods)
})
}
/**
* 获取表单属性
*/
const formModelProps = computed(() => omit(props.formConfig, ['disabled', 'labelWidth', 'schemas']) as Recordable)
const handleSubmit = () => {
submit()
}
provide('formModel', formModelNew)
const setFormModel = (key, value) => {
formModelNew.value[key] = value
}
provide<(key: String, value: any) => void>('setFormModelMethod', setFormModel)
// inject
return {
eFormModel,
submit,
validate,
validateField,
resetFields,
clearValidate,
handleChange,
formModelProps,
handleSubmit,
setFormModel,
formModelNew,
wrapperComp,
noHiddenList
}
}
})
</script>
<style lang="less" scoped>
.v-form-model {
overflow: hidden;
}
</style>

79
src/components/FormDesign/src/components/VFormDesign/components/CodeModal.vue

@ -0,0 +1,79 @@
<!--
* @Description: 渲染代码
-->
<template>
<Modal
title="代码"
:footer="null"
:visible="visible"
@cancel="visible = false"
wrapClassName="v-code-modal"
style="top: 20px"
width="850px"
:destroyOnClose="true"
>
<PreviewCode :editorJson="editorVueJson" fileFormat="vue" />
</Modal>
</template>
<script lang="ts">
import { computed, defineComponent, reactive, toRefs } from 'vue'
import { formatRules, removeAttrs } from '../../../utils'
import PreviewCode from './PreviewCode.vue'
import { IFormConfig } from '../../../typings/v-form-component'
import { Modal } from 'ant-design-vue'
const codeVueFront = `<template>
<div>
<v-form-create
:formConfig="formConfig"
:formData="formData"
v-model="fApi"
/>
<a-button @click="submit">提交</a-button>
</div>
</template>
<script>
export default {
name: 'Demo',
data () {
return {
fApi:{},
formData:{},
formConfig: `
/* eslint-disable */
let codeVueLast = `
}
},
methods: {
async submit() {
const data = await this.fApi.submit()
console.log(data)
}
}
}
<\/script>`
//
export default defineComponent({
name: 'CodeModal',
components: { PreviewCode, Modal },
setup() {
const state = reactive({
visible: false,
jsonData: {} as IFormConfig
})
const showModal = (formConfig: IFormConfig) => {
formConfig.schemas && formatRules(formConfig.schemas)
state.visible = true
state.jsonData = formConfig
}
const editorVueJson = computed(() => {
return codeVueFront + JSON.stringify(removeAttrs(state.jsonData), null, '\t') + codeVueLast
})
return { ...toRefs(state), editorVueJson, showModal }
}
})
</script>

219
src/components/FormDesign/src/components/VFormDesign/components/ComponentProps.vue

@ -0,0 +1,219 @@
<!--
* @Description: 组件属性控件
-->
<template>
<div class="properties-content">
<div class="properties-body" v-if="formConfig.currentItem">
<Empty class="hint-box" v-if="!formConfig.currentItem.key" description="未选择组件" />
<Form label-align="left" layout="vertical">
<!-- 循环遍历渲染组件属性 -->
<div v-if="formConfig.currentItem && formConfig.currentItem.componentProps">
<FormItem v-for="item in inputOptions" :key="item.name" :label="item.label">
<!-- 处理数组属性placeholder -->
<div v-if="item.children">
<component
v-for="(child, index) of item.children"
:key="index"
v-bind="child.componentProps"
:is="child.component"
v-model:value="formConfig.currentItem.componentProps[item.name][index]"
/>
</div>
<!-- 如果不是数组则正常处理属性值 -->
<component
v-else
class="component-prop"
v-bind="item.componentProps"
:is="item.component"
v-model:value="formConfig.currentItem.componentProps[item.name]"
/>
</FormItem>
<FormItem label="控制属性">
<Col v-for="item in controlOptions" :key="item.name">
<Checkbox
v-if="showControlAttrs(item.includes)"
v-bind="item.componentProps"
v-model:checked="formConfig.currentItem.componentProps[item.name]"
>
{{ item.label }}
</Checkbox>
</Col>
</FormItem>
</div>
<FormItem label="关联字段">
<Select mode="multiple" v-model:value="formConfig.currentItem['link']" :options="linkOptions" />
</FormItem>
<FormItem
label="选项"
v-if="
['Select', 'CheckboxGroup', 'RadioGroup', 'TreeSelect', 'Cascader', 'AutoComplete'].includes(formConfig.currentItem.component)
"
>
<FormOptions />
</FormItem>
<FormItem label="栅格" v-if="['Grid'].includes(formConfig.currentItem.component)">
<FormOptions />
</FormItem>
</Form>
</div>
</div>
</template>
<script lang="ts">
import { Empty, Input, Form, FormItem, Switch, Checkbox, Select, InputNumber, RadioGroup } from 'ant-design-vue'
import RadioButtonGroup from '@/components/Form/src/components/RadioButtonGroup.vue'
import { Col, Row } from 'ant-design-vue'
import { computed, defineComponent, ref, watch } from 'vue'
import { useFormDesignState } from '../../../hooks/useFormDesignState'
import {
baseComponentControlAttrs,
baseComponentAttrs,
baseComponentCommonAttrs,
componentPropsFuncs
} from '../../VFormDesign/config/componentPropsConfig'
import FormOptions from './FormOptions.vue'
import { formItemsForEach, remove } from '../../../utils'
import { IBaseFormAttrs } from '../config/formItemPropsConfig'
export default defineComponent({
name: 'ComponentProps',
components: {
FormOptions,
Empty,
Input,
Form,
FormItem,
Switch,
Checkbox,
Select,
InputNumber,
RadioGroup,
RadioButtonGroup,
Col,
Row
},
setup() {
// compuated
const allOptions = ref([] as Omit<IBaseFormAttrs, 'tag'>[])
const showControlAttrs = (includes: string[] | undefined) => {
if (!includes) return true
return includes.includes(formConfig.value.currentItem!.component)
}
const { formConfig } = useFormDesignState()
if (formConfig.value.currentItem) {
formConfig.value.currentItem.componentProps = formConfig.value.currentItem.componentProps || {}
}
watch(
() => formConfig.value.currentItem?.field,
(_newValue, oldValue) => {
formConfig.value.schemas &&
formItemsForEach(formConfig.value.schemas, (item) => {
if (item.link) {
const index = item.link.findIndex((linkItem) => linkItem === oldValue)
index !== -1 && remove(item.link, index)
}
})
}
)
watch(
() => formConfig.value.currentItem && formConfig.value.currentItem.component,
() => {
allOptions.value = []
baseComponentControlAttrs.forEach((item) => {
item.category = 'control'
if (!item.includes) {
// include
allOptions.value.push(item)
} else if (item.includes.includes(formConfig.value.currentItem!.component)) {
// include
allOptions.value.push(item)
}
})
baseComponentCommonAttrs.forEach((item) => {
item.category = 'input'
if (item.includes) {
if (item.includes.includes(formConfig.value.currentItem!.component)) {
allOptions.value.push(item)
}
} else if (item.exclude) {
if (!item.exclude.includes(formConfig.value.currentItem!.component)) {
allOptions.value.push(item)
}
} else {
allOptions.value.push(item)
}
})
baseComponentAttrs[formConfig.value.currentItem!.component] &&
baseComponentAttrs[formConfig.value.currentItem!.component].forEach(async (item) => {
if (item.component) {
if (['Switch', 'Checkbox', 'Radio'].includes(item.component)) {
item.category = 'control'
allOptions.value.push(item)
} else {
item.category = 'input'
allOptions.value.push(item)
}
}
})
},
{
immediate: true
}
)
//
const controlOptions = computed(() => {
return allOptions.value.filter((item) => {
return item.category == 'control'
})
})
//
const inputOptions = computed(() => {
return allOptions.value.filter((item) => {
return item.category == 'input'
})
})
watch(
() => formConfig.value.currentItem!.componentProps,
() => {
const func = componentPropsFuncs[formConfig.value.currentItem!.component]
if (func) {
func(formConfig.value.currentItem!.componentProps, allOptions.value)
}
},
{
immediate: true,
deep: true
}
)
const linkOptions = computed(() => {
return (
formConfig.value.schemas &&
formConfig.value.schemas
.filter((item) => item.key !== formConfig.value.currentItem!.key)
.map(({ label, field }) => ({ label: label + '/' + field, value: field }))
)
})
return {
formConfig,
showControlAttrs,
linkOptions,
controlOptions,
inputOptions
}
}
})
</script>

64
src/components/FormDesign/src/components/VFormDesign/components/FormItemColumnProps.vue

@ -0,0 +1,64 @@
<!--
* @Description: 表单项属性
-->
<template>
<div class="properties-content">
<div class="properties-body" v-if="formConfig.currentItem">
<Empty class="hint-box" v-if="!formConfig.currentItem.key" description="未选择控件" />
<Form v-else label-align="left" layout="vertical">
<div v-for="item of baseItemColumnProps" :key="item.name">
<FormItem :label="item.label" v-if="showProps(item.exclude)">
<component
v-if="formConfig.currentItem.colProps"
class="component-props"
v-bind="item.componentProps"
:is="item.component"
v-model:value="formConfig.currentItem.colProps[item.name]"
/>
</FormItem>
</div>
</Form>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { baseItemColumnProps } from '../config/formItemPropsConfig'
import { Empty, Input, Form, FormItem, Switch, Checkbox, Select, Slider } from 'ant-design-vue'
import RuleProps from './RuleProps.vue'
import { useFormDesignState } from '../../../hooks/useFormDesignState'
import { isArray } from 'lodash-es'
export default defineComponent({
name: 'FormItemProps',
components: {
RuleProps,
Empty,
Input,
Form,
FormItem,
Switch,
Checkbox,
Select,
Slider
},
// props: {} as PropsOptions,
setup() {
const { formConfig } = useFormDesignState()
const showProps = (exclude: string[] | undefined) => {
if (!exclude) {
return true
}
return isArray(exclude) ? !exclude.includes(formConfig.value.currentItem!.component) : true
}
return {
baseItemColumnProps,
formConfig,
showProps
}
}
})
</script>

132
src/components/FormDesign/src/components/VFormDesign/components/FormItemProps.vue

@ -0,0 +1,132 @@
<!--
* @Description: 表单项属性控件属性面板
-->
<template>
<div class="properties-content">
<div class="properties-body" v-if="formConfig.currentItem?.itemProps">
<Empty class="hint-box" v-if="!formConfig.currentItem.key" description="未选择控件" />
<Form v-else label-align="left" layout="vertical">
<div v-for="item of baseFormItemProps" :key="item.name">
<FormItem :label="item.label" v-if="showProps(item.exclude)">
<component
class="component-props"
v-bind="item.componentProps"
:is="item.component"
v-model:value="formConfig.currentItem[item.name]"
/>
</FormItem>
</div>
<div v-for="item of advanceFormItemProps" :key="item.name">
<FormItem :label="item.label" v-if="showProps(item.exclude)">
<component
class="component-props"
v-bind="item.componentProps"
:is="item.component"
v-model:value="formConfig.currentItem.itemProps[item.name]"
/>
</FormItem> </div
><div v-for="item of advanceFormItemColProps" :key="item.name">
<FormItem :label="item.label" v-if="showProps(item.exclude)">
<component
class="component-props"
v-bind="item.componentProps"
:is="item.component"
v-model:value="formConfig.currentItem.itemProps[item.name]['span']"
/>
</FormItem>
</div>
<FormItem label="控制属性" v-if="controlPropsList.length">
<Col v-for="item of controlPropsList" :key="item.name">
<Checkbox v-model:checked="formConfig.currentItem.itemProps[item.name]">
{{ item.label }}
</Checkbox>
</Col>
</FormItem>
<FormItem label="是否必选" v-if="!['Grid'].includes(formConfig.currentItem.component)">
<Switch v-model:checked="formConfig.currentItem.itemProps['required']" />
<Input
v-if="formConfig.currentItem.itemProps['required']"
v-model:value="formConfig.currentItem.itemProps['message']"
placeholder="请输入必选提示"
/>
</FormItem>
<FormItem
v-if="!['Grid'].includes(formConfig.currentItem.component)"
label="校验规则"
:class="{ 'form-rule-props': !!formConfig.currentItem.itemProps['rules'] }"
>
<RuleProps />
</FormItem>
</Form>
</div>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, watch } from 'vue'
import {
baseFormItemControlAttrs,
baseFormItemProps,
advanceFormItemProps,
advanceFormItemColProps
} from '../../VFormDesign/config/formItemPropsConfig'
import { Empty, Input, Form, FormItem, Switch, Checkbox, Select, Slider, Col, RadioGroup } from 'ant-design-vue'
import RuleProps from './RuleProps.vue'
import { useFormDesignState } from '../../../hooks/useFormDesignState'
import { isArray } from 'lodash-es'
export default defineComponent({
name: 'FormItemProps',
components: {
RuleProps,
Empty,
Input,
Form,
FormItem,
Switch,
Checkbox,
Select,
Slider,
Col,
RadioGroup
},
// props: {} as PropsOptions,
setup() {
const { formConfig } = useFormDesignState()
watch(
() => formConfig.value,
() => {
if (formConfig.value.currentItem) {
formConfig.value.currentItem.itemProps = formConfig.value.currentItem.itemProps || {}
formConfig.value.currentItem.itemProps.labelCol = formConfig.value.currentItem.itemProps.labelCol || {}
formConfig.value.currentItem.itemProps.wrapperCol = formConfig.value.currentItem.itemProps.wrapperCol || {}
}
},
{ deep: true, immediate: true }
)
const showProps = (exclude: string[] | undefined) => {
if (!exclude) {
return true
}
return isArray(exclude) ? !exclude.includes(formConfig.value.currentItem!.component) : true
}
const controlPropsList = computed(() => {
return baseFormItemControlAttrs.filter((item) => {
return showProps(item.exclude)
})
})
return {
baseFormItemProps,
advanceFormItemProps,
advanceFormItemColProps,
formConfig,
controlPropsList,
showProps
}
}
})
</script>

49
src/components/FormDesign/src/components/VFormDesign/components/FormNode.vue

@ -0,0 +1,49 @@
<!--
* @Description: 拖拽节点控件
-->
<template>
<div class="drag-move-box" @click.stop="handleSelectItem" :class="{ active: schema.key === formConfig.currentItem?.key }">
<div class="form-item-box">
<VFormItem :formConfig="formConfig" :schema="schema" />
</div>
<div class="show-key-box">
{{ schema.label + (schema.field ? '/' + schema.field : '') }}
</div>
<FormNodeOperate :schema="schema" :currentItem="formConfig.currentItem" />
</div>
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs, PropType } from 'vue'
import { IVFormComponent } from '../../../typings/v-form-component'
import FormNodeOperate from './FormNodeOperate.vue'
import { useFormDesignState } from '../../../hooks/useFormDesignState'
import VFormItem from '../../VFormItem/index.vue'
// import VFormItem from '../../VFormItem/vFormItem.vue';
export default defineComponent({
name: 'FormNode',
components: {
VFormItem,
FormNodeOperate
},
props: {
schema: {
type: Object as PropType<IVFormComponent>,
required: true
}
},
setup(props) {
const { formConfig, formDesignMethods } = useFormDesignState()
const state = reactive({})
// formDesignMethods
const handleSelectItem = () => {
// formDesignMethods
formDesignMethods.handleSetSelectItem(props.schema)
}
return {
...toRefs(state),
handleSelectItem,
formConfig
}
}
})
</script>

69
src/components/FormDesign/src/components/VFormDesign/components/FormNodeOperate.vue

@ -0,0 +1,69 @@
<!--
* @Description: 节点操作复制删除控件
-->
<template>
<div class="copy-delete-box">
<a class="copy" :class="activeClass" @click.stop="handleCopy">
<Icon icon="ant-design:copy-outlined" />
</a>
<a class="delete" :class="activeClass" @click.stop="handleDelete">
<Icon icon="ant-design:delete-outlined" />
</a>
</div>
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue'
import { IVFormComponent } from '../../../typings/v-form-component'
import { remove } from '../../../utils'
import { useFormDesignState } from '../../../hooks/useFormDesignState'
import Icon from '@/components/Icon/index'
export default defineComponent({
name: 'FormNodeOperate',
components: {
Icon
},
props: {
schema: {
type: Object,
default: () => ({})
},
currentItem: {
type: Object,
default: () => ({})
}
},
setup(props) {
const { formConfig, formDesignMethods } = useFormDesignState()
const activeClass = computed(() => {
return props.schema.key === props.currentItem.key ? 'active' : 'unactivated'
})
/**
* 删除当前项
*/
const handleDelete = () => {
const traverse = (schemas: IVFormComponent[]) => {
schemas.some((formItem, index) => {
const { component, key } = formItem
//
;['Grid', 'Tabs'].includes(component) && formItem.columns?.forEach((item) => traverse(item.children))
if (key === props.currentItem.key) {
let params: IVFormComponent =
schemas.length === 1 ? { component: '' } : schemas.length - 1 > index ? schemas[index + 1] : schemas[index - 1]
formDesignMethods.handleSetSelectItem(params)
remove(schemas, index)
return true
}
})
}
traverse(formConfig.value!.schemas)
}
const handleCopy = () => {
formDesignMethods.handleCopy()
}
return { activeClass, handleDelete, handleCopy }
}
})
</script>

111
src/components/FormDesign/src/components/VFormDesign/components/FormOptions.vue

@ -0,0 +1,111 @@
<template>
<div>
<div v-if="['Grid'].includes(formConfig.currentItem!.component)">
<div v-for="(item, index) of formConfig.currentItem!['columns']" :key="index">
<div class="options-box">
<Input v-model:value="item.span" class="options-value" />
<a class="options-delete" @click="deleteGridOptions(index)">
<Icon icon="ant-design:delete-outlined" />
</a>
</div>
</div>
<a @click="addGridOptions">
<Icon icon="ant-design:file-add-outlined" />
添加栅格
</a>
</div>
<div v-else>
<div v-for="(item, index) of formConfig.currentItem!.componentProps![key]" :key="index">
<div class="options-box">
<Input v-model:value="item.label" />
<Input v-model:value="item.value" class="options-value" />
<a class="options-delete" @click="deleteOptions(index)">
<Icon icon="ant-design:delete-outlined" />
</a>
</div>
</div>
<a @click="addOptions">
<Icon icon="ant-design:file-add-outlined" />
添加选项
</a>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs } from 'vue'
import { useFormDesignState } from '../../../hooks/useFormDesignState'
import { remove } from '../../../utils'
import message from '../../../utils/message'
import { Input } from 'ant-design-vue'
import Icon from '@/components/Icon/index'
export default defineComponent({
name: 'FormOptions',
components: { Input, Icon },
// props: {},
setup() {
const state = reactive({})
const { formConfig } = useFormDesignState()
const key = formConfig.value.currentItem?.component === 'TreeSelect' ? 'treeData' : 'options'
const addOptions = () => {
if (!formConfig.value.currentItem?.componentProps?.[key]) formConfig.value.currentItem!.componentProps![key] = []
const len = formConfig.value.currentItem?.componentProps?.[key].length + 1
formConfig.value.currentItem!.componentProps![key].push({
label: `选项${len}`,
value: '' + len
})
}
const deleteOptions = (index: number) => {
remove(formConfig.value.currentItem?.componentProps?.[key], index)
}
const addGridOptions = () => {
formConfig.value.currentItem?.['columns']?.push({
span: 12,
children: []
})
}
const deleteGridOptions = (index: number) => {
if (index === 0) return message.warning('请至少保留一个栅格')
remove(formConfig.value.currentItem!['columns']!, index)
}
return {
...toRefs(state),
formConfig,
addOptions,
deleteOptions,
key,
deleteGridOptions,
addGridOptions
}
}
})
</script>
<style lang="less" scoped>
.options-box {
display: flex;
align-items: center;
margin-bottom: 5px;
.options-value {
margin: 0 8px;
}
.options-delete {
width: 30px;
height: 30px;
flex-shrink: 0;
line-height: 30px;
text-align: center;
border-radius: 50%;
background: #f5f5f5;
color: #666;
&:hover {
background: #ff4d4f;
}
}
}
</style>

94
src/components/FormDesign/src/components/VFormDesign/components/FormProps.vue

@ -0,0 +1,94 @@
<!--
* @Description: 右侧属性面板控件 表单属性面板
-->
<template>
<div class="properties-content">
<Form class="properties-body" label-align="left" layout="vertical">
<!-- <e-upload v-model="fileList"></e-upload>-->
<FormItem label="表单布局">
<RadioGroup button-style="solid" v-model:value="formConfig.layout">
<RadioButton value="horizontal">水平</RadioButton>
<RadioButton value="vertical" :disabled="formConfig.labelLayout === 'Grid'"> 垂直 </RadioButton>
<RadioButton value="inline" :disabled="formConfig.labelLayout === 'Grid'"> 行内 </RadioButton>
</RadioGroup>
</FormItem>
<!-- <Row> -->
<FormItem label="标签布局">
<RadioGroup buttonStyle="solid" v-model:value="formConfig.labelLayout" @change="lableLayoutChange">
<RadioButton value="flex">固定</RadioButton>
<RadioButton value="Grid" :disabled="formConfig.layout !== 'horizontal'"> 栅格 </RadioButton>
</RadioGroup>
</FormItem>
<!-- </Row> -->
<FormItem label="标签宽度(px)" v-show="formConfig.labelLayout === 'flex'">
<InputNumber :style="{ width: '100%' }" v-model:value="formConfig.labelWidth" :min="0" :step="1" />
</FormItem>
<div v-if="formConfig.labelLayout === 'Grid'">
<FormItem label="labelCol">
<Slider v-model:value="formConfig.labelCol!.span" :max="24" />
</FormItem>
<FormItem label="wrapperCol">
<Slider v-model:value="formConfig.wrapperCol!.span" :max="24" />
</FormItem>
<FormItem label="标签对齐">
<RadioGroup button-style="solid" v-model:value="formConfig.labelAlign">
<RadioButton value="left">靠左</RadioButton>
<RadioButton value="right">靠右</RadioButton>
</RadioGroup>
</FormItem>
<FormItem label="控件大小">
<RadioGroup button-style="solid" v-model:value="formConfig.size">
<RadioButton value="default">默认</RadioButton>
<RadioButton value="small"></RadioButton>
<RadioButton value="large"></RadioButton>
</RadioGroup>
</FormItem>
</div>
<FormItem label="表单属性">
<Col><Checkbox v-model:checked="formConfig.colon" v-if="formConfig.layout == 'horizontal'">label后面显示冒号</Checkbox></Col>
<Col><Checkbox v-model:checked="formConfig.disabled">禁用</Checkbox></Col>
<Col><Checkbox v-model:checked="formConfig.hideRequiredMark">隐藏必选标记</Checkbox></Col>
</FormItem>
</Form>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { useFormDesignState } from '../../../hooks/useFormDesignState'
import { InputNumber, Slider, Checkbox, Col, RadioChangeEvent } from 'ant-design-vue'
// import RadioButtonGroup from '/@/components/RadioButtonGroup.vue';
import { Form, FormItem, Radio } from 'ant-design-vue'
export default defineComponent({
name: 'FormProps',
components: {
InputNumber,
Slider,
Checkbox,
RadioGroup: Radio.Group,
RadioButton: Radio.Button,
Form,
FormItem,
Col
},
setup() {
const { formConfig } = useFormDesignState()
formConfig.value = formConfig.value || {
labelCol: { span: 24 },
wrapperCol: { span: 24 }
}
const lableLayoutChange = (e: RadioChangeEvent) => {
if (e.target.value === 'Grid') {
formConfig.value.layout = 'horizontal'
}
}
return { formConfig, lableLayoutChange }
}
})
</script>

130
src/components/FormDesign/src/components/VFormDesign/components/ImportJsonModal.vue

@ -0,0 +1,130 @@
<!--
* @Description: 导入JSON模板
-->
<template>
<Modal
title="JSON数据"
:visible="visible"
@ok="handleImportJson"
@cancel="handleCancel"
cancelText="关闭"
:destroyOnClose="true"
wrapClassName="v-code-modal"
style="top: 20px"
:width="850"
>
<p class="hint-box">导入格式如下:</p>
<div class="v-json-box">
<CodeEditor v-model:value="json" ref="myEditor" :mode="MODE.JSON" />
</div>
<template #footer>
<a-button @click="handleCancel">取消</a-button>
<Upload class="upload-button" :beforeUpload="beforeUpload" :showUploadList="false" accept="application/json">
<a-button type="primary">导入json文件</a-button>
</Upload>
<a-button type="primary" @click="handleImportJson">确定</a-button>
</template>
</Modal>
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs } from 'vue'
// import message from '../../../utils/message';
import { useFormDesignState } from '../../../hooks/useFormDesignState'
// import { codemirror } from 'vue-codemirror-lite';
import { IFormConfig } from '../../../typings/v-form-component'
import { formItemsForEach, generateKey } from '../../../utils'
import { CodeEditor, MODE } from '@/components/CodeEditor'
import { useMessage } from '@/hooks/web/useMessage'
import { Upload, Modal } from 'ant-design-vue'
export default defineComponent({
name: 'ImportJsonModal',
components: {
CodeEditor,
Upload,
Modal
},
setup() {
const { createMessage } = useMessage()
const state = reactive({
visible: false,
json: `{
"schemas": [
{
"component": "input",
"label": "输入框",
"field": "input_2",
"span": 24,
"props": {
"type": "text"
}
}
],
"layout": "horizontal",
"labelLayout": "flex",
"labelWidth": 100,
"labelCol": {},
"wrapperCol": {}
}`,
jsonData: {
schemas: {},
config: {}
},
handleSetSelectItem: null
})
const { formDesignMethods } = useFormDesignState()
const handleCancel = () => {
state.visible = false
}
const showModal = () => {
state.visible = true
}
const handleImportJson = () => {
// JSON
try {
const editorJsonData = JSON.parse(state.json) as IFormConfig
editorJsonData.schemas &&
formItemsForEach(editorJsonData.schemas, (formItem) => {
generateKey(formItem)
})
formDesignMethods.setFormConfig({
...editorJsonData,
activeKey: 1,
currentItem: { component: '' }
})
handleCancel()
createMessage.success('导入成功')
} catch {
createMessage.error('导入失败,数据格式不对')
}
}
const beforeUpload = (e: File) => {
// json
const reader = new FileReader()
reader.readAsText(e)
reader.onload = function () {
state.json = this.result as string
handleImportJson()
}
return false
}
return {
handleImportJson,
beforeUpload,
handleCancel,
showModal,
...toRefs(state),
MODE
}
}
})
</script>
<style lang="less" scoped>
.upload-button {
margin: 0 10px;
}
</style>

64
src/components/FormDesign/src/components/VFormDesign/components/JsonModal.vue

@ -0,0 +1,64 @@
<!--
* @Description: 渲染JSON数据
-->
<template>
<Modal
title="JSON数据"
:footer="null"
:visible="visible"
@cancel="handleCancel"
:destroyOnClose="true"
wrapClassName="v-code-modal"
style="top: 20px"
width="850px"
>
<PreviewCode :editorJson="editorJson" />
</Modal>
</template>
<script lang="ts">
import { computed, defineComponent, reactive, toRefs } from 'vue'
import PreviewCode from './PreviewCode.vue'
import { IFormConfig } from '../../../typings/v-form-component'
import { formatRules, removeAttrs } from '../../../utils'
import { Modal } from 'ant-design-vue'
export default defineComponent({
name: 'JsonModal',
components: {
PreviewCode,
Modal
},
emits: ['cancel'],
setup(_props, { emit }) {
const state = reactive<{
visible: boolean
jsonData: IFormConfig
}>({
visible: false, // json
jsonData: {} as IFormConfig // json
})
/**
* 显示Json数据弹框
* @param jsonData
*/
const showModal = (jsonData: IFormConfig) => {
formatRules(jsonData.schemas)
state.jsonData = jsonData
state.visible = true
}
// json
const editorJson = computed(() => {
return JSON.stringify(removeAttrs(state.jsonData), null, '\t')
})
//
const handleCancel = () => {
state.visible = false
emit('cancel')
}
return { ...toRefs(state), editorJson, handleCancel, showModal }
}
})
</script>

117
src/components/FormDesign/src/components/VFormDesign/components/LayoutItem.vue

@ -0,0 +1,117 @@
<!--
* @Description: 表单项布局控件
* 千万不要在template下面的第一行加注释因为这里拖动的第一个元素
-->
<template>
<Col v-bind="colPropsComputed">
<template v-if="['Grid'].includes(schema.component)">
<div class="grid-box" :class="{ active: schema.key === currentItem.key }" @click.stop="handleSetSelectItem(schema)">
<Row class="grid-row" v-bind="schema.componentProps">
<Col class="grid-col" v-for="(colItem, index) in schema.columns" :key="index" :span="colItem.span">
<draggable
class="list-main draggable-box"
:component-data="{ name: 'list', tag: 'div', type: 'transition-group' }"
v-bind="{
group: 'form-draggable',
ghostClass: 'moving',
animation: 180,
handle: '.drag-move'
}"
item-key="key"
v-model="colItem.children"
@start="$emit('dragStart', $event, colItem.children)"
@add="$emit('handleColAdd', $event, colItem.children)"
>
<template #item="{ element }">
<LayoutItem
class="drag-move"
:schema="element"
:current-item="currentItem"
@handle-copy="$emit('handle-copy')"
@handle-delete="$emit('handle-delete')"
/>
</template>
</draggable>
</Col>
</Row>
<FormNodeOperate :schema="schema" :currentItem="currentItem" />
</div>
</template>
<FormNode
v-else
:key="schema.key"
:schema="schema"
:current-item="currentItem"
@handle-copy="$emit('handle-copy')"
@handle-delete="$emit('handle-delete')"
/>
</Col>
</template>
<script lang="ts">
import { computed, defineComponent, PropType, reactive, toRefs } from 'vue'
import draggable from 'vuedraggable'
import FormNode from './FormNode.vue'
import FormNodeOperate from './FormNodeOperate.vue'
import { useFormDesignState } from '../../../hooks/useFormDesignState'
import { IVFormComponent } from '../../../typings/v-form-component'
import { Row, Col } from 'ant-design-vue'
export default defineComponent({
name: 'LayoutItem',
components: {
FormNode,
FormNodeOperate,
draggable,
Row,
Col
},
props: {
schema: {
type: Object as PropType<IVFormComponent>,
required: true
},
currentItem: {
type: Object,
required: true
}
},
emits: ['dragStart', 'handleColAdd', 'handle-copy', 'handle-delete'],
setup(props) {
const {
formDesignMethods: { handleSetSelectItem },
formConfig
} = useFormDesignState()
const state = reactive({})
const colPropsComputed = computed(() => {
const { colProps = {} } = props.schema
return colProps
})
const list1 = computed(() => props.schema.columns)
// AColdiv
const layoutTag = computed(() => {
return formConfig.value.layout === 'horizontal' ? 'Col' : 'div'
})
return {
...toRefs(state),
colPropsComputed,
handleSetSelectItem,
layoutTag,
list1
}
}
})
</script>
<style lang="less">
@import url('../styles/variable.less');
.layout-width {
width: 100%;
}
.hidden-item {
background-color: rgb(240 191 195);
}
</style>

91
src/components/FormDesign/src/components/VFormDesign/components/PreviewCode.vue

@ -0,0 +1,91 @@
<template>
<div>
<div class="v-json-box">
<CodeEditor :value="editorJson" ref="myEditor" :mode="MODE.JSON" />
</div>
<div class="copy-btn-box">
<a-button @click="handleCopyJson" type="primary" class="copy-btn" data-clipboard-action="copy" :data-clipboard-text="editorJson">
复制数据
</a-button>
<a-button @click="handleExportJson" type="primary">导出代码</a-button>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs, unref } from 'vue'
import { CodeEditor, MODE } from '@/components/CodeEditor'
import { useCopyToClipboard } from '@/hooks/web/useCopyToClipboard'
import { useMessage } from '@/hooks/web/useMessage'
export default defineComponent({
name: 'PreviewCode',
components: {
CodeEditor
},
props: {
fileFormat: {
type: String,
default: 'json'
},
editorJson: {
type: String,
default: ''
}
},
setup(props) {
const state = reactive({
visible: false
})
const exportData = (data: string, fileName = `file.${props.fileFormat}`) => {
let content = 'data:text/csv;charset=utf-8,'
content += data
const encodedUri = encodeURI(content)
const actions = document.createElement('a')
actions.setAttribute('href', encodedUri)
actions.setAttribute('download', fileName)
actions.click()
}
const handleExportJson = () => {
exportData(props.editorJson)
}
const { clipboardRef, copiedRef } = useCopyToClipboard()
const { createMessage } = useMessage()
const handleCopyJson = () => {
//
const value = props.editorJson
if (!value) {
createMessage.warning('代码为空!')
return
}
clipboardRef.value = value
if (unref(copiedRef)) {
createMessage.warning('复制成功!')
}
}
return {
...toRefs(state),
exportData,
handleCopyJson,
handleExportJson,
MODE
}
}
})
</script>
<style lang="less" scoped>
// modal
.copy-btn-box {
padding-top: 8px;
text-align: center;
.copy-btn {
margin-right: 8px;
}
}
</style>

277
src/components/FormDesign/src/components/VFormDesign/components/RuleProps.vue

@ -0,0 +1,277 @@
<!--
* @Description: 正则校验选项组件
-->
<template>
<div class="rule-props-content">
<Form v-if="formConfig.currentItem && formConfig.currentItem['rules']">
<div v-for="(item, index) of formConfig.currentItem['rules']" :key="index" class="rule-props-item">
<Icon icon="ant-design:close-circle-filled" class="rule-props-item-close" @click="removeRule(index)" />
<FormItem label="正则" :labelCol="{ span: 6 }" :wrapperCol="{ span: 16 }">
<AutoComplete v-model:value="item.pattern" placeholder="请输入正则表达式" :dataSource="patternDataSource" />
</FormItem>
<FormItem label="文案" :labelCol="{ span: 6 }" :wrapperCol="{ span: 16 }">
<Input v-model:value="item.message" placeholder="请输入提示文案" />
</FormItem>
</div>
</Form>
<a @click="addRules">
<Icon icon="ant-design:file-add-outlined" />
添加正则
</a>
</div>
</template>
<script lang="ts">
import { ref, defineComponent } from 'vue'
import { remove } from '../../../utils'
import { useFormDesignState } from '../../../hooks/useFormDesignState'
import { isArray } from 'lodash-es'
import { Form, FormItem, AutoComplete, Input } from 'ant-design-vue'
import Icon from '@/components/Icon'
export default defineComponent({
name: 'RuleProps',
components: {
Form,
FormItem,
AutoComplete,
Input,
Icon
},
setup() {
//
const { formConfig } = useFormDesignState()
// currentItem
/**
* 添加正则校验判断当前组件的rules是不是数组如果不是数组使用set方法重置成数组然后添加正则校验
*/
const addRules = () => {
if (!isArray(formConfig.value.currentItem!.rules)) formConfig.value.currentItem!['rules'] = []
formConfig.value.currentItem!.rules?.push({ pattern: '', message: '' })
}
/**
* 删除正则校验当正则规则为0时删除rules属性
* @param index {number} 需要删除的规则下标
*/
const removeRule = (index: number) => {
remove(formConfig.value.currentItem!.rules as Array<any>, index)
if (formConfig.value.currentItem!.rules?.length === 0) delete formConfig.value.currentItem!['rules']
}
const patternDataSource = ref([
{
value: '/^(?:(?:\\+|00)86)?1[3-9]\\d{9}$/',
text: '手机号码'
},
{
value: '/^((ht|f)tps?:\\/\\/)?[\\w-]+(\\.[\\w-]+)+:\\d{1,5}\\/?$/',
text: '网址带端口号'
},
{
value: '/^(((ht|f)tps?):\\/\\/)?[\\w-]+(\\.[\\w-]+)+([\\w.,@?^=%&:/~+#-\\(\\)]*[\\w@?^=%&/~+#-\\(\\)])?$/',
text: '网址带参数'
},
{
value: '/^[0-9A-HJ-NPQRTUWXY]{2}\\d{6}[0-9A-HJ-NPQRTUWXY]{10}$/',
text: '统一社会信用代码'
},
{
value: '/^(s[hz]|S[HZ])(000[\\d]{3}|002[\\d]{3}|300[\\d]{3}|600[\\d]{3}|60[\\d]{4})$/',
text: '股票代码'
},
{
value: '/^([a-f\\d]{32}|[A-F\\d]{32})$/',
text: 'md5格式(32位)'
},
{
value: '/^[a-f\\d]{4}(?:[a-f\\d]{4}-){4}[a-f\\d]{12}$/i',
text: 'GUID/UUID'
},
{
value: '/^\\d+(?:\\.\\d+){2}$/',
text: '版本号(x.y.z)格式'
},
{
value: '/^https?:\\/\\/(.+\\/)+.+(\\.(swf|avi|flv|mpg|rm|mov|wav|asf|3gp|mkv|rmvb|mp4))$/i',
text: '视频链接地址'
},
{
value: '/^https?:\\/\\/(.+\\/)+.+(\\.(gif|png|jpg|jpeg|webp|svg|psd|bmp|tif))$/i',
text: '图片链接地址'
},
{
value: '/^-?\\d+(,\\d{3})*(\\.\\d{1,2})?$/',
text: '数字/货币金额(支持负数、千分位分隔符)'
},
{
value: '/(?:^[1-9]([0-9]+)?(?:\\.[0-9]{1,2})?$)|(?:^(?:0)$)|(?:^[0-9]\\.[0-9](?:[0-9])?$)/',
text: '数字/货币金额'
},
{
value: '/^[1-9]\\d{9,29}$/',
text: '银行卡号'
},
{
value: '/^(?:[\u4e00-\u9fa5·]{2,16})$/',
text: '中文姓名'
},
{
value: '/(^[a-zA-Z][a-zA-Z\\s]{0,20}[a-zA-Z]$)/',
text: '英文姓名'
},
{
value:
// eslint-disable-next-line max-len
'/^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-HJ-NP-Z](?:((\\d{5}[A-HJK])|([A-HJK][A-HJ-NP-Z0-9][0-9]{4}))|[A-HJ-NP-Z0-9]{4}[A-HJ-NP-Z0-9挂学警港澳])$/',
text: '车牌号(新能源)'
},
{
value:
'/^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-HJ-NP-Z][A-HJ-NP-Z0-9]{4}[A-HJ-NP-Z0-9挂学警港澳]$/',
text: '车牌号(非新能源)'
},
{
value:
'/^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-HJ-NP-Z][A-HJ-NP-Z0-9]{4,5}[A-HJ-NP-Z0-9挂学警港澳]$/',
text: '车牌号(新能源+非新能源)'
},
{
value:
// eslint-disable-next-line max-len
'/^(([^<>()[\\]\\\\.,;:\\s@"]+(\\.[^<>()[\\]\\\\.,;:\\s@"]+)*)|(".+"))@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\])|(([a-zA-Z\\-0-9]+\\.)+[a-zA-Z]{2,}))$/',
text: 'email(邮箱)'
},
{
value: '/^(?:(?:\\d{3}-)?\\d{8}|^(?:\\d{4}-)?\\d{7,8})(?:-\\d+)?$/',
text: '座机'
},
{
value: '/^[1-9]\\d{5}(?:18|19|20)\\d{2}(?:0[1-9]|10|11|12)(?:0[1-9]|[1-2]\\d|30|31)\\d{3}[\\dXx]$/',
text: '身份证号'
},
{
value: '/(^[EeKkGgDdSsPpHh]\\d{8}$)|(^(([Ee][a-fA-F])|([DdSsPp][Ee])|([Kk][Jj])|([Mm][Aa])|(1[45]))\\d{7}$)/',
text: '护照'
},
{
value:
// eslint-disable-next-line max-len
'/^(?:[\u3400-\u4DB5\u4E00-\u9FEA\uFA0E\uFA0F\uFA11\uFA13\uFA14\uFA1F\uFA21\uFA23\uFA24\uFA27-\uFA29]|[\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879][\uDC00-\uDFFF]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0])+$/',
text: '中文汉字'
},
{
value: '/^\\d+\\.\\d+$/',
text: '小数'
},
{
value: '/^\\d{1,}$/',
text: '数字'
},
{
value: '/^[1-9][0-9]{4,10}$/',
text: 'qq号'
},
{
value: '/^[A-Za-z0-9]+$/',
text: '数字字母组合'
},
{
value: '/^[a-zA-Z]+$/',
text: '英文字母'
},
{
value: '/^[a-z]+$/',
text: '小写英文字母'
},
{
value: '/^[A-Z]+$/',
text: '大写英文字母'
},
{
value: '/^[a-zA-Z0-9_-]{4,16}$/',
text: '用户名校验,4到16位(字母,数字,下划线,减号)'
},
{
value: '/^#?([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$/',
text: '16进制颜色'
},
{
value: '/^[a-zA-Z][-_a-zA-Z0-9]{5,19}$/',
text: '微信号'
},
{
value: '/^(0[1-7]|1[0-356]|2[0-7]|3[0-6]|4[0-7]|5[1-7]|6[1-7]|7[0-5]|8[013-6])\\d{4}$/',
text: '邮政编码(中国)'
},
{
value: '/^[^A-Za-z]*$/',
text: '不能包含字母'
},
{
value: '/^\\+?[1-9]\\d*$/',
text: '正整数,不包含0'
},
{
value: '/^-[1-9]\\d*$/',
text: '负整数,不包含0'
},
{
value: '/^-?[0-9]\\d*$/',
text: '整数'
},
{
value: '/^(-?\\d+)(\\.\\d+)?$/',
text: '浮点数'
},
{
value: '/^[A-Za-z0-9\u4e00-\u9fa5]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$/',
text: 'email(支持中文邮箱)'
}
])
return { addRules, removeRule, formConfig, patternDataSource }
}
})
</script>
<style lang="less" scoped>
:deep(.icon) {
width: 1em;
height: 1em;
vertical-align: -0.15em;
fill: currentcolor;
overflow: hidden;
}
.rule-props-content {
:deep(.ant-form-item) {
margin-bottom: 0;
}
.rule-props-item {
position: relative;
background-color: #f0eded;
padding: 3px 2px;
border-radius: 5px;
margin-bottom: 5px;
:deep(.ant-form-item) {
border: 0 !important;
}
&-close {
position: absolute;
top: -5px;
right: -5px;
color: #ccc;
cursor: pointer;
border-radius: 7px;
background-color: #a3a0a0;
z-index: 999;
&:hover {
color: #00c;
}
}
}
}
</style>

1148
src/components/FormDesign/src/components/VFormDesign/config/componentPropsConfig.ts

File diff suppressed because it is too large Load Diff

349
src/components/FormDesign/src/components/VFormDesign/config/formItemPropsConfig.ts

@ -0,0 +1,349 @@
import { IAnyObject } from '../../../typings/base-type'
import { baseComponents, customComponents } from '../../../core/formItemConfig'
export const globalConfigState: { span: number } = {
span: 24
}
export interface IBaseFormAttrs {
name: string // 字段名
label: string // 字段标签
component?: string // 属性控件
componentProps?: IAnyObject // 传递给控件的属性
exclude?: string[] // 需要排除的控件
includes?: string[] // 符合条件的组件
on?: IAnyObject
children?: IBaseFormAttrs[]
category?: 'control' | 'input'
}
export interface IBaseFormItemControlAttrs extends IBaseFormAttrs {
target?: 'props' | 'options' // 绑定到对象下的某个目标key中
}
export const baseItemColumnProps: IBaseFormAttrs[] = [
{
name: 'span',
label: '栅格数',
component: 'Slider',
on: {
change(value: number) {
globalConfigState.span = value
}
},
componentProps: {
max: 24,
min: 0,
marks: { 12: '' }
}
},
{
name: 'offset',
label: '栅格左侧的间隔格数',
component: 'Slider',
componentProps: {
max: 24,
min: 0,
marks: { 12: '' }
}
},
{
name: 'order',
label: '栅格顺序,flex 布局模式下有效',
component: 'Slider',
componentProps: {
max: 24,
min: 0,
marks: { 12: '' }
}
},
{
name: 'pull',
label: '栅格向左移动格数',
component: 'Slider',
componentProps: {
max: 24,
min: 0,
marks: { 12: '' }
}
},
{
name: 'push',
label: '栅格向右移动格数',
component: 'Slider',
componentProps: {
max: 24,
min: 0,
marks: { 12: '' }
}
},
{
name: 'xs',
label: '<576px 响应式栅格',
component: 'Slider',
componentProps: {
max: 24,
min: 0,
marks: { 12: '' }
}
},
{
name: 'sm',
label: '≥576px 响应式栅格',
component: 'Slider',
componentProps: {
max: 24,
min: 0,
marks: { 12: '' }
}
},
{
name: 'md',
label: '≥768p 响应式栅格',
component: 'Slider',
componentProps: {
max: 24,
min: 0,
marks: { 12: '' }
}
},
{
name: 'lg',
label: '≥992px 响应式栅格',
component: 'Slider',
componentProps: {
max: 24,
min: 0,
marks: { 12: '' }
}
},
{
name: 'xl',
label: '≥1200px 响应式栅格',
component: 'Slider',
componentProps: {
max: 24,
min: 0,
marks: { 12: '' }
}
},
{
name: 'xxl',
label: '≥1600px 响应式栅格',
component: 'Slider',
componentProps: {
max: 24,
min: 0,
marks: { 12: '' }
}
},
{
name: '≥2000px',
label: '≥1600px 响应式栅格',
component: 'Slider',
componentProps: {
max: 24,
min: 0,
marks: { 12: '' }
}
}
]
// 控件属性面板的配置项
export const advanceFormItemColProps: IBaseFormAttrs[] = [
{
name: 'labelCol',
label: '标签col',
component: 'Slider',
componentProps: {
max: 24,
min: 0,
marks: { 12: '' }
},
exclude: ['Grid']
},
{
name: 'wrapperCol',
label: '控件-span',
component: 'Slider',
componentProps: {
max: 24,
min: 0,
marks: { 12: '' }
},
exclude: ['Grid']
}
]
// 控件属性面板的配置项
export const baseFormItemProps: IBaseFormAttrs[] = [
{
// 动态的切换控件的类型
name: 'component',
label: '控件-FormItem',
component: 'Select',
componentProps: {
options: baseComponents.concat(customComponents).map((item) => ({ value: item.component, label: item.label }))
}
},
{
name: 'label',
label: '标签',
component: 'Input',
componentProps: {
type: 'Input',
placeholder: '请输入标签'
},
exclude: ['Grid']
},
{
name: 'field',
label: '字段标识',
component: 'Input',
componentProps: {
type: 'InputTextArea',
placeholder: '请输入字段标识'
},
exclude: ['Grid']
},
{
name: 'helpMessage',
label: 'helpMessage',
component: 'Input',
componentProps: {
placeholder: '请输入提示信息'
},
exclude: ['Grid']
}
]
// 控件属性面板的配置项
export const advanceFormItemProps: IBaseFormAttrs[] = [
{
name: 'labelAlign',
label: '标签对齐',
component: 'RadioGroup',
componentProps: {
options: [
{
label: '靠左',
value: 'left'
},
{
label: '靠右',
value: 'right'
}
]
},
exclude: ['Grid']
},
{
name: 'help',
label: 'help',
component: 'Input',
componentProps: {
placeholder: '请输入提示信息'
},
exclude: ['Grid']
},
{
name: 'extra',
label: '额外消息',
component: 'Input',
componentProps: {
type: 'InputTextArea',
placeholder: '请输入额外消息'
},
exclude: ['Grid']
},
{
name: 'validateTrigger',
label: 'validateTrigger',
component: 'Input',
componentProps: {
type: 'InputTextArea',
placeholder: '请输入validateTrigger'
},
exclude: ['Grid']
},
{
name: 'validateStatus',
label: '校验状态',
component: 'RadioGroup',
componentProps: {
options: [
{
label: '默认',
value: ''
},
{
label: '成功',
value: 'success'
},
{
label: '警告',
value: 'warning'
},
{
label: '错误',
value: 'error'
},
{
label: '校验中',
value: 'validating'
}
]
},
exclude: ['Grid']
}
]
export const baseFormItemControlAttrs: IBaseFormItemControlAttrs[] = [
{
name: 'required',
label: '必填项',
component: 'Checkbox',
exclude: ['alert']
},
{
name: 'hidden',
label: '隐藏',
component: 'Checkbox',
exclude: ['alert']
},
{
name: 'hiddenLabel',
component: 'Checkbox',
exclude: ['Grid'],
label: '隐藏标签'
},
{
name: 'colon',
label: 'label后面显示冒号',
component: 'Checkbox',
componentProps: {},
exclude: ['Grid']
},
{
name: 'hasFeedback',
label: '输入反馈',
component: 'Checkbox',
componentProps: {},
includes: ['Input']
},
{
name: 'autoLink',
label: '自动关联',
component: 'Checkbox',
componentProps: {},
includes: ['Input']
},
{
name: 'validateFirst',
label: '检验证错误停止',
component: 'Checkbox',
componentProps: {},
includes: ['Input']
}
]

335
src/components/FormDesign/src/components/VFormDesign/index.vue

@ -0,0 +1,335 @@
<template>
<Layout>
<LayoutSider
:class="`left ${prefixCls}-sider`"
collapsible
collapsedWidth="0"
width="270"
:zeroWidthTriggerStyle="{
'margin-top': '-70px',
'background-color': 'gray'
}"
breakpoint="md"
>
<CollapseContainer title="基础控件">
<CollapseItem
:list="baseComponents"
:handleListPush="handleListPushDrag"
@add-attrs="handleAddAttrs"
@handle-list-push="handleListPush"
/>
</CollapseContainer>
<CollapseContainer title="自定义控件">
<CollapseItem
:list="customComponents"
@add-attrs="handleAddAttrs"
:handleListPush="handleListPushDrag"
@handle-list-push="handleListPush"
/>
</CollapseContainer>
<CollapseContainer title="布局控件">
<CollapseItem
:list="layoutComponents"
:handleListPush="handleListPushDrag"
@add-attrs="handleAddAttrs"
@handle-list-push="handleListPush"
/>
</CollapseContainer>
</LayoutSider>
<LayoutContent>
<Toolbar
@handle-open-json-modal="handleOpenModal(jsonModal!)"
@handle-open-import-json-modal="handleOpenModal(importJsonModal!)"
@handle-preview="handleOpenModal(eFormPreview!)"
@handle-preview2="handleOpenModal(eFormPreview2!)"
@handle-open-code-modal="handleOpenModal(codeModal!)"
@handle-clear-form-items="handleClearFormItems"
/>
<FormComponentPanel :current-item="formConfig.currentItem" :data="formConfig" @handle-set-select-item="handleSetSelectItem" />
</LayoutContent>
<LayoutSider
:class="`right ${prefixCls}-sider`"
collapsible
:reverseArrow="true"
collapsedWidth="0"
width="270"
:zeroWidthTriggerStyle="{ 'margin-top': '-70px', 'background-color': 'gray' }"
breakpoint="lg"
>
<PropsPanel ref="propsPanel" :activeKey="formConfig.activeKey">
<template v-for="item of formConfig.schemas" #[`${item.component}Props`]="data">
<slot :name="`${item.component}Props`" v-bind="{ formItem: data, props: data.componentProps }"></slot>
</template>
</PropsPanel>
</LayoutSider>
</Layout>
<JsonModal ref="jsonModal" />
<CodeModal ref="codeModal" />
<ImportJsonModal ref="importJsonModal" />
<VFormPreview ref="eFormPreview" :formConfig="formConfig" />
<VFormPreview2 ref="eFormPreview2" :formConfig="formConfig" />
</template>
<script lang="ts" setup>
import CollapseItem from './modules/CollapseItem.vue'
import FormComponentPanel from './modules/FormComponentPanel.vue'
import JsonModal from './components/JsonModal.vue'
import VFormPreview from '../VFormPreview/index.vue'
import VFormPreview2 from '../VFormPreview/useForm.vue'
import Toolbar from './modules/Toolbar.vue'
import PropsPanel from './modules/PropsPanel.vue'
import ImportJsonModal from './components/ImportJsonModal.vue'
import CodeModal from './components/CodeModal.vue'
import 'codemirror/mode/javascript/javascript'
import { ref, provide, Ref } from 'vue'
import { Layout, LayoutContent, LayoutSider } from 'ant-design-vue'
import { IVFormComponent, IFormConfig, PropsTabKey } from '../../typings/v-form-component'
import { formItemsForEach, generateKey } from '../../utils'
import { cloneDeep } from 'lodash-es'
import { baseComponents, customComponents, layoutComponents } from '../../core/formItemConfig'
import { useRefHistory, UseRefHistoryReturn } from '@vueuse/core'
import { globalConfigState } from './config/formItemPropsConfig'
import { IFormDesignMethods, IPropsPanel, IToolbarMethods } from '../../typings/form-type'
import { useDesign } from '@/hooks/web/useDesign'
import { CollapseContainer } from '@/components/Container/index'
defineProps({
title: {
type: String,
default: 'v-form-antd表单设计器'
}
})
const { prefixCls } = useDesign('form-design')
//
const propsPanel = ref<null | IPropsPanel>(null)
const jsonModal = ref<null | IToolbarMethods>(null)
const importJsonModal = ref<null | IToolbarMethods>(null)
const eFormPreview = ref<null | IToolbarMethods>(null)
const eFormPreview2 = ref<null | IToolbarMethods>(null)
const codeModal = ref<null | IToolbarMethods>(null)
const formModel = ref({})
// endregion
const formConfig = ref<IFormConfig>({
//
schemas: [],
layout: 'horizontal',
labelLayout: 'flex',
labelWidth: 100,
labelCol: {},
wrapperCol: {},
currentItem: {
component: '',
componentProps: {}
},
activeKey: 1
})
const setFormConfig = (config: IFormConfig) => {
//
config.schemas = config.schemas || []
config.schemas.forEach((item) => {
item.colProps = item.colProps || { span: 24 }
item.componentProps = item.componentProps || {}
item.itemProps = item.itemProps || {}
})
formConfig.value = config
}
//
const historyReturn = useRefHistory(formConfig, {
deep: true,
capacity: 20,
parse: (val: IFormConfig) => {
// 使lodash.cloneDeepcurrentItem
const formConfig = cloneDeep(val)
const { currentItem, schemas } = formConfig
// formItems
const item = schemas && schemas.find((item) => item.key === currentItem?.key)
//
if (item) {
formConfig.currentItem = item
}
return formConfig
}
})
/**
* 选中表单项
* @param schema 当前选中的表单项
*/
const handleSetSelectItem = (schema: IVFormComponent) => {
formConfig.value.currentItem = schema
handleChangePropsTabs(schema.key ? (formConfig.value.activeKey! === 1 ? 2 : formConfig.value.activeKey!) : 1)
}
const setGlobalConfigState = (formItem: IVFormComponent) => {
formItem.colProps = formItem.colProps || {}
formItem.colProps.span = globalConfigState.span
// console.log('setGlobalConfigState', formItem);
}
/**
* 添加属性
* @param schemas
* @param index
*/
const handleAddAttrs = (_formItems: IVFormComponent[], _index: number) => {}
const handleListPushDrag = (item: IVFormComponent) => {
const formItem = cloneDeep(item)
setGlobalConfigState(formItem)
generateKey(formItem)
return formItem
}
/**
* 单击控件时添加到面板中
* @param item {IVFormComponent} 当前点击的组件
*/
const handleListPush = (item: IVFormComponent) => {
// console.log('handleListPush', item);
const formItem = cloneDeep(item)
setGlobalConfigState(formItem)
generateKey(formItem)
if (!formConfig.value.currentItem?.key) {
handleSetSelectItem(formItem)
formConfig.value.schemas && formConfig.value.schemas.push(formItem)
return
}
handleCopy(formItem, false)
}
/**
* 复制表单项如果表单项为栅格布局则遍历所有自表单项重新生成key
* @param {IVFormComponent} formItem
* @return {IVFormComponent}
*/
const copyFormItem = (formItem: IVFormComponent) => {
const newFormItem = cloneDeep(formItem)
if (newFormItem.component === 'Grid') {
formItemsForEach([formItem], (item) => {
generateKey(item)
})
}
return newFormItem
}
/**
* 复制或者添加表单isCopy为true时则复制表单
* @param item {IVFormComponent} 当前点击的组件
* @param isCopy {boolean} 是否复制
*/
const handleCopy = (item: IVFormComponent = formConfig.value.currentItem as IVFormComponent, isCopy = true) => {
const key = formConfig.value.currentItem?.key
/**
* 遍历当表单项配置如果是复制则复制一份表单项如果不是复制则直接添加到表单项中
* @param schemas
*/
const traverse = (schemas: IVFormComponent[]) => {
// 使some
schemas.some((formItem: IVFormComponent, index: number) => {
if (formItem.key === key) {
//
isCopy ? schemas.splice(index, 0, copyFormItem(formItem)) : schemas.splice(index + 1, 0, item)
const event = {
newIndex: index + 1
}
//
handleBeforeColAdd(event, schemas, isCopy)
return true
}
if (['Grid', 'Tabs'].includes(formItem.component)) {
//
formItem.columns?.forEach((item) => {
traverse(item.children)
})
}
})
}
if (formConfig.value.schemas) {
traverse(formConfig.value.schemas)
}
}
/**
* 添加到表单中
* @param newIndex {object} 事件对象
* @param schemas {IVFormComponent[]} 表单项列表
* @param isCopy {boolean} 是否复制
*/
const handleBeforeColAdd = ({ newIndex }: any, schemas: IVFormComponent[], isCopy = false) => {
const item = schemas[newIndex]
isCopy && generateKey(item)
handleSetSelectItem(item)
}
/**
* 打开模态框
* @param Modal {IToolbarMethods}
*/
const handleOpenModal = (Modal: IToolbarMethods) => {
const config = cloneDeep(formConfig.value)
Modal?.showModal(config)
}
/**
* 切换属性面板
* @param key
*/
const handleChangePropsTabs = (key: PropsTabKey) => {
formConfig.value.activeKey = key
}
/**
* 清空表单项列表
*/
const handleClearFormItems = () => {
formConfig.value.schemas = []
handleSetSelectItem({ component: '' })
}
const setFormModel = (key, value) => (formModel.value[key] = value)
provide('formModel', formModel)
// inject
provide<(key: String, value: any) => void>('setFormModelMethod', setFormModel)
// region
// provide('currentItem', formConfig.value.currentItem)
// inject
provide<Ref<IFormConfig>>('formConfig', formConfig)
//
provide<UseRefHistoryReturn<any, any>>('historyReturn', historyReturn)
// inject
provide<IFormDesignMethods>('formDesignMethods', {
handleBeforeColAdd,
handleCopy,
handleListPush,
handleSetSelectItem,
handleAddAttrs,
setFormConfig
})
// endregion
</script>
<style lang="less" scoped>
@prefix-cls: ~'@{namespace}-form-design';
[data-theme='dark'] {
.@{prefix-cls}-sider{
background-color: #1f1f1f;
}}
[data-theme='light'] {
.@{prefix-cls}-sider{
background-color: #fff;
}
}
</style>

102
src/components/FormDesign/src/components/VFormDesign/modules/CollapseItem.vue

@ -0,0 +1,102 @@
<template>
<div>
<draggable
tag="ul"
:model-value="list"
v-bind="{
group: { name: 'form-draggable', pull: 'clone', put: false },
sort: false,
clone: cloneItem,
animation: 180,
ghostClass: 'moving'
}"
item-key="type"
@start="handleStart($event, list)"
@add="handleAdd"
>
<template #item="{ element, index }">
<li class="bs-box text-ellipsis" @dragstart="$emit('add-attrs', list, index)" @click="$emit('handle-list-push', element)">
<!-- <svg v-if="element.icon.indexOf('icon-') > -1" class="icon" aria-hidden="true">
<use :xlink:href="`#${element.icon}`" />
</svg> -->
<Icon :icon="element.icon" />
{{ element.label }}</li
></template
>
</draggable>
</div>
</template>
<script lang="ts">
import { defineComponent, reactive } from 'vue'
import { IVFormComponent } from '../../../typings/v-form-component'
import draggable from 'vuedraggable'
// import { toRefs } from '@vueuse/core';
import { Icon } from '@/components/Icon'
export default defineComponent({
name: 'CollapseItem',
components: { draggable, Icon },
props: {
list: {
type: [Array] as PropType<IVFormComponent[]>,
default: () => []
},
handleListPush: {
type: Function as PropType<(item: IVFormComponent) => void>,
default: null
}
},
setup(props, { emit }) {
const state = reactive({})
const handleStart = (e: any, list1: IVFormComponent[]) => {
emit('start', list1[e.oldIndex].component)
}
const handleAdd = (e: any) => {
console.log(e)
}
// https://github.com/SortableJS/vue.draggable.next
// https://github.com/SortableJS/vue.draggable.next/blob/master/example/components/custom-clone.vue
const cloneItem = (one) => {
return props.handleListPush(one)
}
return { state, handleStart, handleAdd, cloneItem }
}
})
</script>
<style lang="less" scoped>
@import url('../styles/variable.less');
ul {
padding: 5px;
list-style: none;
display: flex;
margin-bottom: 0;
flex-wrap: wrap;
// background: #efefef;
li {
padding: 8px 12px;
transition: all 0.3s;
width: calc(50% - 6px);
margin: 2.7px;
height: 36px;
line-height: 20px;
cursor: move;
border: 1px solid @border-color;
border-radius: 3px;
&:hover {
color: @primary-color;
border: 1px solid @primary-color;
position: relative;
// z-index: 1;
box-shadow: 0 2px 6px @primary-color;
}
}
}
svg {
display: inline !important;
}
</style>

155
src/components/FormDesign/src/components/VFormDesign/modules/FormComponentPanel.vue

@ -0,0 +1,155 @@
<!--
* @Description: 中间表单布局面板
* https://github.com/SortableJS/vue.draggable.next/issues/138
-->
<template>
<div class="form-panel v-form-container">
<Empty class="empty-text" v-show="formConfig.schemas.length === 0" description="从左侧选择控件添加" />
<Form v-bind="formConfig">
<div class="draggable-box">
<draggable
class="list-main ant-row"
group="form-draggable"
:component-data="{ name: 'list', tag: 'div', type: 'transition-group' }"
ghostClass="moving"
:animation="180"
handle=".drag-move"
v-model="formConfig.schemas"
item-key="key"
@add="addItem"
@start="handleDragStart"
>
<template #item="{ element }">
<LayoutItem class="drag-move" :schema="element" :data="formConfig" :current-item="formConfig.currentItem || {}" />
</template>
</draggable>
</div>
</Form>
</div>
</template>
<script lang="ts">
import draggable from 'vuedraggable'
import { defineComponent, computed } from 'vue'
import LayoutItem from '../components/LayoutItem.vue'
import { cloneDeep } from 'lodash-es'
import { useFormDesignState } from '../../../hooks/useFormDesignState'
import { Form, Empty } from 'ant-design-vue'
export default defineComponent({
name: 'FormComponentPanel',
components: {
LayoutItem,
draggable,
Form,
Empty
},
emits: ['handleSetSelectItem'],
setup(_, { emit }) {
const { formConfig } = useFormDesignState() as Recordable
/**
* 拖拽完成事件
* @param newIndex
*/
const addItem = ({ newIndex }: any) => {
formConfig.value.schemas = formConfig.value.schemas || []
const schemas = formConfig.value.schemas
schemas[newIndex] = cloneDeep(schemas[newIndex])
emit('handleSetSelectItem', schemas[newIndex])
}
/**
* 拖拽开始事件
* @param e {Object} 事件对象
*/
const handleDragStart = (e: any) => {
emit('handleSetSelectItem', formConfig.value.schemas[e.oldIndex])
}
// currentItem
// AColdiv
const layoutTag = computed(() => {
return formConfig.value.layout === 'horizontal' ? 'Col' : 'div'
})
return {
addItem,
handleDragStart,
formConfig,
layoutTag
}
}
})
</script>
<style lang="less" scoped>
@import url('../styles/variable.less');
@import url('../styles/drag.less');
.v-form-container {
//
.ant-form-inline {
.list-main {
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
align-content: flex-start;
.layout-width {
width: 100%;
}
}
.ant-form-item-control-wrapper {
min-width: 175px !important;
}
}
}
.form-panel {
position: relative;
height: 100%;
.empty-text {
color: #aaa;
height: 150px;
inset: -10% 0 0;
margin: auto;
position: absolute;
z-index: 100;
}
.draggable-box {
// width: 100%;
.drag-move {
cursor: move;
min-height: 62px;
}
.list-main {
overflow: auto;
height: 100%;
//
.list-enter-active {
transition: all 0.5s;
}
.list-leave-active {
transition: all 0.3s;
}
.list-enter,
.list-leave-to {
opacity: 0;
transform: translateX(-100px);
}
.list-enter {
height: 30px;
}
}
}
}
</style>

96
src/components/FormDesign/src/components/VFormDesign/modules/PropsPanel.vue

@ -0,0 +1,96 @@
<!--
* @Description: 右侧属性配置面板
-->
<template>
<div>
<Tabs v-model:activeKey="formConfig.activeKey" :tabBarStyle="{ margin: 0 }">
<TabPane :key="1" tab="表单">
<FormProps />
</TabPane>
<TabPane :key="2" tab="控件">
<FormItemProps />
</TabPane>
<TabPane :key="3" tab="栅格">
<ComponentColumnProps />
</TabPane>
<TabPane :key="4" tab="组件">
<slot v-if="slotProps" :name="slotProps.component + 'Props'"></slot>
<ComponentProps v-else />
</TabPane>
</Tabs>
</div>
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue'
import FormProps from '../components/FormProps.vue'
import FormItemProps from '../components/FormItemProps.vue'
import ComponentProps from '../components/ComponentProps.vue'
import ComponentColumnProps from '../components/FormItemColumnProps.vue'
import { useFormDesignState } from '../../../hooks/useFormDesignState'
import { customComponents } from '../../../core/formItemConfig'
import { TabPane, Tabs } from 'ant-design-vue'
type ChangeTabKey = 1 | 2
export interface IPropsPanel {
changeTab: (key: ChangeTabKey) => void
}
export default defineComponent({
name: 'PropsPanel',
components: {
FormProps,
FormItemProps,
ComponentProps,
ComponentColumnProps,
Tabs,
TabPane
},
setup() {
const { formConfig } = useFormDesignState()
const slotProps = computed(() => {
return customComponents.find((item) => item.component === formConfig.value.currentItem?.component)
})
return { formConfig, customComponents, slotProps }
}
})
</script>
<style lang="less" scoped>
@import url('../styles/variable.less');
:deep(.ant-tabs) {
box-sizing: border-box;
form {
width: 100%;
position: absolute;
height: calc(100% - 50px);
margin-right: 10px;
overflow-y: auto;
overflow-x: hidden;
}
.hint-box {
margin-top: 200px;
}
.ant-form-item,
.ant-slider-with-marks {
margin-left: 10px;
margin-right: 20px;
margin-bottom: 0;
}
.ant-form-item {
// width: 100%;
margin-bottom: 0;
.ant-form-item-label {
line-height: 2;
vertical-align: text-top;
}
}
.ant-input-number {
width: 100%;
}
}
</style>

136
src/components/FormDesign/src/components/VFormDesign/modules/Toolbar.vue

@ -0,0 +1,136 @@
<!--
* @Description: 工具栏
-->
<template>
<div class="operating-area">
<!-- 头部操作按钮区域 start -->
<!-- 操作左侧区域 start -->
<div class="left-btn-box">
<Tooltip v-for="item in toolbarsConfigs" :title="item.title" :key="item.icon">
<a @click="$emit(item.event)" class="toolbar-text">
<Icon :icon="item.icon" />
</a>
</Tooltip>
<Divider type="vertical" />
<Tooltip title="撤销">
<a :class="{ disabled: !canUndo }" :disabled="!canUndo" @click="undo">
<Icon icon="ant-design:undo-outlined" />
</a>
</Tooltip>
<Tooltip title="重做">
<a :class="{ disabled: !canRedo }" :disabled="!canRedo" @click="redo">
<Icon icon="ant-design:redo-outlined" />
</a>
</Tooltip>
</div>
</div>
<!-- 操作区域 start -->
</template>
<script lang="ts">
import { defineComponent, inject, reactive, toRefs } from 'vue'
import { UseRefHistoryReturn } from '@vueuse/core'
import { IFormConfig } from '../../../typings/v-form-component'
import { Tooltip, Divider } from 'ant-design-vue'
import Icon from '@/components/Icon/index'
interface IToolbarsConfig {
type: string
title: string
icon: string
event: string
}
export default defineComponent({
name: 'OperatingArea',
components: {
Tooltip,
Icon,
Divider
},
setup() {
const state = reactive<{
toolbarsConfigs: IToolbarsConfig[]
}>({
toolbarsConfigs: [
{
title: '预览-支持布局',
type: 'preview',
event: 'handlePreview',
icon: 'ant-design:chrome-filled'
},
{
title: '预览-不支持布局',
type: 'preview',
event: 'handlePreview2',
icon: 'ant-design:chrome-filled'
},
{
title: '导入JSON',
type: 'importJson',
event: 'handleOpenImportJsonModal',
icon: 'ant-design:import-outlined'
},
{
title: '生成JSON',
type: 'exportJson',
event: 'handleOpenJsonModal',
icon: 'ant-design:export-outlined'
},
{
title: '生成代码',
type: 'exportCode',
event: 'handleOpenCodeModal',
icon: 'ant-design:code-filled'
},
{
title: '清空',
type: 'reset',
event: 'handleClearFormItems',
icon: 'ant-design:clear-outlined'
}
]
})
const historyRef = inject('historyReturn') as UseRefHistoryReturn<IFormConfig, IFormConfig>
const { undo, redo, canUndo, canRedo } = historyRef
return { ...toRefs(state), undo, redo, canUndo, canRedo }
}
})
</script>
<style lang="less" scoped>
//noinspection CssUnknownTarget
@import url('../styles/variable.less');
.operating-area {
border-bottom: 2px solid @border-color;
font-size: 16px;
text-align: left;
height: @operating-area-height;
line-height: @operating-area-height;
padding: 0 12px;
display: flex;
justify-content: space-between;
align-content: center;
padding-left: 30px;
a {
color: #666;
margin: 0 5px;
&.disabled,
&.disabled:hover {
color: #ccc;
}
&:hover {
color: @primary-color;
}
> span {
font-size: 14px;
padding-left: 2px;
}
}
}
</style>

226
src/components/FormDesign/src/components/VFormDesign/styles/drag.less

@ -0,0 +1,226 @@
.draggable-box {
height: 100%;
overflow: auto;
/* stylelint-disable-next-line selector-pseudo-class-no-unknown */
:deep(.list-main) {
overflow: hidden;
padding: 5px;
position: relative;
.moving {
// 拖放移动中;
min-height: 35px;
box-sizing: border-box;
overflow: hidden;
padding: 0 !important;
position: relative;
&::before {
content: '';
height: 5px;
width: 100%;
background-color: @primary-color;
position: absolute;
top: 0;
right: 0;
}
}
.drag-move-box {
position: relative;
box-sizing: border-box;
padding: 8px;
overflow: hidden;
transition: all 0.3s;
min-height: 60px;
&:hover {
background-color: @primary-hover-bg-color;
}
// 选择时 start
&::before {
content: '';
height: 5px;
width: 100%;
background-color: @primary-color;
position: absolute;
top: 0;
right: -100%;
transition: all 0.3s;
}
&.active {
background-color: @primary-hover-bg-color;
outline-offset: 0;
&::before {
right: 0;
}
}
// 选择时 end
.form-item-box {
position: relative;
box-sizing: border-box;
word-wrap: break-word;
&::before {
content: '';
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
}
.ant-form-item {
// 修改ant form-item的margin为padding
margin: 0;
padding-bottom: 6px;
}
}
.show-key-box {
// 显示key
position: absolute;
bottom: 2px;
right: 5px;
font-size: 14px;
// z-index: 999;
color: @primary-color;
}
.copy,
.delete {
position: absolute;
top: 0;
width: 30px;
height: 30px;
line-height: 30px;
text-align: center;
color: #fff;
// z-index: 989;
transition: all 0.3s;
&.unactivated {
opacity: 0 !important;
pointer-events: none;
}
&.active {
opacity: 1 !important;
}
}
.copy {
border-radius: 0 0 0 8px;
right: 30px;
background-color: @primary-color;
}
.delete {
right: 0;
background-color: @primary-color;
}
}
.grid-box {
position: relative;
box-sizing: border-box;
padding: 5px;
background-color: @layout-background-color;
width: 100%;
transition: all 0.3s;
overflow: hidden;
.form-item-box {
position: relative;
box-sizing: border-box;
.ant-form-item {
// 修改ant form-item的margin为padding
margin: 0;
padding-bottom: 15px;
}
}
.grid-row {
background-color: @layout-background-color;
.grid-col {
.draggable-box {
min-height: 80px;
min-width: 50px;
border: 1px #ccc dashed;
// background: #fff;
.list-main {
min-height: 83px;
position: relative;
border: 1px #ccc dashed;
}
}
}
}
// 选择时 start
&::before {
content: '';
height: 5px;
width: 100%;
background: transparent;
position: absolute;
top: 0;
right: -100%;
transition: all 0.3s;
}
&.active {
background-color: @layout-hover-bg-color;
outline-offset: 0;
&::before {
background-color: @layout-color;
right: 0;
}
}
// 选择时 end
> .copy-delete-box {
> .copy,
> .delete {
position: absolute;
top: 0;
width: 30px;
height: 30px;
line-height: 30px;
text-align: center;
color: #fff;
// z-index: 989;
transition: all 0.3s;
&.unactivated {
opacity: 0 !important;
pointer-events: none;
}
&.active {
opacity: 1 !important;
}
}
> .copy {
border-radius: 0 0 0 8px;
right: 30px;
background-color: @layout-color;
}
> .delete {
right: 0;
background-color: @layout-color;
}
}
}
}
}

15
src/components/FormDesign/src/components/VFormDesign/styles/variable.less

@ -0,0 +1,15 @@
// 表单设计器样式
@primary-color: #13c2c2;
@layout-color: #9867f7;
@primary-background-color: fade(@primary-color, 6%);
@primary-hover-bg-color: fade(@primary-color, 20%);
@layout-background-color: fade(@layout-color, 12%);
@layout-hover-bg-color: fade(@layout-color, 24%);
@title-text-color: #fff;
@border-color: #ccc;
@left-right-width: 280px;
@header-height: 56px;
@operating-area-height: 45px;

213
src/components/FormDesign/src/components/VFormItem/index.vue

@ -0,0 +1,213 @@
<!--
* @Description:
-->
<template>
<Col v-bind="colPropsComputed">
<FormItem v-bind="{ ...formItemProps }">
<template #label v-if="!formItemProps.hiddenLabel && schema.component !== 'Divider'">
<Tooltip>
<span>{{ schema.label }}</span>
<template #title v-if="schema.helpMessage"
><span>{{ schema.helpMessage }}</span></template
>
<Icon v-if="schema.helpMessage" class="ml-5" icon="ant-design:question-circle-outlined" />
</Tooltip>
</template>
<slot v-if="schema.componentProps && schema.componentProps?.slotName" :name="schema.componentProps.slotName" v-bind="schema"></slot>
<Divider v-else-if="schema.component == 'Divider' && schema.label && !formItemProps.hiddenLabel">{{ schema.label }}</Divider>
<!-- 部分控件需要一个空div -->
<div
><component
class="v-form-item-wrapper"
:is="componentItem"
v-bind="{ ...cmpProps, ...asyncProps }"
:schema="schema"
:style="schema.width ? { width: schema.width } : {}"
@change="handleChange"
@click="handleClick(schema)"
/></div>
<span v-if="['Button'].includes(schema.component)">{{ schema.label }}</span>
</FormItem>
</Col>
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs, computed, PropType, unref } from 'vue'
import { componentMap } from '../../core/formItemConfig'
import { IVFormComponent, IFormConfig } from '../../typings/v-form-component'
import { asyncComputed } from '@vueuse/core'
import { handleAsyncOptions } from '../../utils'
import { omit } from 'lodash-es'
import { Tooltip, FormItem, Divider, Col } from 'ant-design-vue'
// import FormItem from '/@/components/Form/src/components/FormItem.vue';
import { Icon } from '@/components/Icon'
import { useFormModelState } from '../../hooks/useFormDesignState'
export default defineComponent({
name: 'VFormItem',
components: {
Tooltip,
Icon,
FormItem,
Divider,
Col
},
props: {
formData: {
type: Object,
default: () => ({})
},
schema: {
type: Object as PropType<IVFormComponent>,
required: true
},
formConfig: {
type: Object as PropType<IFormConfig>,
required: true
}
},
emits: ['update:form-data', 'change'],
setup(props, { emit }) {
const state = reactive({
componentMap
})
const { formModel: formData1, setFormModel } = useFormModelState()
const colPropsComputed = computed(() => {
const { colProps = {} } = props.schema
return colProps
})
const formItemProps = computed(() => {
const { formConfig } = unref(props)
let { field, required, rules, labelCol, wrapperCol } = unref(props.schema)
const { colon } = props.formConfig
const { itemProps } = unref(props.schema)
//<editor-fold desc="">
labelCol = labelCol
? labelCol
: formConfig.layout === 'horizontal'
? formConfig.labelLayout === 'flex'
? { style: `width:${formConfig.labelWidth}px` }
: formConfig.labelCol
: {}
wrapperCol = wrapperCol
? wrapperCol
: formConfig.layout === 'horizontal'
? formConfig.labelLayout === 'flex'
? { style: 'width:auto;flex:1' }
: formConfig.wrapperCol
: {}
const style = formConfig.layout === 'horizontal' && formConfig.labelLayout === 'flex' ? { display: 'flex' } : {}
/**
* 将字符串正则格式化成正则表达式
*/
const newConfig = Object.assign(
{},
{
name: field,
style: { ...style },
colon,
required,
rules,
labelCol,
wrapperCol
},
itemProps
)
if (!itemProps?.labelCol?.span) {
newConfig.labelCol = labelCol
}
if (!itemProps?.wrapperCol?.span) {
newConfig.wrapperCol = wrapperCol
}
if (!itemProps?.rules) {
newConfig.rules = rules
}
return newConfig
}) as Recordable
const componentItem = computed(() => componentMap.get(props.schema.component as string))
// console.log('component change:', props.schema.component, componentItem.value);
const handleClick = (schema: IVFormComponent) => {
if (schema.component === 'Button' && schema.componentProps?.handle) emit(schema.componentProps?.handle)
}
/**
* 处理异步属性异步属性会导致一些属性渲染错误如defaultValue异步加载会导致渲染不出来故而此处只处理optionstreeData同步属性在cmpProps中处理
*/
const asyncProps = asyncComputed(async () => {
let { options, treeData } = props.schema.componentProps ?? {}
if (options) options = await handleAsyncOptions(options)
if (treeData) treeData = await handleAsyncOptions(treeData)
return {
options,
treeData
}
})
/**
* 处理同步属性
*/
const cmpProps = computed(() => {
const isCheck = props.schema && ['Switch', 'Checkbox', 'Radio'].includes(props.schema.component)
let { field } = props.schema
let { disabled, ...attrs } = omit(props.schema.componentProps, ['options', 'treeData']) ?? {}
disabled = props.formConfig.disabled || disabled
return {
...attrs,
disabled,
[isCheck ? 'checked' : 'value']: formData1.value[field!]
}
})
const handleChange = function (e) {
const isCheck = ['Switch', 'Checkbox', 'Radio'].includes(props.schema.component)
const target = e ? e.target : null
const value = target ? (isCheck ? target.checked : target.value) : e
setFormModel(props.schema.field!, value)
emit('change', value)
}
return {
...toRefs(state),
componentItem,
formItemProps,
handleClick,
asyncProps,
cmpProps,
handleChange,
colPropsComputed
}
}
})
</script>
<style lang="less" scoped>
.ml-5 {
margin-left: 5px;
}
// formant-col使width:100%
:deep(.ant-col) {
width: auto;
}
.ant-form-item:not(.ant-form-item-with-help) {
margin-bottom: 20px;
}
// .w-full {
// width: 100% !important;
// }
</style>

68
src/components/FormDesign/src/components/VFormItem/vFormItem.vue

@ -0,0 +1,68 @@
<!--
* @Description:
`<FormItem`
:tableAction="tableAction"
:formActionType="formActionType"
:schema="schema2"
:formProps="getProps"
:allDefaultValues="defaultValueRef"
:formModel="formModel"
:setFormModel="setFormModel"
>
-->
<template>
<FormItem :schema="schemaNew" :formProps="getProps">
<template #[item]="data" v-for="item in Object.keys($slots)">
<slot :name="item" v-bind="data || {}"></slot>
</template>
</FormItem>
</template>
<script lang="ts">
import { computed, defineComponent, unref } from 'vue'
import { IFormConfig, IVFormComponent } from '../../typings/v-form-component'
import { FormProps, FormSchema } from '@/components/Form'
import FormItem from '/@/components/Form/src/components/FormItem.vue'
export default defineComponent({
name: 'VFormItem',
components: {
FormItem
},
props: {
formData: {
type: Object,
default: () => ({})
},
schema: {
type: Object as PropType<IVFormComponent>,
required: true
},
formConfig: {
type: Object as PropType<IFormConfig>,
required: true
}
},
setup(props) {
const schema = computed(() => {
const schema: FormSchema = {
...unref(props.schema)
} as FormSchema
return schema
})
// Get the basic configuration of the form
const getProps = computed((): FormProps => {
return { ...unref(props.formConfig) } as FormProps
})
return {
schemaNew: schema,
getProps
}
}
})
</script>
<style lang="less" scoped></style>

96
src/components/FormDesign/src/components/VFormPreview/index.vue

@ -0,0 +1,96 @@
<!--
* @Description: 渲染组件无法使用Vben的组件
-->
<template>
<Modal
title="预览(支持布局)"
:visible="visible"
@ok="handleGetData"
@cancel="handleCancel"
okText="获取数据"
cancelText="关闭"
style="top: 20px"
:destroyOnClose="true"
:width="900"
>
<VFormCreate :form-config="formConfig" v-model:fApi="fApi" v-model:formModel="formModel" @submit="onSubmit">
<template #slotName="{ formModel, field }">
<a-input v-model:value="formModel[field]" placeholder="我是插槽传递的输入框" />
</template>
</VFormCreate>
<JsonModal ref="jsonModal" />
</Modal>
</template>
<script lang="ts">
import { defineComponent, reactive, ref, toRefs } from 'vue'
import { IFormConfig } from '../../typings/v-form-component'
import { IAnyObject } from '../../typings/base-type'
import VFormCreate from '../VFormCreate/index.vue'
import { formatRules } from '../../utils'
import { IVFormMethods } from '../../hooks/useVFormMethods'
import JsonModal from '../VFormDesign/components/JsonModal.vue'
import { IToolbarMethods } from '../../typings/form-type'
import { Modal } from 'ant-design-vue'
export default defineComponent({
name: 'VFormPreview',
components: {
JsonModal,
VFormCreate,
Modal
},
setup() {
const jsonModal = ref<IToolbarMethods | null>(null)
const state = reactive<{
formModel: IAnyObject
visible: boolean
formConfig: IFormConfig
fApi: IVFormMethods
}>({
formModel: {},
formConfig: {} as IFormConfig,
visible: false,
fApi: {} as IVFormMethods
})
/**
* 显示Json数据弹框
* @param jsonData
*/
const showModal = (jsonData: IFormConfig) => {
// console.log('showModal-', jsonData);
formatRules(jsonData.schemas)
state.formConfig = jsonData
state.visible = true
}
/**
* 获取表单数据
* @return {Promise<void>}
*/
const handleCancel = () => {
state.visible = false
state.formModel = {}
}
const handleGetData = async () => {
const _data = await state.fApi.submit?.()
jsonModal.value?.showModal?.(_data)
}
const onSubmit = (_data: IAnyObject) => {
//
}
const onCancel = () => {
state.formModel = {}
}
return {
handleGetData,
handleCancel,
...toRefs(state),
showModal,
jsonModal,
onSubmit,
onCancel
}
}
})
</script>

71
src/components/FormDesign/src/components/VFormPreview/useForm.vue

@ -0,0 +1,71 @@
<!--
* @Description: 使用vbenForm的功能进行渲染
-->
<template>
<Modal
title="预览(不支持布局)"
:visible="state.visible"
@ok="handleGetData"
@cancel="handleCancel"
okText="获取数据"
cancelText="关闭"
style="top: 20px"
:destroyOnClose="true"
:width="900"
>
<BasicForm v-bind="attrs" @register="registerForm" />
<JsonModal ref="jsonModal" />
</Modal>
</template>
<script lang="ts" setup>
import { BasicForm, useForm } from '@/components/Form/index'
import { reactive, ref, computed } from 'vue'
import { IFormConfig } from '../../typings/v-form-component'
import { IAnyObject } from '../../typings/base-type'
import JsonModal from '../VFormDesign/components/JsonModal.vue'
import { IToolbarMethods } from '../../typings/form-type'
import { Modal } from 'ant-design-vue'
const jsonModal = ref<IToolbarMethods | null>(null)
const state = reactive<{
formModel: IAnyObject
visible: boolean
formConfig: IFormConfig
}>({
formModel: {},
formConfig: {} as IFormConfig,
visible: false
})
const attrs = computed(() => {
return {
...state.formConfig
} as Recordable
})
/**
* 显示Json数据弹框
* @param jsonData
*/
const showModal = (jsonData: IFormConfig) => {
state.formConfig = jsonData
state.visible = true
}
//
const [registerForm, { validate }] = useForm()
const handleCancel = () => {
state.visible = false
}
/**
* 获取表单数据
* @return {Promise<void>}
*/
const handleGetData = async () => {
let data = await validate()
jsonModal.value?.showModal?.(data)
}
defineExpose({ showModal })
</script>

69
src/components/FormDesign/src/components/index.ts

@ -0,0 +1,69 @@
import type { Component } from 'vue'
import { ComponentType } from '@/components/Form/src/types'
import { IconPicker } from '@/components/Icon/index'
/**
* Component list, register here to setting it in the form
*/
import {
Input,
Button,
Select,
Radio,
Checkbox,
AutoComplete,
Cascader,
DatePicker,
InputNumber,
Switch,
TimePicker,
// ColorPicker,
TreeSelect,
Slider,
Rate,
Divider,
Calendar,
Transfer
} from 'ant-design-vue'
//ant-desing本身的Form控件库
const componentMap = new Map<string, Component>()
componentMap.set('Radio', Radio)
componentMap.set('Button', Button)
componentMap.set('Calendar', Calendar)
componentMap.set('Input', Input)
componentMap.set('InputGroup', Input.Group)
componentMap.set('InputPassword', Input.Password)
componentMap.set('InputSearch', Input.Search)
componentMap.set('InputTextArea', Input.TextArea)
componentMap.set('InputNumber', InputNumber)
componentMap.set('AutoComplete', AutoComplete)
componentMap.set('Select', Select)
componentMap.set('TreeSelect', TreeSelect)
componentMap.set('Switch', Switch)
componentMap.set('RadioGroup', Radio.Group)
componentMap.set('Checkbox', Checkbox)
componentMap.set('CheckboxGroup', Checkbox.Group)
componentMap.set('Cascader', Cascader)
componentMap.set('Slider', Slider)
componentMap.set('Rate', Rate)
componentMap.set('Transfer', Transfer)
componentMap.set('DatePicker', DatePicker)
componentMap.set('MonthPicker', DatePicker.MonthPicker)
componentMap.set('RangePicker', DatePicker.RangePicker)
componentMap.set('WeekPicker', DatePicker.WeekPicker)
componentMap.set('TimePicker', TimePicker)
componentMap.set('IconPicker', IconPicker)
componentMap.set('Divider', Divider)
export function add(compName: ComponentType, component: Component) {
componentMap.set(compName, component)
}
export function del(compName: ComponentType) {
componentMap.delete(compName)
}
export { componentMap }

420
src/components/FormDesign/src/core/formItemConfig.ts

@ -0,0 +1,420 @@
/**
* @description
*/
import { IVFormComponent } from '../typings/v-form-component'
import { isArray } from 'lodash-es'
import { componentMap as VbenCmp, add } from '@/components/Form/src/componentMap'
import { ComponentType } from '@/components/Form/src/types'
import { componentMap as Cmp } from '../components'
import { Component } from 'vue'
const componentMap = new Map<string, Component>()
//如果有其它控件,可以在这里初始化
//注册Ant控件库
Cmp.forEach((value, key) => {
componentMap.set(key, value)
if (VbenCmp[key] == null) {
add(key as ComponentType, value)
}
})
//注册vben控件库
VbenCmp.forEach((value, key) => {
componentMap.set(key, value)
})
export { componentMap }
/**
*
* @param {IVFormComponent | IVFormComponent[]} config
*/
export function setFormDesignComponents(config: IVFormComponent | IVFormComponent[]) {
if (isArray(config)) {
config.forEach((item) => {
const { componentInstance: component, ...rest } = item
componentMap[item.component] = component
customComponents.push(Object.assign({ props: {} }, rest))
})
} else {
const { componentInstance: component, ...rest } = config
componentMap[config.component] = component
customComponents.push(Object.assign({ props: {} }, rest))
}
}
//外部设置的自定义控件
export const customComponents: IVFormComponent[] = []
// 左侧控件列表与初始化的控件属性
// props.slotName,会在formitem级别生成一个slot,并绑定当前record值
// 属性props,类型为对象,不能为undefined或是null。
export const baseComponents: IVFormComponent[] = [
{
component: 'InputCountDown',
label: '倒计时输入',
icon: 'line-md:iconify2',
colProps: { span: 24 },
field: '',
componentProps: {}
},
{
component: 'IconPicker',
label: '图标选择器',
icon: 'line-md:iconify2',
colProps: { span: 24 },
field: '',
componentProps: {}
},
{
component: 'StrengthMeter',
label: '密码强度',
icon: 'wpf:password1',
colProps: { span: 24 },
field: '',
componentProps: {}
},
{
component: 'AutoComplete',
label: '自动完成',
icon: 'wpf:password1',
colProps: { span: 24 },
field: '',
componentProps: {
placeholder: '请输入正则表达式',
options: [
{
value: '/^(?:(?:\\+|00)86)?1[3-9]\\d{9}$/',
label: '手机号码'
},
{
value: '/^((ht|f)tps?:\\/\\/)?[\\w-]+(\\.[\\w-]+)+:\\d{1,5}\\/?$/',
label: '网址带端口号'
}
]
}
},
{
component: 'Divider',
label: '分割线',
icon: 'radix-icons:divider-horizontal',
colProps: { span: 24 },
field: '',
componentProps: {
orientation: 'center',
dashed: true
}
},
{
component: 'Checkbox',
label: '复选框',
icon: 'ant-design:check-circle-outlined',
colProps: { span: 24 },
field: ''
},
{
component: 'CheckboxGroup',
label: '复选框-组',
icon: 'ant-design:check-circle-filled',
field: '',
colProps: { span: 24 },
componentProps: {
options: [
{
label: '选项1',
value: '1'
},
{
label: '选项2',
value: '2'
}
]
}
},
{
component: 'Input',
label: '输入框',
icon: 'bi:input-cursor-text',
field: '',
colProps: { span: 24 },
componentProps: {
type: 'text'
}
},
{
component: 'InputNumber',
label: '数字输入框',
icon: 'ant-design:field-number-outlined',
field: '',
colProps: { span: 24 },
componentProps: { style: 'width:200px' }
},
{
component: 'InputTextArea',
label: '文本域',
icon: 'ant-design:file-text-filled',
field: '',
colProps: { span: 24 },
componentProps: {}
},
{
component: 'Select',
label: '下拉选择',
icon: 'gg:select',
field: '',
colProps: { span: 24 },
componentProps: {
options: [
{
label: '选项1',
value: '1'
},
{
label: '选项2',
value: '2'
}
]
}
},
{
component: 'Radio',
label: '单选框',
icon: 'ant-design:check-circle-outlined',
field: '',
colProps: { span: 24 },
componentProps: {}
},
{
component: 'RadioGroup',
label: '单选框-组',
icon: 'carbon:radio-button-checked',
field: '',
colProps: { span: 24 },
componentProps: {
options: [
{
label: '选项1',
value: '1'
},
{
label: '选项2',
value: '2'
}
]
}
},
{
component: 'DatePicker',
label: '日期选择',
icon: 'healthicons:i-schedule-school-date-time-outline',
field: '',
colProps: { span: 24 },
componentProps: {}
},
{
component: 'RangePicker',
label: '日期范围',
icon: 'healthicons:i-schedule-school-date-time-outline',
field: '',
colProps: { span: 24 },
componentProps: {
placeholder: ['开始日期', '结束日期']
}
},
{
component: 'MonthPicker',
label: '月份选择',
icon: 'healthicons:i-schedule-school-date-time-outline',
field: '',
colProps: { span: 24 },
componentProps: {
placeholder: '请选择月份'
}
},
{
component: 'TimePicker',
label: '时间选择',
icon: 'healthicons:i-schedule-school-date-time',
field: '',
colProps: { span: 24 },
componentProps: {}
},
{
component: 'Slider',
label: '滑动输入条',
icon: 'vaadin:slider',
field: '',
colProps: { span: 24 },
componentProps: {}
},
{
component: 'Rate',
label: '评分',
icon: 'ic:outline-star-rate',
field: '',
colProps: { span: 24 },
componentProps: {}
},
{
component: 'Switch',
label: '开关',
icon: 'entypo:switch',
field: '',
colProps: { span: 24 },
componentProps: {}
},
{
component: 'TreeSelect',
label: '树形选择',
icon: 'clarity:tree-view-line',
field: '',
colProps: { span: 24 },
componentProps: {
treeData: [
{
label: '选项1',
value: '1',
children: [
{
label: '选项三',
value: '1-1'
}
]
},
{
label: '选项2',
value: '2'
}
]
}
},
{
component: 'Upload',
label: '上传',
icon: 'ant-design:upload-outlined',
field: '',
colProps: { span: 24 },
componentProps: {
api: () => 1
}
},
{
component: 'Cascader',
label: '级联选择',
icon: 'ant-design:check-outlined',
field: '',
colProps: { span: 24 },
componentProps: {
options: [
{
label: '选项1',
value: '1',
children: [
{
label: '选项三',
value: '1-1'
}
]
},
{
label: '选项2',
value: '2'
}
]
}
},
// {
// component: 'Button',
// label: '按钮',
// icon: 'dashicons:button',
// field: '',
// colProps: { span: 24 },
// hiddenLabel: true,
// componentProps: {},
// },
// {
// component: 'ColorPicker',
// label: '颜色选择器',
// icon: 'carbon:color-palette',
// field: '',
// colProps: { span: 24 },
// componentProps: {
// defaultValue: '',
// value: '',
// },
// },
{
component: 'slot',
label: '插槽',
icon: 'vs:timeslot-question',
field: '',
colProps: { span: 24 },
componentProps: {
slotName: 'slotName'
}
}
]
// https://next.antdv.com/components/transfer-cn
const transferControl = {
component: 'Transfer',
label: '穿梭框',
icon: 'bx:bx-transfer-alt',
field: '',
colProps: { span: 24 },
componentProps: {
render: (item) => item.title,
dataSource: [
{
key: 'key-1',
title: '标题1',
description: '描述',
disabled: false,
chosen: true
},
{
key: 'key-2',
title: 'title2',
description: 'description2',
disabled: true
},
{
key: 'key-3',
title: '标题3',
description: '描述3',
disabled: false,
chosen: true
}
]
}
}
baseComponents.push(transferControl)
export const layoutComponents: IVFormComponent[] = [
{
field: '',
component: 'Grid',
label: '栅格布局',
icon: 'icon-grid',
componentProps: {},
columns: [
{
span: 12,
children: []
},
{
span: 12,
children: []
}
],
colProps: { span: 24 },
options: {
gutter: 0
}
}
]

739
src/components/FormDesign/src/core/iconConfig.ts

@ -0,0 +1,739 @@
const iconConfig = {
filled: [
'account-book',
'alert',
'alipay-circle',
'alipay-square',
'aliwangwang',
'amazon-circle',
'android',
'amazon-square',
'api',
'appstore',
'audio',
'apple',
'backward',
'bank',
'behance-circle',
'bell',
'behance-square',
'book',
'box-plot',
'bug',
'bulb',
'calculator',
'build',
'calendar',
'camera',
'car',
'caret-down',
'caret-left',
'caret-right',
'carry-out',
'caret-up',
'check-circle',
'check-square',
'chrome',
'ci-circle',
'clock-circle',
'close-circle',
'cloud',
'close-square',
'code-sandbox-square',
'code-sandbox-circle',
'code',
'codepen-circle',
'compass',
'codepen-square',
'contacts',
'container',
'control',
'copy',
'copyright-circle',
'credit-card',
'crown',
'customer-service',
'dashboard',
'delete',
'diff',
'dingtalk-circle',
'database',
'dingtalk-square',
'dislike',
'dollar-circle',
'down-circle',
'down-square',
'dribbble-circle',
'dribbble-square',
'dropbox-circle',
'dropbox-square',
'environment',
'edit',
'exclamation-circle',
'euro-circle',
'experiment',
'eye-invisible',
'eye',
'facebook',
'fast-backward',
'fast-forward',
'file-add',
'file-excel',
'file-exclamation',
'file-image',
'file-markdown',
'file-pdf',
'file-ppt',
'file-text',
'file-unknown',
'file-word',
'file-zip',
'file',
'filter',
'fire',
'flag',
'folder-add',
'folder',
'folder-open',
'forward',
'frown',
'fund',
'funnel-plot',
'gift',
'github',
'gitlab',
'golden',
'google-circle',
'google-plus-circle',
'google-plus-square',
'google-square',
'hdd',
'heart',
'highlight',
'home',
'hourglass',
'html5',
'idcard',
'ie-circle',
'ie-square',
'info-circle',
'instagram',
'insurance',
'interaction',
'interation',
'layout',
'left-circle',
'left-square',
'like',
'linkedin',
'lock',
'mail',
'medicine-box',
'medium-circle',
'medium-square',
'meh',
'message',
'minus-circle',
'minus-square',
'mobile',
'money-collect',
'pause-circle',
'pay-circle',
'notification',
'phone',
'picture',
'pie-chart',
'play-circle',
'play-square',
'plus-circle',
'plus-square',
'pound-circle',
'printer',
'profile',
'project',
'pushpin',
'property-safety',
'qq-circle',
'qq-square',
'question-circle',
'read',
'reconciliation',
'red-envelope',
'reddit-circle',
'reddit-square',
'rest',
'right-circle',
'rocket',
'right-square',
'safety-certificate',
'save',
'schedule',
'security-scan',
'setting',
'shop',
'shopping',
'sketch-circle',
'sketch-square',
'skin',
'slack-circle',
'skype',
'slack-square',
'sliders',
'smile',
'snippets',
'sound',
'star',
'step-backward',
'step-forward',
'stop',
'switcher',
'tablet',
'tag',
'tags',
'taobao-circle',
'taobao-square',
'tool',
'thunderbolt',
'trademark-circle',
'twitter-circle',
'trophy',
'twitter-square',
'unlock',
'up-circle',
'up-square',
'usb',
'video-camera',
'wallet',
'warning',
'wechat',
'weibo-circle',
'windows',
'yahoo',
'weibo-square',
'yuque',
'youtube',
'zhihu-circle',
'zhihu-square'
],
outlined: [
'account-book',
'alert',
'alipay-circle',
'aliwangwang',
'android',
'api',
'appstore',
'audio',
'apple',
'backward',
'bank',
'bell',
'behance-square',
'book',
'box-plot',
'bug',
'bulb',
'calculator',
'build',
'calendar',
'camera',
'car',
'caret-down',
'caret-left',
'caret-right',
'carry-out',
'caret-up',
'check-circle',
'check-square',
'chrome',
'clock-circle',
'close-circle',
'cloud',
'close-square',
'code',
'codepen-circle',
'compass',
'contacts',
'container',
'control',
'copy',
'credit-card',
'crown',
'customer-service',
'dashboard',
'delete',
'diff',
'database',
'dislike',
'down-circle',
'down-square',
'dribbble-square',
'environment',
'edit',
'exclamation-circle',
'experiment',
'eye-invisible',
'eye',
'facebook',
'fast-backward',
'fast-forward',
'file-add',
'file-excel',
'file-exclamation',
'file-image',
'file-markdown',
'file-pdf',
'file-ppt',
'file-text',
'file-unknown',
'file-word',
'file-zip',
'file',
'filter',
'fire',
'flag',
'folder-add',
'folder',
'folder-open',
'forward',
'frown',
'fund',
'funnel-plot',
'gift',
'github',
'gitlab',
'hdd',
'heart',
'highlight',
'home',
'hourglass',
'html5',
'idcard',
'info-circle',
'instagram',
'insurance',
'interaction',
'interation',
'layout',
'left-circle',
'left-square',
'like',
'linkedin',
'lock',
'mail',
'medicine-box',
'meh',
'message',
'minus-circle',
'minus-square',
'mobile',
'money-collect',
'pause-circle',
'pay-circle',
'notification',
'phone',
'picture',
'pie-chart',
'play-circle',
'play-square',
'plus-circle',
'plus-square',
'printer',
'profile',
'project',
'pushpin',
'property-safety',
'question-circle',
'read',
'reconciliation',
'red-envelope',
'rest',
'right-circle',
'rocket',
'right-square',
'safety-certificate',
'save',
'schedule',
'security-scan',
'setting',
'shop',
'shopping',
'skin',
'skype',
'slack-square',
'sliders',
'smile',
'snippets',
'sound',
'star',
'step-backward',
'step-forward',
'stop',
'switcher',
'tablet',
'tag',
'tags',
'taobao-circle',
'tool',
'thunderbolt',
'trophy',
'unlock',
'up-circle',
'up-square',
'usb',
'video-camera',
'wallet',
'warning',
'wechat',
'weibo-circle',
'windows',
'yahoo',
'weibo-square',
'yuque',
'youtube',
'alibaba',
'align-center',
'align-left',
'align-right',
'alipay',
'aliyun',
'amazon',
'ant-cloud',
'apartment',
'ant-design',
'area-chart',
'arrow-left',
'arrow-down',
'arrow-up',
'arrows-alt',
'arrow-right',
'audit',
'bar-chart',
'barcode',
'bars',
'behance',
'bg-colors',
'block',
'bold',
'border-bottom',
'border-left',
'border-outer',
'border-inner',
'border-right',
'border-horizontal',
'border-top',
'border-verticle',
'border',
'branches',
'check',
'ci',
'close',
'cloud-download',
'cloud-server',
'cloud-sync',
'cloud-upload',
'cluster',
'codepen',
'code-sandbox',
'colum-height',
'column-width',
'column-height',
'coffee',
'copyright',
'dash',
'deployment-unit',
'desktop',
'dingding',
'disconnect',
'dollar',
'double-left',
'dot-chart',
'double-right',
'down',
'drag',
'download',
'dribbble',
'dropbox',
'ellipsis',
'enter',
'euro',
'exception',
'exclamation',
'export',
'fall',
'file-done',
'file-jpg',
'file-protect',
'file-sync',
'file-search',
'font-colors',
'font-size',
'fork',
'form',
'fullscreen-exit',
'fullscreen',
'gateway',
'global',
'google-plus',
'gold',
'google',
'heat-map',
'history',
'ie',
'import',
'inbox',
'info',
'italic',
'key',
'issues-close',
'laptop',
'left',
'line-chart',
'link',
'line-height',
'line',
'loading-3-quarters',
'loading',
'login',
'logout',
'man',
'medium',
'medium-workmark',
'menu-unfold',
'menu-fold',
'menu',
'minus',
'monitor',
'more',
'ordered-list',
'number',
'pause',
'percentage',
'paper-clip',
'pic-center',
'pic-left',
'pic-right',
'plus',
'pound',
'poweroff',
'pull-request',
'qq',
'question',
'radar-chart',
'qrcode',
'radius-bottomleft',
'radius-bottomright',
'radius-upleft',
'radius-setting',
'radius-upright',
'reddit',
'redo',
'reload',
'retweet',
'right',
'rise',
'rollback',
'safety',
'robot',
'scan',
'search',
'scissor',
'select',
'shake',
'share-alt',
'shopping-cart',
'shrink',
'sketch',
'slack',
'small-dash',
'solution',
'sort-descending',
'sort-ascending',
'stock',
'swap-left',
'swap-right',
'strikethrough',
'swap',
'sync',
'table',
'team',
'taobao',
'to-top',
'trademark',
'transaction',
'twitter',
'underline',
'undo',
'unordered-list',
'up',
'upload',
'user-add',
'user-delete',
'usergroup-add',
'user',
'usergroup-delete',
'vertical-align-bottom',
'vertical-align-middle',
'vertical-align-top',
'vertical-left',
'vertical-right',
'weibo',
'wifi',
'zhihu',
'woman',
'zoom-out',
'zoom-in'
],
twoTone: [
'account-book',
'alert',
'api',
'appstore',
'audio',
'bank',
'bell',
'book',
'box-plot',
'bug',
'bulb',
'calculator',
'build',
'calendar',
'camera',
'car',
'carry-out',
'check-circle',
'check-square',
'clock-circle',
'close-circle',
'cloud',
'close-square',
'code',
'compass',
'contacts',
'container',
'control',
'copy',
'credit-card',
'crown',
'customer-service',
'dashboard',
'delete',
'diff',
'database',
'dislike',
'down-circle',
'down-square',
'environment',
'edit',
'exclamation-circle',
'experiment',
'eye-invisible',
'eye',
'file-add',
'file-excel',
'file-exclamation',
'file-image',
'file-markdown',
'file-pdf',
'file-ppt',
'file-text',
'file-unknown',
'file-word',
'file-zip',
'file',
'filter',
'fire',
'flag',
'folder-add',
'folder',
'folder-open',
'frown',
'fund',
'funnel-plot',
'gift',
'hdd',
'heart',
'highlight',
'home',
'hourglass',
'html5',
'idcard',
'info-circle',
'insurance',
'interaction',
'interation',
'layout',
'left-circle',
'left-square',
'like',
'lock',
'mail',
'medicine-box',
'meh',
'message',
'minus-circle',
'minus-square',
'mobile',
'money-collect',
'pause-circle',
'notification',
'phone',
'picture',
'pie-chart',
'play-circle',
'play-square',
'plus-circle',
'plus-square',
'pound-circle',
'printer',
'profile',
'project',
'pushpin',
'property-safety',
'question-circle',
'reconciliation',
'red-envelope',
'rest',
'right-circle',
'rocket',
'right-square',
'safety-certificate',
'save',
'schedule',
'security-scan',
'setting',
'shop',
'shopping',
'skin',
'sliders',
'smile',
'snippets',
'sound',
'star',
'stop',
'switcher',
'tablet',
'tag',
'tags',
'tool',
'thunderbolt',
'trademark-circle',
'trophy',
'unlock',
'up-circle',
'up-square',
'usb',
'video-camera',
'wallet',
'warning',
'ci',
'copyright',
'dollar',
'euro',
'gold',
'canlendar'
]
}
export default iconConfig

18
src/components/FormDesign/src/hooks/useFormDesignState.ts

@ -0,0 +1,18 @@
import { inject, Ref } from 'vue'
import { IFormDesignMethods } from '../typings/form-type'
import { IFormConfig } from '../typings/v-form-component'
/**
* formDesign状态
*/
export function useFormDesignState() {
const formConfig = inject('formConfig') as Ref<IFormConfig>
const formDesignMethods = inject('formDesignMethods') as IFormDesignMethods
return { formConfig, formDesignMethods }
}
export function useFormModelState() {
const formModel = inject('formModel') as Ref<{}>
const setFormModel = inject('setFormModelMethod') as (key: String, value: any) => void
return { formModel, setFormModel }
}

56
src/components/FormDesign/src/hooks/useFormInstanceMethods.ts

@ -0,0 +1,56 @@
import { IAnyObject } from '../typings/base-type'
import { Ref, SetupContext } from 'vue'
import { cloneDeep, forOwn, isFunction } from 'lodash-es'
import { AForm, IVFormComponent } from '../typings/v-form-component'
import { getCurrentInstance } from 'vue'
import { Form } from 'ant-design-vue'
import { toRaw } from 'vue'
export function useFormInstanceMethods(props: IAnyObject, formdata, context: Partial<SetupContext>, _formInstance: Ref<AForm | null>) {
/**
* props和on中的上下文为parent
*/
const bindContext = () => {
const instance = getCurrentInstance()
const vm = instance?.parent
if (!vm) return
;(props.formConfig.schemas as IVFormComponent[]).forEach((item) => {
// 绑定 props 中的上下文
forOwn(item.componentProps, (value: any, key) => {
if (isFunction(value)) {
item.componentProps![key] = value.bind(vm)
}
})
// 绑定事件监听(v-on)的上下文
forOwn(item.on, (value: any, key) => {
if (isFunction(value)) {
item.componentProps![key] = value.bind(vm)
}
})
})
}
bindContext()
const { emit } = context
const useForm = Form.useForm
const { resetFields, validate, clearValidate, validateField } = useForm(formdata, [])
const submit = async () => {
//const _result = await validate();
const data = cloneDeep(toRaw(formdata.value))
emit?.('submit', data)
props.formConfig.submit?.(data)
return data
}
return {
validate,
validateField,
resetFields,
clearValidate,
submit
}
}

188
src/components/FormDesign/src/hooks/useVFormMethods.ts

@ -0,0 +1,188 @@
import { Ref, SetupContext } from 'vue'
import { IVFormComponent, IFormConfig, AForm } from '../typings/v-form-component'
import { findFormItem, formItemsForEach } from '../utils'
import { cloneDeep, isFunction } from 'lodash-es'
import { IAnyObject } from '../typings/base-type'
interface IFormInstanceMethods extends AForm {
submit: () => Promise<any>
}
export interface IProps {
formConfig: IFormConfig
formModel: IAnyObject
}
type ISet = <T extends keyof IVFormComponent>(field: string, key: T, value: IVFormComponent[T]) => void
// 获取当前field绑定的表单项
type IGet = (field: string) => IVFormComponent | undefined
// 获取field在formData中的值
type IGetValue = (field: string) => any
// 设置field在formData中的值并且触发校验
type ISetValue = (field: string | IAnyObject, value?: any) => void
// 隐藏field对应的表单项
type IHidden = (field: string) => void
// 显示field对应的表单项
type IShow = (field: string) => void
// 设置field对应的表单项绑定的props属性
type ISetProps = (field: string, key: string, value: any) => void
// 获取formData中的值
type IGetData = () => Promise<IAnyObject>
// 禁用表单,如果field为空,则禁用整个表单
type IDisable = (field?: string | boolean) => void
// 设置表单配置方法
type ISetFormConfig = (key: string, value: any) => void
interface ILinkOn {
[key: string]: Set<IVFormComponent>
}
export interface IVFormMethods extends Partial<IFormInstanceMethods> {
set: ISet
get: IGet
getValue: IGetValue
setValue: ISetValue
hidden: IHidden
show: IShow
setProps: ISetProps
linkOn: ILinkOn
getData: IGetData
disable: IDisable
}
export function useVFormMethods(
props: IProps,
_context: Partial<SetupContext>,
formInstance: Ref<AForm | null>,
formInstanceMethods: Partial<IFormInstanceMethods>
): IVFormMethods {
/**
* field获取表单项
* @param {string} field
* @return {IVFormComponent | undefined}
*/
const get: IGet = (field) => findFormItem(props.formConfig.schemas, (item) => item.field === field)
/**
* field设置表单项字段值
* @param {string} field
* @param {keyof IVFormComponent} key
* @param {never} value
*/
const set: ISet = (field, key, value) => {
const formItem = get(field)
if (formItem) formItem[key] = value
}
/**
* props
* @param {string} field field
* @param {string} key key
* @param value
*/
const setProps: ISetProps = (field, key, value) => {
const formItem = get(field)
if (formItem?.componentProps) {
;['options', 'treeData'].includes(key) && setValue(field, undefined)
formItem.componentProps[key] = value
}
}
/**
*
* @param {string} field
* @param value
*/
const setValue: ISetValue = (field, value) => {
if (typeof field === 'string') {
// props.formData[field] = value
props.formModel[field] = value
formInstance.value?.validateField(field, value, [])
} else {
const keys = Object.keys(field)
keys.forEach((key) => {
props.formModel[key] = field[key]
formInstance.value?.validateField(key, field[key], [])
})
}
}
/**
*
* @param {string} key
* @param value
*/
const setFormConfig: ISetFormConfig = (key, value) => {
props.formConfig[key] = value
}
/**
* field获取字段值field为空
* @param {string} field
*/
const getValue: IGetValue = (field) => {
const formData = cloneDeep(props.formModel)
return formData[field]
}
/**
* formData中的值
* @return {Promise<IAnyObject<any>>}
*/
const getData: IGetData = async () => {
return cloneDeep(props.formModel)
}
/**
*
* @param {string} field field
*/
const hidden: IHidden = (field) => {
set(field, 'hidden', true)
}
/**
*
* @param {string | undefined} field
*/
const disable: IDisable = (field) => {
typeof field === 'string' ? setProps(field, 'disabled', true) : setFormConfig('disabled', field !== false)
}
/**
*
* @param {string} field field
*/
const show: IShow = (field) => {
set(field, 'hidden', false)
}
/**
*
* @type {ILinkOn}
*/
const linkOn: ILinkOn = {}
const initLink = (schemas: IVFormComponent[]) => {
// 首次遍历,查找需要关联字段的表单
formItemsForEach(schemas, (formItem) => {
// 如果需要关联,则进行第二层遍历,查找表单中关联的字段,存到Set中
formItemsForEach(schemas, (item) => {
if (!linkOn[item.field!]) linkOn[item.field!] = new Set<IVFormComponent>()
if (formItem.link?.includes(item.field!) && isFunction(formItem.update)) {
linkOn[item.field!].add(formItem)
}
})
linkOn[formItem.field!].add(formItem)
})
}
initLink(props.formConfig.schemas)
return {
linkOn,
setValue,
getValue,
hidden,
show,
set,
get,
setProps,
getData,
disable,
...formInstanceMethods
}
}

10
src/components/FormDesign/src/typings/base-type.ts

@ -0,0 +1,10 @@
export interface IAnyObject<T = any> {
[key: string]: T
}
export interface IInputEvent {
target: {
value: any
checked: boolean
}
}

48
src/components/FormDesign/src/typings/form-type.ts

@ -0,0 +1,48 @@
import { Ref } from 'vue'
import { IAnyObject } from './base-type'
import { IFormConfig, IVFormComponent } from './v-form-component'
export interface IToolbarMethods {
showModal: (jsonData: IAnyObject) => void
}
type ChangeTabKey = 1 | 2
export interface IPropsPanel {
changeTab: (key: ChangeTabKey) => void
}
export interface IState {
// 语言
locale: any
// 公用组件
baseComponents: IVFormComponent[]
// 自定义组件
customComponents: IVFormComponent[]
// 布局组件
layoutComponents: IVFormComponent[]
// 属性面板实例
propsPanel: Ref<null | IPropsPanel>
// json模态框实例
jsonModal: Ref<null | IToolbarMethods>
// 导入json数据模态框
importJsonModal: Ref<null | IToolbarMethods>
// 代码预览模态框
codeModal: Ref<null | IToolbarMethods>
// 预览模态框
eFormPreview: Ref<null | IToolbarMethods>
eFormPreview2: Ref<null | IToolbarMethods>
}
export interface IFormDesignMethods {
// 设置当前选中的控件
handleSetSelectItem(item: IVFormComponent): void
// 添加控件到formConfig.formItems中
handleListPush(item: IVFormComponent): void
// 复制控件
handleCopy(item?: IVFormComponent, isCopy?: boolean): void
// 添加控件属性
handleAddAttrs(schemas: IVFormComponent[], index: number): void
setFormConfig(config: IFormConfig): void
// 添加到表单中之前触发
handleBeforeColAdd(event: { newIndex: string }, schemas: IVFormComponent[], isCopy?: boolean): void
}

344
src/components/FormDesign/src/typings/v-form-component.ts

@ -0,0 +1,344 @@
import { IAnyObject } from './base-type'
// import { ComponentOptions } from 'vue/types/options';
import { ComponentOptions } from 'vue'
import { IVFormMethods } from '../hooks/useVFormMethods'
import { ColEx } from '@/components/Form/src/types'
import { SelectValue } from 'ant-design-vue/lib/select'
import { validateOptions } from 'ant-design-vue/lib/form/useForm'
import { RuleError } from 'ant-design-vue/lib/form/interface'
import { FormItem } from '@/components/Form'
type LayoutType = 'horizontal' | 'vertical' | 'inline'
type labelLayout = 'flex' | 'Grid'
export type PropsTabKey = 1 | 2 | 3
type ColSpanType = number | string
declare type Value = [number, number] | number
/**
*
*/
export interface IVFormComponent {
// extends Omit<FormSchema, 'component' | 'label' | 'field' | 'rules'> {
// 对应的字段
field?: string
// 组件类型
component: string
// 组件label
label?: string
// 自定义组件控件实例
componentInstance?: ComponentOptions<any>
// 组件icon
icon?: string
// 组件校验规则
rules?: Partial<IValidationRule>[]
// 是否隐藏
hidden?: boolean
// 隐藏label
hiddenLabel?: boolean
// 组件宽度
width?: string
// 是否必选
required?: boolean
// 必选提示
message?: string
// 提示信息
helpMessage?: string
// 传给给组件的属性,默认会吧所有的props都传递给控件
componentProps?: IAnyObject
// 监听组件事件对象,以v-on方式传递给控件
on?: IAnyObject<(...any: []) => void>
// 组件选项
options?: IAnyObject
// 唯一标识
key?: string
// Reference formModelItem
itemProps?: Partial<FormItem>
colProps?: Partial<ColEx>
// 联动字段
link?: string[]
// 联动属性变化的回调
update?: (value: any, formItem: IVFormComponent, fApi: IVFormMethods) => void
// 控件栅格数
// span?: number;
// 标签布局
labelCol?: IAnyObject
// 组件布局
wrapperCol?: IAnyObject
// 子控件
columns?: Array<{ span: number; children: any[] }>
}
declare type namesType = string | string[]
/**
*
*/
export interface IFormConfig {
// 表单项配置列表
// schemas: IVFormComponent[];
// 表单配置
// config: {
layout?: LayoutType
labelLayout?: labelLayout
labelWidth?: number
labelCol?: Partial<IACol>
wrapperCol?: Partial<IACol>
hideRequiredMark?: boolean
// Whether to disable
schemas: IVFormComponent[]
disabled?: boolean
labelAlign?: 'left' | 'right'
// Internal component size of the form
size?: 'default' | 'small' | 'large'
// };
// 当前选中项
currentItem?: IVFormComponent
activeKey?: PropsTabKey
colon?: boolean
}
export interface AForm {
/**
* Hide required mark of all form items
* @default false
* @type boolean
*/
hideRequiredMark: boolean
/**
* The layout of label. You can set span offset to something like {span: 3, offset: 12} or sm: {span: 3, offset: 12} same as with <Col>
* @type IACol
*/
labelCol: IACol
/**
* Define form layout
* @default 'horizontal'
* @type string
*/
layout: 'horizontal' | 'inline' | 'vertical'
/**
* The layout for input controls, same as labelCol
* @type IACol
*/
wrapperCol: IACol
/**
* change default props colon value of Form.Item (only effective when prop layout is horizontal)
* @type boolean
* @default true
*/
colon: boolean
/**
* text align of label of all items
* @type 'left' | 'right'
* @default 'left'
*/
labelAlign: 'left' | 'right'
/**
* data of form component
* @type object
*/
model: IAnyObject
/**
* validation rules of form
* @type object
*/
rules: IAnyObject
/**
* Default validate message. And its format is similar with newMessages's returned value
* @type any
*/
validateMessages?: any
/**
* whether to trigger validation when the rules prop is changed
* @type Boolean
* @default true
*/
validateOnRuleChange: boolean
/**
* validate the whole form. Takes a callback as a param. After validation,
* the callback will be executed with two params: a boolean indicating if the validation has passed,
* and an object containing all fields that fail the validation. Returns a promise if callback is omitted
* @type Function
*/
validate: <T = any>(names?: namesType, option?: validateOptions) => Promise<T>
/**
* validate one or several form items
* @type Function
*/
validateField: (name: string, value: any, rules: Record<string, unknown>[], option?: validateOptions) => Promise<RuleError[]>
/**
* reset all the fields and remove validation result
*/
resetFields: () => void
/**
* clear validation message for certain fields.
* The parameter is prop name or an array of prop names of the form items whose validation messages will be removed.
* When omitted, all fields' validation messages will be cleared
* @type string[] | string
*/
clearValidate: (props: string[] | string) => void
}
interface IACol {
/**
* raster number of cells to occupy, 0 corresponds to display: none
* @default none (0)
* @type ColSpanType
*/
span: Value
/**
* raster order, used in flex layout mode
* @default 0
* @type ColSpanType
*/
order: ColSpanType
/**
* the layout fill of flex
* @default none
* @type ColSpanType
*/
flex: ColSpanType
/**
* the number of cells to offset Col from the left
* @default 0
* @type ColSpanType
*/
offset: ColSpanType
/**
* the number of cells that raster is moved to the right
* @default 0
* @type ColSpanType
*/
push: ColSpanType
/**
* the number of cells that raster is moved to the left
* @default 0
* @type ColSpanType
*/
pull: ColSpanType
/**
* <576px and also default setting, could be a span value or an object containing above props
* @type { span: ColSpanType, offset: ColSpanType } | ColSpanType
*/
xs: { span: ColSpanType; offset: ColSpanType } | ColSpanType
/**
* 576px, could be a span value or an object containing above props
* @type { span: ColSpanType, offset: ColSpanType } | ColSpanType
*/
sm: { span: ColSpanType; offset: ColSpanType } | ColSpanType
/**
* 768px, could be a span value or an object containing above props
* @type { span: ColSpanType, offset: ColSpanType } | ColSpanType
*/
md: { span: ColSpanType; offset: ColSpanType } | ColSpanType
/**
* 992px, could be a span value or an object containing above props
* @type { span: ColSpanType, offset: ColSpanType } | ColSpanType
*/
lg: { span: ColSpanType; offset: ColSpanType } | ColSpanType
/**
* 1200px, could be a span value or an object containing above props
* @type { span: ColSpanType, offset: ColSpanType } | ColSpanType
*/
xl: { span: ColSpanType; offset: ColSpanType } | ColSpanType
/**
* 1600px, could be a span value or an object containing above props
* @type { span: ColSpanType, offset: ColSpanType } | ColSpanType
*/
xxl: { span: ColSpanType; offset: ColSpanType } | ColSpanType
}
export interface IValidationRule {
trigger?: 'change' | 'blur' | ['change', 'blur']
/**
* validation error message
* @type string | Function
*/
message?: string | number
/**
* built-in validation type, available options: https://github.com/yiminghe/async-validator#type
* @default 'string'
* @type string
*/
type?: string
/**
* indicates whether field is required
* @default false
* @type boolean
*/
required?: boolean
/**
* treat required fields that only contain whitespace as errors
* @default false
* @type boolean
*/
whitespace?: boolean
/**
* validate the exact length of a field
* @type number
*/
len?: number
/**
* validate the min length of a field
* @type number
*/
min?: number
/**
* validate the max length of a field
* @type number
*/
max?: number
/**
* validate the value from a list of possible values
* @type string | string[]
*/
enum?: string | string[]
/**
* validate from a regular expression
* @type boolean
*/
pattern?: SelectValue
/**
* transform a value before validation
* @type Function
*/
transform?: (value: any) => any
/**
* custom validate function (Note: callback must be called)
* @type Function
*/
validator?: (rule: any, value: any, callback: () => void) => any
}

195
src/components/FormDesign/src/utils/index.ts

@ -0,0 +1,195 @@
// import { VueConstructor } from 'vue';
import { IVFormComponent, IFormConfig, IValidationRule } from '../typings/v-form-component'
import { cloneDeep, isArray, isFunction, isNumber, uniqueId } from 'lodash-es'
// import { del } from '@vue/composition-api';
// import { withInstall } from '/@/utils';
/**
* install方法
* @param comp install方法的组件
*/
// export function withInstall<T extends { name: string }>(comp: T) {
// return Object.assign(comp, {
// install(Vue: VueConstructor) {
// Vue.component(comp.name, comp);
// },
// });
// }
/**
* key
* @param [formItem] key key
* @returns {string|boolean} id false
*/
export function generateKey(formItem?: IVFormComponent): string | boolean {
if (formItem && formItem.component) {
const key = uniqueId(`${toLine(formItem.component)}_`)
formItem.key = key
formItem.field = key
return true
}
return uniqueId('key_')
}
/**
* value可以是一个数字下标true的元素
* @param array {Array<T>}
* @param value {number | ((item: T, index: number, array: Array<T>) => boolean}
* @returns {T}
*/
export function remove<T>(array: Array<T>, value: number | ((item: T, index: number, array: Array<T>) => boolean)): T | undefined {
let removeVal: Array<T | undefined> = []
if (!isArray(array)) return undefined
if (isNumber(value)) {
removeVal = array.splice(value, 1)
} else {
const index = array.findIndex(value)
if (index !== -1) {
removeVal = array.splice(index, 1)
}
}
return removeVal.shift()
}
/**
*
* @param value
*/
export function getType(value: any): string {
return Object.prototype.toString.call(value).slice(8, -1)
}
/**
* guid
* @returns {String} id标识符
*/
export function randomUUID(): string {
function S4() {
return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1)
}
return `${S4() + S4()}-${S4()}-${S4()}-${S4()}-${S4() + S4() + S4()}`
}
/**
* 线
* @param str
*/
export function toLine(str: string) {
return str.replace(/([A-Z])/g, '_$1').toLowerCase()
}
/**
*
* @param array
* @param cb
*/
export function formItemsForEach(array: IVFormComponent[], cb: (item: IVFormComponent) => void) {
if (!isArray(array)) return
const traverse = (schemas: IVFormComponent[]) => {
schemas.forEach((formItem: IVFormComponent) => {
if (['Grid'].includes(formItem.component)) {
// 栅格布局
formItem.columns?.forEach((item) => traverse(item.children))
} else {
cb(formItem)
}
})
}
traverse(array)
}
/**
*
*/
export const findFormItem: (schemas: IVFormComponent[], cb: (formItem: IVFormComponent) => boolean) => IVFormComponent | undefined = (
schemas,
cb
) => {
let res
const traverse = (schemas: IVFormComponent[]): boolean => {
return schemas.some((formItem: IVFormComponent) => {
const { component: type } = formItem
// 处理栅格
if (['Grid'].includes(type)) {
return formItem.columns?.some((item) => traverse(item.children))
}
if (cb(formItem)) res = formItem
return cb(formItem)
})
}
traverse(schemas)
return res
}
/**
* json模态框时删除当前项属性
* @param formConfig {IFormConfig}
* @returns {IFormConfig}
*/
export const removeAttrs = (formConfig: IFormConfig): IFormConfig => {
const copyFormConfig = cloneDeep(formConfig)
delete copyFormConfig.currentItem
delete copyFormConfig.activeKey
copyFormConfig.schemas &&
formItemsForEach(copyFormConfig.schemas, (item) => {
delete item.icon
delete item.key
})
return copyFormConfig
}
/**
* select treeSelect Promise对象
* @param {(() => Promise<any[]>) | any[]} options
* @return {Promise<any[]>}
*/
export const handleAsyncOptions = async (options: (() => Promise<any[]>) | any[]): Promise<any[]> => {
try {
if (isFunction(options)) return await options()
return options
} catch {
return []
}
}
/**
*
* @param {IVFormComponent[]} schemas
*/
export const formatRules = (schemas: IVFormComponent[]) => {
formItemsForEach(schemas, (item) => {
if ('required' in item) {
!isArray(item.rules) && (item.rules = [])
item.rules.push({ required: true, message: item.message })
delete item['required']
delete item['message']
}
})
}
/**
*
* @param {IValidationRule[]} rules
* @return {IValidationRule[]}
*/
export const strToReg = (rules: IValidationRule[]) => {
const newRules = cloneDeep(rules)
return newRules.map((item) => {
if (item.pattern) item.pattern = runCode(item.pattern)
return item
})
}
/**
*
* @param code
* @return {any}
*/
export const runCode = <T>(code: any): T => {
try {
return new Function(`return ${code}`)()
} catch {
return code
}
}

18
src/components/FormDesign/src/utils/message.ts

@ -0,0 +1,18 @@
import { useMessage } from '@/hooks/web/useMessage'
const { createMessage } = useMessage()
const message = Object.assign({
success: (msg: string) => {
createMessage.success(msg)
},
error: (msg: string) => {
createMessage.error(msg)
},
warning: (msg: string) => {
createMessage.warning(msg)
},
info: (msg: string) => {
createMessage.info(msg)
}
})
export default message

9
src/views/infra/build/index.vue

@ -1,3 +1,10 @@
<template>
<div>开发中</div>
<PageWrapper dense contentFullHeight fixedHeight>
<VFormDesign />
</PageWrapper>
</template>
<script lang="ts" setup>
import { PageWrapper } from '@/components/Page'
import { VFormDesign } from '@/components/FormDesign'
</script>