perf(form): improve the form function

This commit is contained in:
vben
2020-12-27 22:25:35 +08:00
parent 4ff1c408dc
commit ac1a369502
23 changed files with 344 additions and 100 deletions

View File

@@ -1,6 +1,6 @@
<template>
<Form v-bind="{ ...$attrs, ...$props }" ref="formElRef" :model="formModel">
<Row :class="getProps.compact ? 'compact-form-row' : ''" :style="getRowWrapStyle">
<Form v-bind="{ ...$attrs, ...$props }" :class="getFormClass" ref="formElRef" :model="formModel">
<Row :style="getRowWrapStyle">
<slot name="formHeader" />
<template v-for="schema in getSchema" :key="schema.field">
<FormItem
@@ -18,7 +18,6 @@
</FormItem>
</template>
<!-- -->
<FormAction
v-bind="{ ...getProps, ...advanceState }"
@toggle-advanced="handleToggleAdvanced"
@@ -46,8 +45,10 @@
import useAdvanced from './hooks/useAdvanced';
import { useFormEvents } from './hooks/useFormEvents';
import { createFormContext } from './hooks/useFormContext';
import { useAutoFocus } from './hooks/useAutoFocus';
import { basicProps } from './props';
import { useDesign } from '/@/hooks/web/useDesign';
export default defineComponent({
name: 'BasicForm',
@@ -71,6 +72,8 @@
const schemaRef = ref<Nullable<FormSchema[]>>(null);
const formElRef = ref<Nullable<FormActionType>>(null);
const { prefixCls } = useDesign('basic-form');
// Get the basic configuration of the form
const getProps = computed(
(): FormProps => {
@@ -78,6 +81,15 @@
}
);
const getFormClass = computed(() => {
return [
prefixCls,
{
[`${prefixCls}--compact`]: unref(getProps).compact,
},
];
});
// Get uniform row style
const getRowWrapStyle = computed(
(): CSSProperties => {
@@ -115,7 +127,7 @@
defaultValueRef,
});
const { transformDateFunc, fieldMapToTime } = toRefs(props);
const { transformDateFunc, fieldMapToTime, autoFocusFirstItem } = toRefs(props);
const { handleFormValues, initDefault } = useFormValues({
transformDateFuncRef: transformDateFunc,
@@ -125,6 +137,13 @@
formModel,
});
useAutoFocus({
getSchema,
autoFocusFirstItem,
isInitedDefault: isInitedDefaultRef,
formElRef: formElRef as Ref<FormActionType>,
});
const {
handleSubmit,
setFieldsValue,
@@ -217,8 +236,51 @@
getSchema,
formActionType,
setFormModel,
prefixCls,
getFormClass,
...formActionType,
};
},
});
</script>
<style lang="less">
@import (reference) '../../../design/index.less';
@prefix-cls: ~'@{namespace}-basic-form';
.@{prefix-cls} {
.ant-form-item {
&-label label::after {
margin: 0 6px 0 2px;
}
&-with-help {
margin-bottom: 0;
}
&:not(.ant-form-item-with-help) {
margin-bottom: 20px;
}
&.suffix-item {
.ant-form-item-children {
display: flex;
}
.suffix {
display: inline-block;
padding-left: 6px;
}
}
}
.ant-form-explain {
font-size: 14px;
}
&--compact {
.ant-form-item {
margin-bottom: 8px;
}
}
}
</style>

View File

@@ -19,6 +19,7 @@ import {
} from 'ant-design-vue';
import RadioButtonGroup from './components/RadioButtonGroup.vue';
import ApiSelect from './components/ApiSelect.vue';
import { BasicUpload } from '/@/components/Upload';
const componentMap = new Map<ComponentType, Component>();
@@ -32,6 +33,7 @@ componentMap.set('InputNumber', InputNumber);
componentMap.set('AutoComplete', AutoComplete);
componentMap.set('Select', Select);
componentMap.set('ApiSelect', ApiSelect);
// componentMap.set('SelectOptGroup', Select.OptGroup);
// componentMap.set('SelectOption', Select.Option);
componentMap.set('TreeSelect', TreeSelect);

