feat: 增加表单设计器 (#2533)

This commit is contained in:
wwsheng009
2023-02-10 07:43:27 +08:00
committed by GitHub
parent 4c0f2038af
commit c5b39f2c16
47 changed files with 7840 additions and 2 deletions

View File

@@ -73,7 +73,8 @@
"vxe-table": "^4.3.9",
"vxe-table-plugin-export-xlsx": "^3.0.4",
"xe-utils": "^3.5.7",
"xlsx": "^0.18.5"
"xlsx": "^0.18.5",
"vuedraggable": "^4.1.0"
},
"devDependencies": {
"@commitlint/cli": "^16.2.3",

View File

@@ -0,0 +1,34 @@
import type { AppRouteModule } from '/@/router/types';
import { LAYOUT } from '/@/router/constant';
const permission: AppRouteModule = {
path: '/form-designer',
name: 'Form-designer',
component: LAYOUT,
meta: {
orderNo: 10000,
icon: 'icon:add-circle',
title: '表单设计',
},
children: [
{
path: 'design',
name: 'Design',
meta: {
title: '表单设计',
},
component: () => import('/@/views/form-design/index.vue'),
},
{
path: 'example1',
name: 'Example1',
meta: {
title: '示例',
},
component: () => import('/@/views/form-design/examples/baseForm.vue'),
},
],
};
export default permission;

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,81 @@
<template>
<!-- <component :is="layoutTag" v-bind="schema.colProps"> -->
<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>
<!-- </component> -->
</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>

View File

@@ -0,0 +1,153 @@
<!--
* @Author: ypt
* @Date: 2021/11/29
* @Description: 表单渲染器根据json生成表单
-->
<template>
<div class="v-form-container">
<Form class="v-form-model" ref="eFormModel" :model="formModel" v-bind="formModelProps">
<Row>
<!-- <component :is="wrapperComp"> -->
<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>
<!-- </component> -->
</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) => {
// console.log('handleChange', formItem, field, value);
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>

View File

@@ -0,0 +1,81 @@
<!--
* @Author: ypt
* @Date: 2021/12/7
* @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>

View File

@@ -0,0 +1,245 @@
<!--
* @Author: ypt
* @Date: 2021/11/26
* @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>
<!-- </Row> -->
<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 dummyUpdate = ref(0);
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>

View File

@@ -0,0 +1,66 @@
<!--
* @Author: ypt
* @Date: 2021/11/24
* @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>

View File

@@ -0,0 +1,148 @@
<!--
* @Author: ypt
* @Date: 2021/11/24
* @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(() => {
// console.log('const list2 = computed(() => {');
return baseFormItemControlAttrs.filter((item) => {
return showProps(item.exclude);
});
});
return {
baseFormItemProps,
advanceFormItemProps,
advanceFormItemColProps,
formConfig,
controlPropsList,
showProps,
};
},
});
</script>

View File

@@ -0,0 +1,55 @@
<!--
* @Author: ypt
* @Date: 2021/11/19
* @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>

View File

@@ -0,0 +1,76 @@
<!--
* @Author: ypt
* @Date: 2021/11/11
* @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>

View File

@@ -0,0 +1,114 @@
<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)">
<!-- <a-icon type="delete" /> -->
<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)">
<!-- <a-icon type="delete" /> -->
<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>

View File

@@ -0,0 +1,117 @@
<!--
* @Author: ypt
* @Date: 2021/11/23
* @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,
// RadioButtonGroup,
RadioGroup: Radio.Group,
RadioButton: Radio.Button,
Form,
FormItem,
Col,
},
setup() {
// const labelColspan = computed(()=>)
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>

View File

@@ -0,0 +1,139 @@
<!--
* @Author: ypt
* @Date: 2021/12/7
* @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 style="height: 100%" ref="myEditor" v-model="json"></CodeEditor> -->
<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
console.log(state.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>

View File

@@ -0,0 +1,66 @@
<!--
* @Author: ypt
* @Date: 2021/11/23
* @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>

View File

@@ -0,0 +1,135 @@
<!--
* @Author: ypt
* @Date: 2021/11/19
* @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"
>
<!-- <div class="draggable-box"> -->
<!-- <div class="list-main"> -->
<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)"
>
<!-- <transition-group tag="div" name="list" class="list-main"> -->
<template #item="{ element }">
<LayoutItem
class="drag-move"
:schema="element"
:current-item="currentItem"
@handle-copy="$emit('handle-copy')"
@handle-delete="$emit('handle-delete')"
/>
</template>
<!-- </transition-group> -->
</draggable>
<!-- </div> -->
<!-- </div> -->
</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);
//opacity: 0.5;
}
</style>

