mirror of
https://github.com/vbenjs/vue-vben-admin.git
synced 2025-08-27 14:13:40 +08:00
perf(form): improve the form function
This commit is contained in:
@@ -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>
|
||||
|
@@ -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);
|
||||
|
89
src/components/Form/src/components/ApiSelect.vue
Normal file
89
src/components/Form/src/components/ApiSelect.vue
Normal 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>
|
@@ -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
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 时间字段
|
||||
*/
|
||||
|
@@ -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';
|
||||
|
34
src/components/Form/src/hooks/useAutoFocus.ts
Normal file
34
src/components/Form/src/hooks/useAutoFocus.ts
Normal 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();
|
||||
});
|
||||
}
|
@@ -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);
|
||||
}
|
||||
|
@@ -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>>,
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -89,6 +89,7 @@ export type ComponentType =
|
||||
| 'InputNumber'
|
||||
| 'InputCountDown'
|
||||
| 'Select'
|
||||
| 'ApiSelect'
|
||||
| 'SelectOptGroup'
|
||||
| 'SelectOption'
|
||||
| 'TreeSelect'
|
||||
|
Reference in New Issue
Block a user