View File

@@ -0,0 +1,89 @@
<template>
<Select v-bind="attrs" :options="options" v-model:value="state">
<template #[item]="data" v-for="item in Object.keys($slots)">
<slot :name="item" v-bind="data" />
</template>
<template #suffixIcon v-if="loading">
<LoadingOutlined spin />
</template>
<template #notFoundContent v-if="loading">
<span>
<LoadingOutlined spin class="mr-1" />
{{ t('component.form.apiSelectNotFound') }}
</span>
</template>
</Select>
</template>
<script lang="ts">
import { defineComponent, PropType, ref, watchEffect } from 'vue';
import { Select } from 'ant-design-vue';
import { isFunction } from '/@/utils/is';
import { useRuleFormItem } from '/@/hooks/component/useFormItem';
import { useAttrs } from '/@/hooks/core/useAttrs';
import { get } from 'lodash-es';
import { LoadingOutlined } from '@ant-design/icons-vue';
import { useI18n } from '/@/hooks/web/useI18n';
type OptionsItem = { label: string; value: string; disabled?: boolean };
export default defineComponent({
name: 'RadioButtonGroup',
components: {
Select,
LoadingOutlined,
},
props: {
value: {
type: String as PropType<string>,
},
api: {
type: Function as PropType<(arg: Recordable) => Promise<OptionsItem[]>>,
default: null,
},
params: {
type: Object as PropType<Recordable>,
default: () => {},
},
resultField: {
type: String as PropType<string>,
default: '',
},
},
setup(props) {
const options = ref<OptionsItem[]>([]);
const loading = ref(false);
const attrs = useAttrs();
const { t } = useI18n();
// Embedded in the form, just use the hook binding to perform form verification
const [state] = useRuleFormItem(props);
watchEffect(() => {
fetch();
});
async function fetch() {
const api = props.api;
if (!api || !isFunction(api)) return;
try {
loading.value = true;
const res = await api(props.params);
if (Array.isArray(res)) {
options.value = res;
return;
}
if (props.resultField) {
options.value = get(res, props.resultField) || [];
}
} catch (error) {
console.warn(error);
} finally {
loading.value = false;
}
}
return { state, attrs, options, loading, t };
},
});
</script>

View File