View File

@@ -0,0 +1,97 @@
<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>

View File

@@ -0,0 +1,295 @@
<!--
* @Author: ypt
* @Date: 2021/11/25
* @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:
'/^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][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:
'/^(([^<>()[\\]\\\\.,;:\\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:
'/^(?:[\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>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,351 @@
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'],
},
];

View File

@@ -0,0 +1,365 @@
<template>
<!-- <div class="v-form-design-container"> -->
<!-- <header class="v-form-design-header">{{ title }}</header> -->
<Layout>
<LayoutSider
class="left"
theme="light"
collapsible
collapsedWidth="0"
width="270"
:zeroWidthTriggerStyle="{ 'margin-top': '-70px' }"
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"
collapsible
:reverseArrow="true"
theme="light"
collapsedWidth="0"
width="270"
:zeroWidthTriggerStyle="{ 'margin-top': '-70px' }"
breakpoint="lg"
>
<!-- <div class="right" onselectstart="return false"> -->
<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>
<!-- </div> -->
</LayoutSider>
</Layout>
<JsonModal ref="jsonModal" />
<CodeModal ref="codeModal" />
<ImportJsonModal ref="importJsonModal" />
<VFormPreview ref="eFormPreview" :formConfig="formConfig" />
<VFormPreview2 ref="eFormPreview2" :formConfig="formConfig" />
<!-- </div> -->
</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 zhCN from 'ant-design-vue/lib/locale-provider/zh_CN';
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 { IAnyObject } from '../../typings/base-type';
import { globalConfigState } from './config/formItemPropsConfig';
import { IFormDesignMethods, IPropsPanel, IToolbarMethods } from '../../typings/form-type';
import { CollapseContainer } from '/@/components/Container/index';
defineProps({
title: {
type: String,
default: 'v-form-antd表单设计器',
},
});
// 子组件实例
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 _state = reactive<IState>({
// locale: zhCN, // 国际化
// baseComponents, // 基础控件列表
// layoutComponents, // 布局组件列表
// customComponents,
// propsPanel,
// jsonModal,
// eFormPreview,
// eFormPreview2,
// importJsonModal,
// codeModal,
// });
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 item = schemas[index];
// setGlobalConfigState(item);
// generateKey(item);
// handleListPush(item);
};
const handleListPushDrag = (item: IVFormComponent) => {
const formItem = cloneDeep(item);
setGlobalConfigState(formItem);
generateKey(formItem);
// if (!formConfig.value.currentItem?.key) {
// formConfig.value.schemas.push(formItem);
// handleSetSelectItem(formItem);
// return formItem;
// }
// handleCopy(formItem, false);
// handleCopy(formItem, false);
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>
// @import url(./styles/variable.less);
</style>

View File

@@ -0,0 +1,106 @@
<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>

View File

@@ -0,0 +1,169 @@
<!--
* @Author: ypt
* @Date: 2021/11/18
* @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;
top: -10%;
left: 0;
right: 0;
bottom: 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>

View File

@@ -0,0 +1,100 @@
<!--
* @Author: ypt
* @Date: 2021/11/19
* @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>

View File

@@ -0,0 +1,141 @@
<!--
* @Author: ypt
* @Date: 2021/11/23
* @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">
<!-- <a-icon :type="item.icon" /> -->
<Icon :icon="item.icon" />
</a>
</Tooltip>
<Divider type="vertical" />
<Tooltip title="撤销">
<a :class="{ disabled: !canUndo }" :disabled="!canUndo" @click="undo">
<!-- <a-icon type="undo" /> -->
<Icon icon="ant-design:undo-outlined" />
</a>
</Tooltip>
<Tooltip title="重做">
<a :class="{ disabled: !canRedo }" :disabled="!canRedo" @click="redo">
<!-- <a-icon type="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: '预览2',
type: 'preview',
event: 'handlePreview2',
icon: 'ant-design:chrome-filled',
},
{
title: '导入',
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>

View File

@@ -0,0 +1,231 @@
.draggable-box {
height: 100%;
overflow: auto;
:deep(.list-main) {
overflow: hidden;
min-height: 100%;
padding: 5px;
position: relative;
background: #fafafa;
// border : 1px #ccc dashed;
.moving {
// 拖放移动中
// outline-width: 0;
min-height: 35px;
box-sizing: border-box;
overflow: hidden;
padding: 0 !important;
// margin : 3px 0;
position: relative;
&::before {
content: '';
height: 5px;
width: 100%;
background: @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: @primary-hover-bg-color;
}
// 选择时 start
&::before {
content: '';
height: 5px;
width: 100%;
background: @primary-color;
position: absolute;
top: 0;
right: -100%;
transition: all 0.3s;
}
&.active {
background: @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;
// z-index: 888;
}
.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: @primary-color;
}
.delete {
right: 0;
background: @primary-color;
}
}
.grid-box {
position: relative;
box-sizing: border-box;
padding: 5px;
background: @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: @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: @layout-hover-bg-color;
outline-offset: 0;
&::before {
background: @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: @layout-color;
}
> .delete {
right: 0;
background: @layout-color;
}
}
}
}
}

