diff --git a/package.json b/package.json index cdd0d136..46aeb1de 100644 --- a/package.json +++ b/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": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f0d6cc14..06b415f2 100644 --- a/pnpm-lock.yaml +++ b/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: diff --git a/src/components/FormDesign/index.ts b/src/components/FormDesign/index.ts new file mode 100644 index 00000000..50925e3a --- /dev/null +++ b/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 } diff --git a/src/components/FormDesign/src/components/VFormCreate/components/FormRender.vue b/src/components/FormDesign/src/components/VFormCreate/components/FormRender.vue new file mode 100644 index 00000000..b7f2536e --- /dev/null +++ b/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> diff --git a/src/components/FormDesign/src/components/VFormCreate/index.vue b/src/components/FormDesign/src/components/VFormCreate/index.vue new file mode 100644 index 00000000..170fe59e --- /dev/null +++ b/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> diff --git a/src/components/FormDesign/src/components/VFormDesign/components/CodeModal.vue b/src/components/FormDesign/src/components/VFormDesign/components/CodeModal.vue new file mode 100644 index 00000000..35c1c293 --- /dev/null +++ b/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> diff --git a/src/components/FormDesign/src/components/VFormDesign/components/ComponentProps.vue b/src/components/FormDesign/src/components/VFormDesign/components/ComponentProps.vue new file mode 100644 index 00000000..e50a36ce --- /dev/null +++ b/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> diff --git a/src/components/FormDesign/src/components/VFormDesign/components/FormItemColumnProps.vue b/src/components/FormDesign/src/components/VFormDesign/components/FormItemColumnProps.vue new file mode 100644 index 00000000..93610287 --- /dev/null +++ b/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> diff --git a/src/components/FormDesign/src/components/VFormDesign/components/FormItemProps.vue b/src/components/FormDesign/src/components/VFormDesign/components/FormItemProps.vue new file mode 100644 index 00000000..2d51c9ab --- /dev/null +++ b/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> diff --git a/src/components/FormDesign/src/components/VFormDesign/components/FormNode.vue b/src/components/FormDesign/src/components/VFormDesign/components/FormNode.vue new file mode 100644 index 00000000..8d52ec94 --- /dev/null +++ b/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> diff --git a/src/components/FormDesign/src/components/VFormDesign/components/FormNodeOperate.vue b/src/components/FormDesign/src/components/VFormDesign/components/FormNodeOperate.vue new file mode 100644 index 00000000..e9bf0e57 --- /dev/null +++ b/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> diff --git a/src/components/FormDesign/src/components/VFormDesign/components/FormOptions.vue b/src/components/FormDesign/src/components/VFormDesign/components/FormOptions.vue new file mode 100644 index 00000000..d514fdfd --- /dev/null +++ b/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> diff --git a/src/components/FormDesign/src/components/VFormDesign/components/FormProps.vue b/src/components/FormDesign/src/components/VFormDesign/components/FormProps.vue new file mode 100644 index 00000000..3d1620e7 --- /dev/null +++ b/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> diff --git a/src/components/FormDesign/src/components/VFormDesign/components/ImportJsonModal.vue b/src/components/FormDesign/src/components/VFormDesign/components/ImportJsonModal.vue new file mode 100644 index 00000000..5ada14dd --- /dev/null +++ b/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> diff --git a/src/components/FormDesign/src/components/VFormDesign/components/JsonModal.vue b/src/components/FormDesign/src/components/VFormDesign/components/JsonModal.vue new file mode 100644 index 00000000..6d7a2306 --- /dev/null +++ b/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> diff --git a/src/components/FormDesign/src/components/VFormDesign/components/LayoutItem.vue b/src/components/FormDesign/src/components/VFormDesign/components/LayoutItem.vue new file mode 100644 index 00000000..f9b68506 --- /dev/null +++ b/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) + + // 计算布局元素,水平模式下为ACol,非水平模式下为div + 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> diff --git a/src/components/FormDesign/src/components/VFormDesign/components/PreviewCode.vue b/src/components/FormDesign/src/components/VFormDesign/components/PreviewCode.vue new file mode 100644 index 00000000..a78bac85 --- /dev/null +++ b/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> diff --git a/src/components/FormDesign/src/components/VFormDesign/components/RuleProps.vue b/src/components/FormDesign/src/components/VFormDesign/components/RuleProps.vue new file mode 100644 index 00000000..64dccc0f --- /dev/null +++ b/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> diff --git a/src/components/FormDesign/src/components/VFormDesign/config/componentPropsConfig.ts b/src/components/FormDesign/src/components/VFormDesign/config/componentPropsConfig.ts new file mode 100644 index 00000000..09037a5a --- /dev/null +++ b/src/components/FormDesign/src/components/VFormDesign/config/componentPropsConfig.ts @@ -0,0 +1,1148 @@ +import { IBaseFormAttrs } from './formItemPropsConfig' + +interface IBaseComponentProps { + [key: string]: IBaseFormAttrs[] +} + +type BaseFormAttrs = Omit<IBaseFormAttrs, 'tag'> + +export const baseComponentControlAttrs: Omit<IBaseFormAttrs, 'tag'>[] = [ + { + // 没有disabled属性的控件不能作为form控件 + name: 'disabled', + label: '禁用' + }, + { + // 没有disabled属性的控件不能作为form控件 + name: 'autofocus', + label: '自动获取焦点', + includes: [ + 'Input', + 'Select', + 'InputTextArea', + 'InputNumber', + 'DatePicker', + 'RangePicker', + 'MonthPicker', + 'TimePicker', + 'Cascader', + 'TreeSelect', + 'Switch', + 'AutoComplete', + 'Slider' + ] + }, + { + name: 'allowClear', + label: '可清除', + includes: [ + 'Input', + 'Select', + 'InputTextArea', + 'InputNumber', + 'DatePicker', + 'RangePicker', + 'MonthPicker', + 'TimePicker', + 'Cascader', + 'TreeSelect', + 'AutoComplete' + ] + }, + { name: 'fullscreen', label: '全屏', includes: ['Calendar'] }, + { + name: 'showSearch', + label: '可搜索', + includes: ['Select', 'TreeSelect', 'Cascader', 'Transfer'] + }, + { + name: 'showTime', + label: '显示时间', + includes: ['DatePicker', 'RangePicker', 'MonthPicker'] + }, + { + name: 'range', + label: '双向滑动', + includes: [] + }, + { + name: 'allowHalf', + label: '允许半选', + includes: ['Rate'] + }, + { + name: 'multiple', + label: '多选', + includes: ['Select', 'TreeSelect', 'Upload'] + }, + { + name: 'directory', + label: '文件夹', + includes: ['Upload'] + }, + { + name: 'withCredentials', + label: '携带cookie', + includes: ['Upload'] + }, + { + name: 'bordered', + label: '是否有边框', + includes: ['Select', 'Input'] + }, + { + name: 'defaultActiveFirstOption', + label: '高亮第一个选项', + component: 'Checkbox', + includes: ['Select', 'AutoComplete'] + }, + { + name: 'dropdownMatchSelectWidth', + label: '下拉菜单和选择器同宽', + component: 'Checkbox', + includes: ['Select', 'TreeSelect', 'AutoComplete'] + } +] + +//共用属性 +export const baseComponentCommonAttrs: Omit<IBaseFormAttrs, 'tag'>[] = [ + { + name: 'size', + label: '尺寸', + component: 'RadioGroup', + componentProps: { + options: [ + { + label: '默认', + value: 'default' + }, + { + label: '大', + value: 'large' + }, + { + label: '小', + value: 'small' + } + ] + }, + includes: ['InputNumber', 'Input', 'Cascader', 'Button'] + }, + { + name: 'placeholder', + label: '占位符', + component: 'Input', + componentProps: { + placeholder: '请输入占位符' + }, + includes: [ + 'AutoComplete', + 'InputTextArea', + 'InputNumber', + 'Input', + 'InputTextArea', + 'Select', + 'DatePicker', + 'MonthPicker', + 'TimePicker', + 'TreeSelect', + 'Cascader' + ] + }, + { + name: 'style', + label: '样式', + component: 'Input', + componentProps: { + placeholder: '请输入样式' + } + }, + { + name: 'open', + label: '一直展开下拉菜单', + component: 'RadioGroup', + componentProps: { + options: [ + { + label: '默认', + value: undefined + }, + { + label: '是', + value: true + }, + { + label: '否', + value: false + } + ] + }, + includes: ['Select', 'AutoComplete'] + } +] + +const componentAttrs: IBaseComponentProps = { + AutoComplete: [ + { + name: 'backfill', + label: '自动回填', + component: 'Switch', + componentProps: { + span: 8 + } + }, + { + name: 'defaultOpen', + label: '是否默认展开下拉菜单', + component: 'Checkbox' + } + ], + IconPicker: [ + { + name: 'mode', + label: '模式', + component: 'RadioGroup', + componentProps: { + options: [ + { label: 'ICONIFY', value: null }, + { label: 'SVG', value: 'svg' } + // { label: '组合', value: 'combobox' }, + ] + } + } + ], + + // https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/input#%3Cinput%3E_types + Input: [ + { + name: 'type', + label: '类型', + component: 'Select', + componentProps: { + options: [ + { value: 'text', label: '文本' }, + { value: 'search', label: '搜索' }, + { value: 'number', label: '数字' }, + { value: 'range', label: '数字范围' }, + { value: 'password', label: '密码' }, + { value: 'date', label: '日期' }, + { value: 'datetime-local', label: '日期-无时区' }, + { value: 'time', label: '时间' }, + { value: 'month', label: '年月' }, + { value: 'week', label: '星期' }, + { value: 'email', label: '邮箱' }, + { value: 'url', label: 'URL' }, + { value: 'tel', label: '电话号码' }, + { value: 'file', label: '文件' }, + { value: 'button', label: '按钮' }, + { value: 'submit', label: '提交按钮' }, + { value: 'reset', label: '重置按钮' }, + { value: 'radio', label: '单选按钮' }, + { value: 'checkbox', label: '复选框' }, + { value: 'color', label: '颜色' }, + { value: 'image', label: '图像' }, + { value: 'hidden', label: '隐藏' } + ] + } + }, + { + name: 'defaultValue', + label: '默认值', + component: 'Input', + componentProps: { + type: 'text', + placeholder: '请输入默认值' + } + }, + { + name: 'prefix', + label: '前缀', + component: 'Input', + componentProps: { + type: 'text', + placeholder: '请输入前缀' + } + }, + { + name: 'suffix', + label: '后缀', + component: 'Input', + componentProps: { + type: 'text', + placeholder: '请输入后缀' + } + }, + { + name: 'addonBefore', + label: '前置标签', + component: 'Input', + componentProps: { + type: 'text', + placeholder: '请输入前置标签' + } + }, + { + name: 'addonAfter', + label: '后置标签', + component: 'Input', + componentProps: { + type: 'text', + placeholder: '请输入后置标签' + } + }, + { + name: 'maxLength', + label: '最大长度', + component: 'InputNumber', + componentProps: { + type: 'text', + placeholder: '请输入最大长度' + } + } + ], + + InputNumber: [ + { + name: 'defaultValue', + label: '默认值', + component: 'InputNumber', + componentProps: { + placeholder: '请输入默认值' + } + }, + { + name: 'min', + label: '最小值', + component: 'InputNumber', + componentProps: { + type: 'text', + placeholder: '请输入最小值' + } + }, + { + name: 'max', + label: '最大值', + component: 'InputNumber', + componentProps: { + type: 'text', + placeholder: '请输入最大值' + } + }, + { + name: 'precision', + label: '数值精度', + component: 'InputNumber', + componentProps: { + type: 'text', + placeholder: '请输入最大值' + } + }, + { + name: 'step', + label: '步长', + component: 'InputNumber', + componentProps: { + type: 'text', + placeholder: '请输入步长' + } + }, + { + name: 'decimalSeparator', + label: '小数点', + component: 'Input', + componentProps: { type: 'text', placeholder: '请输入小数点' } + }, + { + name: 'addonBefore', + label: '前置标签', + component: 'Input', + componentProps: { + type: 'text', + placeholder: '请输入前置标签' + } + }, + { + name: 'addonAfter', + label: '后置标签', + component: 'Input', + componentProps: { + type: 'text', + placeholder: '请输入后置标签' + } + }, + { + name: 'controls', + label: '是否显示增减按钮', + component: 'Checkbox' + }, + { + name: 'keyboard', + label: '是否启用键盘快捷行为', + component: 'Checkbox' + }, + { + name: 'stringMode', + label: '字符值模式', + component: 'Checkbox' + }, + { + name: 'bordered', + label: '是否有边框', + component: 'Checkbox' + } + ], + InputTextArea: [ + { + name: 'defaultValue', + label: '默认值', + component: 'Input', + componentProps: { + type: 'text', + placeholder: '请输入默认值' + } + }, + { + name: 'maxlength', + label: '最大长度', + component: 'InputNumber', + componentProps: { + placeholder: '请输入最大长度' + } + }, + { + name: 'minlength', + label: '最小长度', + component: 'InputNumber', + componentProps: { + placeholder: '请输入最小长度' + } + }, + { + name: 'cols', + label: '可见列数', + component: 'InputNumber', + componentProps: { + placeholder: '请输入可见列数', + min: 0 + } + }, + { + name: 'rows', + label: '可见行数', + component: 'InputNumber', + componentProps: { + placeholder: '请输入可见行数', + min: 0 + } + }, + { + name: 'minlength', + label: '最小长度', + component: 'InputNumber', + componentProps: { + placeholder: '请输入最小长度' + } + }, + { + name: 'autosize', + label: '自适应内容高度', + component: 'Checkbox' + }, + { + name: 'showCount', + label: '是否展示字数', + component: 'Checkbox' + }, + { + name: 'readonly', + label: '是否只读', + component: 'Checkbox' + }, + { + name: 'spellcheck', + label: '读写检查', + component: 'Checkbox' + }, + { + name: 'autocomplete', + label: '是否自动完成', + component: 'RadioGroup', + componentProps: { + options: [ + { label: '正常', value: null }, + { label: '开', value: 'on' }, + { label: '关', value: 'off' } + ] + } + }, + { + name: 'autocorrect', + label: '是否自动纠错', + component: 'RadioGroup', + componentProps: { + options: [ + { label: '正常', value: null }, + { label: '开', value: 'on' }, + { label: '关', value: 'off' } + ] + } + } + ], + Select: [ + { + name: 'mode', + label: '选择模式(默认单选)', + component: 'RadioGroup', + componentProps: { + options: [ + { label: '单选', value: null }, + { label: '多选', value: 'multiple' }, + { label: '标签', value: 'tags' } + // { label: '组合', value: 'combobox' }, + ] + } + }, + { + name: 'autoClearSearchValue', + label: '是否在选中项后清空搜索框', + component: 'Checkbox' + }, + { + name: 'labelInValue', + label: '选项的label包装到value中', + component: 'Checkbox' + }, + { + name: 'showArrow', + label: '显示下拉小箭头', + component: 'Checkbox' + }, + { + name: 'defaultOpen', + label: '默认展开下拉菜单', + component: 'Checkbox' + } + ], + Checkbox: [ + { + name: 'indeterminate', + label: '设置indeterminate状态', + component: 'Checkbox' + } + ], + CheckboxGroup: [], + RadioGroup: [ + { + name: 'defaultValue', + label: '默认值', + component: 'Input', + componentProps: { + placeholder: '请输入默认值' + } + }, + { + name: 'buttonStyle', + label: 'RadioButton的风格样式', + component: 'RadioGroup', + componentProps: { + options: [ + { + label: 'outline', + value: 'outline' + }, + { + label: 'solid', + value: 'solid' + } + ] + } + }, + { + name: 'optionType', + label: 'options类型', + component: 'RadioGroup', + componentProps: { + options: [ + { + label: '默认', + value: 'default' + }, + { + label: '按钮', + value: 'button' + } + ] + //根据其它选项的值更新自身控件配置值 + //compProp当前组件的属性, + //configProps,当且组件的所有配置选项 + //self,当前配置的componentProps属性 + //返回真值进行更新 + // _propsFunc: (compProp, configProps, self) => { + // console.log("i'm called"); + // console.log(compProp, configProps, self); + // if (compProp['buttonStyle'] && compProp['buttonStyle'] == 'outline') { + // if (!self['disabled']) { + // self['disabled'] = true; + // return 1; + // } + // } else { + // if (self['disabled']) { + // self['disabled'] = false; + // return 1; + // } + // } + + // // return prop.optionType == 'button'; + // }, + } + }, + { + name: 'size', + label: '尺寸', + component: 'RadioGroup', + componentProps: { + options: [ + { + label: '默认', + value: 'default' + }, + { + label: '大', + value: 'large' + }, + { + label: '小', + value: 'small' + } + ] + } + } + ], + DatePicker: [ + { + name: 'format', + label: '展示格式(format)', + component: 'Input', + componentProps: { + placeholder: 'YYYY-MM-DD' + } + }, + { + name: 'valueFormat', + label: '绑定值格式(valueFormat)', + component: 'Input', + componentProps: { + placeholder: 'YYYY-MM-DD' + } + } + ], + RangePicker: [ + { + name: 'placeholder', + label: '占位符', + children: [ + { + name: '', + label: '', + component: 'Input' + }, + { + name: '', + label: '', + component: 'Input' + } + ] + }, + { + name: 'format', + label: '展示格式(format)', + component: 'Input', + componentProps: { + placeholder: 'YYYY-MM-DD HH:mm:ss' + } + }, + { + name: 'valueFormat', + label: '绑定值格式(valueFormat)', + component: 'Input', + componentProps: { + placeholder: 'YYYY-MM-DD' + } + } + ], + MonthPicker: [ + { + name: 'format', + label: '展示格式(format)', + component: 'Input', + componentProps: { + placeholder: 'YYYY-MM' + } + }, + { + name: 'valueFormat', + label: '绑定值格式(valueFormat)', + component: 'Input', + componentProps: { + placeholder: 'YYYY-MM' + } + } + ], + TimePicker: [ + { + name: 'format', + label: '展示格式(format)', + component: 'Input', + componentProps: { + placeholder: 'YYYY-MM' + } + }, + { + name: 'valueFormat', + label: '绑定值格式(valueFormat)', + component: 'Input', + componentProps: { + placeholder: 'YYYY-MM' + } + } + ], + Slider: [ + { + name: 'defaultValue', + label: '默认值', + component: 'InputNumber', + componentProps: { + placeholder: '请输入默认值' + } + }, + { + name: 'min', + label: '最小值', + component: 'InputNumber', + componentProps: { + placeholder: '请输入最小值' + } + }, + { + name: 'max', + label: '最大值', + component: 'InputNumber', + componentProps: { + placeholder: '请输入最大值' + } + }, + { + name: 'step', + label: '步长', + component: 'InputNumber', + componentProps: { + placeholder: '请输入步长' + } + }, + { + name: 'tooltipPlacement', + label: 'Tooltip 展示位置', + component: 'Select', + componentProps: { + options: [ + { value: 'top', label: '上' }, + { value: 'left', label: '左' }, + { value: 'right', label: '右' }, + { value: 'bottom', label: '下' }, + { value: 'topLeft', label: '上右' }, + { value: 'topRight', label: '上左' }, + { value: 'bottomLeft', label: '右下' }, + { value: 'bottomRight', label: '左下' }, + { value: 'leftTop', label: '左下' }, + { value: 'leftBottom', label: '左上' }, + { value: 'rightTop', label: '右下' }, + { value: 'rightBottom', label: '右上' } + ] + } + }, + { + name: 'tooltipVisible', + label: '始终显示Tooltip', + component: 'Checkbox' + }, + { + name: 'dots', + label: '只能拖拽到刻度上', + component: 'Checkbox' + }, + { + name: 'range', + label: '双滑块模式', + component: 'Checkbox' + }, + { + name: 'reverse', + label: '反向坐标轴', + component: 'Checkbox' + }, + { + name: 'vertical', + label: '垂直方向', + component: 'Checkbox' + }, + { + name: 'included', + label: '值为包含关系', + component: 'Checkbox' + } + ], + Rate: [ + { + name: 'defaultValue', + label: '默认值', + component: 'InputNumber', + componentProps: { + placeholder: '请输入默认值' + } + }, + { + name: 'character', + label: '自定义字符', + component: 'Input', + componentProps: { + placeholder: '请输入自定义字符' + } + }, + { + name: 'count', + label: 'start 总数', + component: 'InputNumber', + componentProps: { + placeholder: '请输入自定义字符' + } + } + ], + Switch: [ + { + name: 'checkedChildren', + label: '选中时的内容', + component: 'Input', + componentProps: { + placeholder: '请输入选中时的内容' + } + }, + { + name: 'checkedValue', + label: '选中时的值', + component: 'Input', + componentProps: { + placeholder: '请输入选中时的值' + } + }, + { + name: 'unCheckedChildren', + label: '非选中时的内容', + component: 'Input', + componentProps: { + placeholder: '请输入非选中时的内容' + } + }, + { + name: 'unCheckedValue', + label: '非选中时的值', + component: 'Input', + componentProps: { + placeholder: '请输入非选中时的值' + } + }, + { + name: 'loading', + label: '加载中的开关', + component: 'Checkbox' + }, + { + name: 'size', + label: '尺寸', + component: 'RadioGroup', + componentProps: { + options: [ + { + label: '默认', + value: 'default' + }, + { + label: '小', + value: 'small' + } + ] + } + } + ], + TreeSelect: [ + { + name: 'defaultValue', + label: '默认值', + component: 'Input', + componentProps: { + placeholder: '请输入默认值' + } + }, + { + name: 'searchPlaceholder', + label: '搜索框默认文字', + component: 'Input', + componentProps: { + placeholder: '请输入搜索框默认文字' + } + }, + { + name: 'treeNodeFilterProp', + label: '输入项过滤对应的 treeNode 属性', + component: 'Input', + componentProps: { + defaultValue: 'value' + } + }, + { + name: 'treeNodeLabelProp', + label: '作为显示的 prop 设置', + component: 'Input', + componentProps: { + defaultValue: 'title' + } + }, + { + name: 'dropdownClassName', + label: '下拉菜单的 className 属性', + component: 'Input', + componentProps: { + placeholder: '请输入下拉菜单的 className 属性' + } + }, + + { + name: 'labelInValue', + label: '选项的label包装到value中', + component: 'Checkbox' + }, + { + name: 'treeIcon', + label: '展示TreeNode title前的图标', + component: 'Checkbox' + }, + { + name: 'treeCheckable', + label: '选项可勾选', + component: 'Checkbox' + }, + { + name: 'treeCheckStrictly', + label: '节点选择完全受控', + component: 'Checkbox' + }, + { + name: 'treeDefaultExpandAll', + label: '默认展开所有', + component: 'Checkbox' + }, + { + name: 'treeLine', + label: '是否展示线条样式', + component: 'Checkbox' + }, + { + name: 'maxTagCount', + label: '最多显示多少个 tag', + component: 'InputNumber', + componentProps: { + placeholder: '最多显示多少个 tag' + } + }, + { + name: 'size', + label: '尺寸', + component: 'RadioGroup', + componentProps: { + options: [ + { + label: '默认', + value: 'default' + }, + { + label: '小', + value: 'small' + } + ] + } + } + ], + Cascader: [ + { + name: 'expandTrigger', + label: '次级展开方式(默认click)', + component: 'RadioGroup', + componentProps: { + options: [ + { + label: 'click', + value: 'click' + }, + { + label: 'hover', + value: 'hover' + } + ] + } + } + ], + Button: [ + { + name: 'type', + label: '类型', + component: 'RadioGroup', + componentProps: { + options: [ + { + label: 'default', + value: 'default' + }, + { + label: 'primary', + value: 'primary' + }, + { + label: 'danger', + value: 'danger' + }, + { + label: 'dashed', + value: 'dashed' + } + ] + } + }, + { + name: 'handle', + label: '操作', + component: 'RadioGroup', + componentProps: { + options: [ + { + label: '提交', + value: 'submit' + }, + { + label: '重置', + value: 'reset' + } + ] + } + } + ], + Upload: [ + { + name: 'action', + label: '上传地址', + component: 'Input' + }, + { + name: 'name', + label: '附件参数名(name)', + component: 'Input' + } + ], + // ColorPicker: [ + // { + // name: 'defaultValue', + // label: '默认值', + // component: 'AColorPicker', + // }, + // ], + slot: [ + { + name: 'slotName', + label: '插槽名称', + component: 'Input' + } + ], + Transfer: [ + // { + // name: 'operations', + // label: '操作文案集合,顺序从上至下', + // component: 'Input', + // componentProps: { + // type: 'text', + // // defaultValue: ['>', '<'], + // }, + // }, + // { + // name: 'titles', + // label: '标题集合,顺序从左至右', + // component: 'Input', + // componentProps: { + // type: 'text', + // // defaultValue: ['', ''], + // }, + // }, + { + name: 'oneWay', + label: '展示为单向样式', + component: 'Checkbox' + }, + { + name: 'pagination', + label: '使用分页样式', + component: 'Checkbox' + }, + { + name: 'showSelectAll', + label: '展示全选勾选框', + component: 'Checkbox' + } + ] +} + +function deleteProps(list: Omit<IBaseFormAttrs, 'tag'>[], key: string) { + list.forEach((element, index) => { + if (element.name == key) { + list.splice(index, 1) + } + }) +} + +componentAttrs['StrengthMeter'] = componentAttrs['Input'] +componentAttrs['StrengthMeter'].push({ + name: 'visibilityToggle', + label: '是否显示切换按钮', + component: 'Checkbox' +}) + +deleteProps(componentAttrs['StrengthMeter'], 'type') +deleteProps(componentAttrs['StrengthMeter'], 'prefix') +deleteProps(componentAttrs['StrengthMeter'], 'defaultValue') +deleteProps(componentAttrs['StrengthMeter'], 'suffix') +//组件属性 +// name 控件的属性 +export const baseComponentAttrs: IBaseComponentProps = componentAttrs + +//在所有的选项中查找需要配置项 +const findCompoentProps = (props, name) => { + const idx = props.findIndex((value: BaseFormAttrs, _index) => { + return value.name == name + }) + if (idx) { + if (props[idx].componentProps) { + return props[idx].componentProps + } + } +} + +// 根据其它选项的值更新自身控件配置值 +export const componentPropsFuncs = { + RadioGroup: (compProp, options: BaseFormAttrs[]) => { + const props = findCompoentProps(options, 'size') + if (props) { + if (compProp['optionType'] && compProp['optionType'] != 'button') { + props['disabled'] = true + compProp['size'] = null + } else { + props['disabled'] = false + } + } + } +} diff --git a/src/components/FormDesign/src/components/VFormDesign/config/formItemPropsConfig.ts b/src/components/FormDesign/src/components/VFormDesign/config/formItemPropsConfig.ts new file mode 100644 index 00000000..bee2a9c6 --- /dev/null +++ b/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'] + } +] diff --git a/src/components/FormDesign/src/components/VFormDesign/index.vue b/src/components/FormDesign/src/components/VFormDesign/index.vue new file mode 100644 index 00000000..61cdf786 --- /dev/null +++ b/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.cloneDeep重新拷贝数据,把currentItem指向选中项 + 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> diff --git a/src/components/FormDesign/src/components/VFormDesign/modules/CollapseItem.vue b/src/components/FormDesign/src/components/VFormDesign/modules/CollapseItem.vue new file mode 100644 index 00000000..a785ff27 --- /dev/null +++ b/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> diff --git a/src/components/FormDesign/src/components/VFormDesign/modules/FormComponentPanel.vue b/src/components/FormDesign/src/components/VFormDesign/modules/FormComponentPanel.vue new file mode 100644 index 00000000..0ab94b38 --- /dev/null +++ b/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 + + // 计算布局元素,水平模式下为ACol,非水平模式下为div + 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> diff --git a/src/components/FormDesign/src/components/VFormDesign/modules/PropsPanel.vue b/src/components/FormDesign/src/components/VFormDesign/modules/PropsPanel.vue new file mode 100644 index 00000000..7e2b8fb1 --- /dev/null +++ b/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> diff --git a/src/components/FormDesign/src/components/VFormDesign/modules/Toolbar.vue b/src/components/FormDesign/src/components/VFormDesign/modules/Toolbar.vue new file mode 100644 index 00000000..b48704b6 --- /dev/null +++ b/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> diff --git a/src/components/FormDesign/src/components/VFormDesign/styles/drag.less b/src/components/FormDesign/src/components/VFormDesign/styles/drag.less new file mode 100644 index 00000000..e8fb00a4 --- /dev/null +++ b/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; + } + } + } + } +} diff --git a/src/components/FormDesign/src/components/VFormDesign/styles/variable.less b/src/components/FormDesign/src/components/VFormDesign/styles/variable.less new file mode 100644 index 00000000..8749dce1 --- /dev/null +++ b/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; diff --git a/src/components/FormDesign/src/components/VFormItem/index.vue b/src/components/FormDesign/src/components/VFormItem/index.vue new file mode 100644 index 00000000..d6ea5361 --- /dev/null +++ b/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异步加载会导致渲染不出来,故而此处只处理options,treeData,同步属性在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; +} + +// form字段中的标签有ant-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> diff --git a/src/components/FormDesign/src/components/VFormItem/vFormItem.vue b/src/components/FormDesign/src/components/VFormItem/vFormItem.vue new file mode 100644 index 00000000..24a17609 --- /dev/null +++ b/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> diff --git a/src/components/FormDesign/src/components/VFormPreview/index.vue b/src/components/FormDesign/src/components/VFormPreview/index.vue new file mode 100644 index 00000000..5cfee306 --- /dev/null +++ b/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> diff --git a/src/components/FormDesign/src/components/VFormPreview/useForm.vue b/src/components/FormDesign/src/components/VFormPreview/useForm.vue new file mode 100644 index 00000000..d50e8107 --- /dev/null +++ b/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> diff --git a/src/components/FormDesign/src/components/index.ts b/src/components/FormDesign/src/components/index.ts new file mode 100644 index 00000000..e8f98aa1 --- /dev/null +++ b/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 } diff --git a/src/components/FormDesign/src/core/formItemConfig.ts b/src/components/FormDesign/src/core/formItemConfig.ts new file mode 100644 index 00000000..8871e116 --- /dev/null +++ b/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 + } + } +] diff --git a/src/components/FormDesign/src/core/iconConfig.ts b/src/components/FormDesign/src/core/iconConfig.ts new file mode 100644 index 00000000..19a84e74 --- /dev/null +++ b/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 diff --git a/src/components/FormDesign/src/hooks/useFormDesignState.ts b/src/components/FormDesign/src/hooks/useFormDesignState.ts new file mode 100644 index 00000000..8e0cdc6f --- /dev/null +++ b/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 } +} diff --git a/src/components/FormDesign/src/hooks/useFormInstanceMethods.ts b/src/components/FormDesign/src/hooks/useFormInstanceMethods.ts new file mode 100644 index 00000000..fa2d638f --- /dev/null +++ b/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 + } +} diff --git a/src/components/FormDesign/src/hooks/useVFormMethods.ts b/src/components/FormDesign/src/hooks/useVFormMethods.ts new file mode 100644 index 00000000..62ef621b --- /dev/null +++ b/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 + } +} diff --git a/src/components/FormDesign/src/typings/base-type.ts b/src/components/FormDesign/src/typings/base-type.ts new file mode 100644 index 00000000..a8a41aa6 --- /dev/null +++ b/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 + } +} diff --git a/src/components/FormDesign/src/typings/form-type.ts b/src/components/FormDesign/src/typings/form-type.ts new file mode 100644 index 00000000..6e9bf505 --- /dev/null +++ b/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 +} diff --git a/src/components/FormDesign/src/typings/v-form-component.ts b/src/components/FormDesign/src/typings/v-form-component.ts new file mode 100644 index 00000000..b67712ae --- /dev/null +++ b/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 +} diff --git a/src/components/FormDesign/src/utils/index.ts b/src/components/FormDesign/src/utils/index.ts new file mode 100644 index 00000000..859a4f87 --- /dev/null +++ b/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 + } +} diff --git a/src/components/FormDesign/src/utils/message.ts b/src/components/FormDesign/src/utils/message.ts new file mode 100644 index 00000000..b32b953e --- /dev/null +++ b/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 diff --git a/src/views/infra/build/index.vue b/src/views/infra/build/index.vue index 3b64cfc4..6046e0c6 100644 --- a/src/views/infra/build/index.vue +++ b/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>