@@ -3,7 +3,6 @@ import type { FormActionType, FormProps } from '../types/form';
import type { FormSchema } from '../types/form';
import type { ValidationRule } from 'ant-design-vue/lib/form/Form';
import type { TableActionType } from '/@/components/Table';
import type { ComponentType } from '../types';
import { defineComponent, computed, unref, toRefs } from 'vue';
import { Form, Col } from 'ant-design-vue';
@@ -16,7 +15,6 @@ import { createPlaceholderMessage, setComponentRuleType } from '../helper';
import { upperFirst, cloneDeep } from 'lodash-es';
import { useItemLabelWidth } from '../hooks/useLabelWidth';
import { isNumber } from '/@/utils/is';
import { useI18n } from '/@/hooks/web/useI18n';
export default defineComponent({
@@ -81,7 +79,7 @@ export default defineComponent({
if (!isFunction(componentProps)) {
return componentProps;
}
return componentProps({ schema, tableAction, formModel, formActionType }) || {};
return componentProps({ schema, tableAction, formModel, formActionType }) ?? {};
});
const getDisable = computed(() => {
@@ -99,7 +97,7 @@ export default defineComponent({
return disabled;
});
function getShow() {
const getShow = computed(() => {
const { show, ifShow } = props.schema;
const { showAdvancedButton } = props.formProps;
const itemIsAdvanced = showAdvancedButton
@@ -124,7 +122,7 @@ export default defineComponent({
}
isShow = isShow && itemIsAdvanced;
return { isShow, isIfShow };
}
});
function handleRules(): ValidationRule[] {
const {
@@ -171,7 +169,7 @@ export default defineComponent({
}
}
// 最大输入长度规则校验
// Maximum input length rule check
const characterInx = rules.findIndex((val) => val.max);
if (characterInx !== -1 && !rules[characterInx].validator) {
rules[characterInx].message =
@@ -180,20 +178,6 @@ export default defineComponent({
return rules;
}
function handleValue(component: ComponentType, field: string) {
const val = props.formModel[field];
if (['Input', 'InputPassword', 'InputSearch', 'InputTextArea'].includes(component)) {
if (val && isNumber(val)) {
props.setFormModel(field, `${val}`);
// props.formModel[field] = `${val}`;
return `${val}`;
}
return val;
}
return val;
}
function renderComponent() {
const {
renderComponentContent,
@@ -217,7 +201,6 @@ export default defineComponent({
const value = target ? (isCheck ? target.checked : target.value) : e;
props.setFormModel(field, value);
// props.formModel[field] = value;
},
};
const Comp = componentMap.get(component) as typeof defineComponent;
@@ -233,7 +216,7 @@ export default defineComponent({
const isCreatePlaceholder = !propsData.disabled && autoSetPlaceHolder;
let placeholder;
// RangePicker place为数组
// RangePicker place is an array
if (isCreatePlaceholder && component !== 'RangePicker' && component) {
placeholder = unref(getComponentsProps)?.placeholder || createPlaceholderMessage(component);
}
@@ -242,7 +225,7 @@ export default defineComponent({
propsData.formValues = unref(getValues);
const bindValue: Recordable = {
[valueField || (isCheck ? 'checked' : 'value')]: handleValue(component, field),
[valueField || (isCheck ? 'checked' : 'value')]: props.formModel[field],
};
const compAttr: Recordable = {
@@ -284,7 +267,7 @@ export default defineComponent({
}
function renderItem() {
const { itemProps, slot, render, field } = props.schema;
const { itemProps, slot, render, field, suffix } = props.schema;
const { labelCol, wrapperCol } = unref(itemLabelWidthProp);
const { colon } = props.formProps;
@@ -296,17 +279,27 @@ export default defineComponent({
: renderComponent();
};
const showSuffix = !!suffix;
const getSuffix = isFunction(suffix) ? suffix(unref(getValues)) : suffix;
return (
<Form.Item
name={field}
colon={colon}
class={{ 'suffix-item': showSuffix }}
{...(itemProps as Recordable)}
label={renderLabelHelpMessage()}
rules={handleRules()}
labelCol={labelCol}
wrapperCol={wrapperCol}
>
{() => getContent()}
{() => (
<>
{getContent()}
{showSuffix && <span class="suffix">{getSuffix}</span>}
</>
)}
</Form.Item>
);
}
@@ -317,7 +310,7 @@ export default defineComponent({
const { baseColProps = {} } = props.formProps;
const realColProps = { ...baseColProps, ...colProps };
const { isIfShow, isShow } = getShow();
const { isIfShow, isShow } = unref(getShow);
const getContent = () => {
return colSlot

View File

@@ -1,6 +1,7 @@
import type { ValidationRule } from 'ant-design-vue/lib/form/Form';
import type { ComponentType } from './types/index';
import { useI18n } from '/@/hooks/web/useI18n';
import { isNumber } from '/@/utils/is';
const { t } = useI18n();
@@ -41,6 +42,14 @@ export function setComponentRuleType(rule: ValidationRule, component: ComponentT
}
}
export function handleInputNumberValue(component?: ComponentType, val: any) {
if (!component) return val;
if (['Input', 'InputPassword', 'InputSearch', 'InputTextArea'].includes(component)) {
return val && isNumber(val) ? `${val}` : val;
}
return val;
}
/**
* 时间字段
*/

View File

@@ -1,6 +1,6 @@
import type { ColEx } from '../types';
import type { AdvanceState } from '../types/hooks';
import { ComputedRef, Ref } from 'vue';
import type { ComputedRef, Ref } from 'vue';
import type { FormProps, FormSchema } from '../types/form';
import { computed, unref, watch } from 'vue';

View File

@@ -0,0 +1,34 @@
import type { ComputedRef, Ref } from 'vue';
import type { FormSchema, FormActionType } from '../types/form';
import { unref, nextTick, watchEffect } from 'vue';
interface UseAutoFocusContext {
getSchema: ComputedRef<FormSchema[]>;
autoFocusFirstItem: Ref<boolean>;
isInitedDefault: Ref<boolean>;
formElRef: Ref<FormActionType>;
}
export async function useAutoFocus({
getSchema,
autoFocusFirstItem,
formElRef,
isInitedDefault,
}: UseAutoFocusContext) {
watchEffect(async () => {
if (unref(isInitedDefault) || !unref(autoFocusFirstItem)) return;
await nextTick();
const schemas = unref(getSchema);
const formEl = unref(formElRef);
const el = (formEl as any)?.$el as HTMLElement;
if (!formEl || !el || !schemas || schemas.length === 0) return;
const firstItem = schemas[0];
// Only open when the first form item is input type
if (!firstItem.component.includes('Input')) return;
const inputEl = el.querySelector('.ant-row:first-child input') as Nullable<HTMLInputElement>;
if (!inputEl) return;
inputEl?.focus();
});
}

View File

@@ -6,7 +6,7 @@ import { unref, toRaw } from 'vue';
import { isArray, isFunction, isObject, isString } from '/@/utils/is';
import { deepMerge, unique } from '/@/utils';
import { dateItemType } from '../helper';
import { dateItemType, handleInputNumberValue } from '../helper';
import moment from 'moment';
import { cloneDeep } from 'lodash-es';
import { error } from '/@/utils/log';
@@ -49,29 +49,32 @@ export function useFormEvents({
/**
* @description: Set form value
*/
async function setFieldsValue(values: any): Promise<void> {
async function setFieldsValue(values: Recordable): Promise<void> {
const fields = unref(getSchema)
.map((item) => item.field)
.filter(Boolean);
const validKeys: string[] = [];
Object.keys(values).forEach((key) => {
const element = values[key];
const schema = unref(getSchema).find((item) => item.field === key);
let value = values[key];
value = handleInputNumberValue(schema?.component, value);
// 0| '' is allow
if (element !== undefined && element !== null && fields.includes(key)) {
if (value !== undefined && value !== null && fields.includes(key)) {
// time type
if (itemIsDateType(key)) {
if (Array.isArray(element)) {
const arr: any[] = [];
for (const ele of element) {
if (Array.isArray(value)) {
const arr: moment.Moment[] = [];
for (const ele of value) {
arr.push(moment(ele));
}
formModel[key] = arr;
} else {
formModel[key] = moment(element);
formModel[key] = moment(value);
}
} else {
formModel[key] = element;
formModel[key] = value;
}
validKeys.push(key);
}

View File

@@ -65,6 +65,8 @@ export const basicProps = {
actionColOptions: Object as PropType<Partial<ColEx>>,
// 显示重置按钮
showResetButton: propTypes.bool.def(true),
// 是否聚焦第一个输入框只在第一个表单项为input的时候作用
autoFocusFirstItem: propTypes.bool,
// 重置按钮配置
resetButtonOptions: Object as PropType<Partial<ButtonProps>>,

View File

@@ -82,6 +82,8 @@ export interface FormProps {
rulesMessageJoinLabel?: boolean;
// Whether to show collapse and expand buttons
showAdvancedButton?: boolean;
// Whether to focus on the first input box, only works when the first form item is input
autoFocusFirstItem?: boolean;
// Automatically collapse over the specified number of rows
autoAdvancedLine?: number;
// Whether to show the operation button
@@ -139,6 +141,8 @@ export interface FormSchema {
// Required
required?: boolean;
suffix?: string | number | ((values: RenderCallbackParams) => string | number);
// Validation rules
rules?: Rule[];
// Check whether the information is added to the label

View File

@@ -89,6 +89,7 @@ export type ComponentType =
| 'InputNumber'
| 'InputCountDown'
| 'Select'
| 'ApiSelect'
| 'SelectOptGroup'
| 'SelectOption'
| 'TreeSelect'