View File

@@ -0,0 +1,522 @@
.v-form-design-container {
// height: 100%;
width: 100%;
// overflow: hidden;
display: flex;
flex-direction: column;
& > .v-form-design-header {
height: @header-height;
line-height: @header-height;
background: @primary-color;
text-align: center;
font-size: 20px;
color: #fff;
}
:deep(.icon) {
width: 1em;
height: 1em;
vertical-align: -0.15em;
fill: currentcolor;
// overflow: hidden;
}
.content {
position: relative;
flex: 1;
// margin-top: 5px;
display: flex;
// flex-flow: row nowrap;
// height: 100%;
// overflow: hidden;
box-sizing: border-box;
.left,
.right {
width: @left-right-width;
box-shadow: 0 0 1px 1px #ccc;
// overflow: hidden;
// height: 100%;
// height: 600px;
//
border: 1px solid green;
// overflow-y: auto;
// :deep(.ant-tabs) {
// height: 100%;
// .ant-tabs-content-holder {
// // display: flex;
// // flex-flow: column;
// height: 100%;
// .ant-tabs-content {
// // flex:1;
// // height: 0;
// // overflow-y: auto;
// // overflow-x: hidden;
// display: flex;
// flex-flow: column;
// height: 100%;
// }
// }
// }
}
:deep(.right) {
// & > div {
// height: 100%;
// }
// overflow: hidden;
// & > div {
// height: 100%;
// .ant-tabs-content-holder{
// height: 100%;
// .ant-tabs-content{
// height: 100%;
// .ant-tabs-tabpane{
// height: 100%;
// }
// }
// }
// }
.ant-tabs {
width: 280px;
height: 100%;
// overflow: hidden;
.ant-tabs-content-holder {
// display: flex;
// flex-flow: column;
// height: 100%;
// overflow: hidden;
.ant-tabs-content {
// flex:1;
// height: 0;
// overflow-y: auto;
// overflow-x: hidden;
height: 100%;
// overflow: hidden;
.ant-tabs-tabpane {
.properties-content {
// height: 100%;
// overflow: auto;
// overflow: hidden;
// background: #fff;
.properties-body {
box-sizing: border-box;
// height: 100%;
// display: flex;
// flex-flow: column;
form {
position: absolute;
// height: 400px;
height: calc(100% - 50px);
// height: 100%;
// flex: 1;
// height: 0;
margin-right: 10px;
// overflow: auto;
overflow-y: auto;
overflow-x: hidden;
// overflow: auto;
}
// overflow: auto;
// height: 100%;
// padding: 8px 16px;
.hint-box {
margin-top: 200px;
}
.ant-form-item,
.ant-slider-with-marks {
margin-left: 10px;
margin-right: 20px;
margin-bottom: 0;
}
.ant-form-item {
// box-sizing: border-box;
width: 100%;
margin-bottom: 0;
// padding: 2px 0;
border-bottom: 1px solid @border-color;
.ant-form-item-label {
line-height: 2;
vertical-align: text-top;
}
}
}
}
}
}
}
}
}
:deep(.left) {
.ant-collapse {
border: 0;
.ant-collapse-header {
padding: 7px 0 7px 40px;
}
.ant-collapse-content-box {
padding: 0;
}
}
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;
}
}
}
}
:deep(.node-panel) {
box-shadow: 0 0 1px 1px #ccc;
flex: 1;
margin: 0 8px;
overflow: hidden;
.draggable-box {
height: 100%;
overflow: auto;
.list-main {
overflow: hidden;
min-height: 100%;
padding: 5px;
position: relative;
background: #fafafa;
// border : 1px #ccc dashed;
.moving {
// 拖放移动中
// outline-width: 0;
min-height: 35px;
box-sizing: border-box;
overflow: hidden;
padding: 0 !important;
// margin : 3px 0;
position: relative;
&::before {
content: '';
height: 5px;
width: 100%;
background: @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: @primary-hover-bg-color;
}
// 选择时 start
&::before {
content: '';
height: 5px;
width: 100%;
background: @primary-color;
position: absolute;
top: 0;
right: -100%;
transition: all 0.3s;
}
&.active {
background: @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;
// z-index: 888;
}
.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: @primary-color;
}
.delete {
right: 0;
background: @primary-color;
}
}
.grid-box {
position: relative;
box-sizing: border-box;
padding: 5px;
background: @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: @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: @layout-hover-bg-color;
outline-offset: 0;
&::before {
background: @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: @layout-color;
}
> .delete {
right: 0;
background: @layout-color;
}
}
}
}
}
}
}
::-webkit-scrollbar {
/* 滚动条整体样式 */
width: 6px;
/* 高宽分别对应横竖滚动条的尺寸 */
height: 6px;
scrollbar-arrow-color: red;
}
::-webkit-scrollbar-thumb {
/* 滚动条里面小方块 */
border-radius: 5px;
box-shadow: inset 0 0 5px rgb(0 0 0 / 20%);
background: rgb(0 0 0 / 20%);
scrollbar-arrow-color: red;
}
::-webkit-scrollbar-track {
/* 滚动条里面轨道 */
box-shadow: inset 0 0 5px rgb(0 0 0 / 20%);
border-radius: 0;
background: rgb(0 0 0 / 10%);
}
}
// code盒子样式
.v-json-box {
height: 570px;
overflow: auto;
.vue-codemirror-wrap {
height: 100%;
.CodeMirror-wrap {
height: 100%;
background: #f6f6f6;
.CodeMirror-scroll {
height: 100%;
width: 100%;
}
pre.CodeMirror-line,
.CodeMirror-linenumber {
min-height: 21px;
line-height: 21px;
}
}
}
}
// code-modal盒子样式
.v-code-modal {
.ant-modal-body {
padding: 12px;
}
}
.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;
}
}
}

View File

@@ -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;

View File

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

View File

@@ -0,0 +1,79 @@
<!--
* @Author: ypt
* @Date: 2021/11/19
* @Description:
`<FormItem`
:tableAction="tableAction"
:formActionType="formActionType"
:schema="schema2"
:formProps="getProps"
:allDefaultValues="defaultValueRef"
:formModel="formModel"
:setFormModel="setFormModel"
>
<FormItem
:tableAction="tableAction"
:formActionType="formActionType"
:schema="schemaNew"
:formProps="getProps"
:allDefaultValues="defaultValueRef"
:formModel="formModel"
>
-->
<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>

View File

@@ -0,0 +1,108 @@
<!--
* @Author: ypt
* @Date: 2021/11/29
* @Description: 渲染组件无法使用Vben的组件
-->
<template>
<Modal
title="预览(标准Ant控件)"
: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, toRaw, 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 = () => {
// console.log('handleCancel');
state.visible = false;
state.formModel = {};
};
const handleGetData = async () => {
// console.log('handleGetData');
console.log(toRaw(state.formModel));
const _data = await state.fApi.submit?.();
// console.log('handleGetData', 'end');
jsonModal.value?.showModal?.(_data);
// jsonModal.value?.showModal?.(toRaw(state.formModel));
};
const onSubmit = (_data: IAnyObject) => {
// console.log('-> data', data);
};
const onCancel = () => {
state.formModel = {};
};
return {
handleGetData,
handleCancel,
...toRefs(state),
showModal,
jsonModal,
onSubmit,
onCancel,
};
},
});
</script>

View File

@@ -0,0 +1,74 @@
<!--
* @Author: ypt
* @Date: 2021/11/29
* @Description: 使用vbenForm的功能进行渲染
-->
<template>
<Modal
title="预览(VbenForm)"
: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();
console.log(data);
jsonModal.value?.showModal?.(data);
};
defineExpose({ showModal });
</script>

View File

@@ -0,0 +1,71 @@
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('ColorPicker', 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 };

View File

@@ -0,0 +1,423 @@
/**
* @name: formItemConfig
* @author: ypt
* @date: 2021/11/18 16:25
* @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,
},
},
];

View File

@@ -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;

View File

@@ -0,0 +1,37 @@
<template>
<BasicForm @register="register" />
</template>
<script lang="ts" setup>
import { BasicForm, FormSchema, useForm } from '/@/components/Form/index';
const schemas: FormSchema[] = [
{
field: 'field1',
component: 'Input',
label: '字段1',
span: 8,
// colProps: {
// span: 8,
// },
componentProps: {
placeholder: '自定义placeholder',
onChange: (e: any) => {
console.log(e);
},
},
},
{
field: 'field2',
component: 'Input',
label: '字段2',
span: 8,
// colProps: {
// span: 8,
// },
},
];
const [register] = useForm({
schemas,
});
</script>

View File

@@ -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 };
}

View File

@@ -0,0 +1,62 @@
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,
};
}

View File

@@ -0,0 +1,195 @@
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,
};
}

View File

@@ -0,0 +1,9 @@
<template>
<VFormDesign />
</template>
<script lang="ts" setup>
import VFormDesign from './components/VFormDesign/index.vue';
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,48 @@
{
"schemas": [{
"field": "filename",
"component": "Input",
"label": "component.excel.fileName",
"rules": [{
"required": true
}]
},
{
"field": "bookType",
"component": "Select",
"label": "component.excel.fileType",
"defaultValue": "xlsx",
"rules": [{
"required": true
}],
"componentProps": {
"options": [{
"label": "xlsx",
"value": "xlsx",
"key": "xlsx"
},
{
"label": "html",
"value": "html",
"key": "html"
},
{
"label": "csv",
"value": "csv",
"key": "csv"
},
{
"label": "txt",
"value": "txt",
"key": "txt"
}
]
}
}
],
"layout": "horizontal",
"labelLayout": "flex",
"labelWidth": 100,
"labelCol": {},
"wrapperCol": {}
}

View File

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

View File

@@ -0,0 +1,52 @@
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;
}

View File

@@ -0,0 +1,349 @@
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;
}

View File

@@ -0,0 +1,200 @@
// 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;
}
};

View File

@@ -0,0 +1,21 @@
// import Vue from 'vue';
const message = Object.assign(
{
success: (msg: string) => {
console.log(msg);
},
error: (msg: string) => {
console.error(msg);
},
warning: (msg: string) => {
console.warn(msg);
},
info: (msg: string) => {
console.info(msg);
},
},
// Vue.prototype.$message,
);
export default message;

View File

@@ -9,7 +9,7 @@ module.exports = {
'selector-pseudo-class-no-unknown': [
true,
{
ignorePseudoClasses: ['global'],
ignorePseudoClasses: ['global', 'deep'],
},
],
'selector-pseudo-element-no-unknown': [