Commit ac1a36950259844822c6300a00710b040dfc2640

Authored by vben
1 parent 4ff1c408

perf(form): improve the form function

CHANGELOG.zh_CN.md
@@ -8,6 +8,9 @@ @@ -8,6 +8,9 @@
8 - 新增主框架外页面示例 8 - 新增主框架外页面示例
9 - `route.meta` 新增`currentActiveMenu`,`hideTab`,`hideMenu`参数 用于控制详情页面包屑级菜单显示隐藏。 9 - `route.meta` 新增`currentActiveMenu`,`hideTab`,`hideMenu`参数 用于控制详情页面包屑级菜单显示隐藏。
10 - 新增面包屑导航示例 10 - 新增面包屑导航示例
  11 +- form: 新增`suffix`属性,用于配置后缀内容
  12 +- form: 新增远程下拉`ApiSelect`及示例
  13 +- form: 新增`autoFocusFirstItem`配置。用于配置是否聚焦表单第一个输入框
11 14
12 ### 🐛 Bug Fixes 15 ### 🐛 Bug Fixes
13 16
mock/demo/select-demo.ts 0 → 100644
  1 +import { MockMethod } from 'vite-plugin-mock';
  2 +import { resultSuccess } from '../_util';
  3 +
  4 +const demoList = (() => {
  5 + const result: any[] = [];
  6 + for (let index = 0; index < 20; index++) {
  7 + result.push({
  8 + label: `选项${index}`,
  9 + value: `${index}`,
  10 + });
  11 + }
  12 + return result;
  13 +})();
  14 +
  15 +export default [
  16 + {
  17 + url: '/api/select/getDemoOptions',
  18 + timeout: 4000,
  19 + method: 'get',
  20 + response: ({ query }) => {
  21 + return resultSuccess(demoList);
  22 + },
  23 + },
  24 +] as MockMethod[];
package.json
@@ -22,7 +22,7 @@ @@ -22,7 +22,7 @@
22 }, 22 },
23 "dependencies": { 23 "dependencies": {
24 "@iconify/iconify": "^2.0.0-rc.4", 24 "@iconify/iconify": "^2.0.0-rc.4",
25 - "@vueuse/core": "^4.0.0", 25 + "@vueuse/core": "^4.0.1",
26 "ant-design-vue": "^2.0.0-rc.5", 26 "ant-design-vue": "^2.0.0-rc.5",
27 "apexcharts": "^3.23.0", 27 "apexcharts": "^3.23.0",
28 "axios": "^0.21.1", 28 "axios": "^0.21.1",
@@ -35,7 +35,7 @@ @@ -35,7 +35,7 @@
35 "path-to-regexp": "^6.2.0", 35 "path-to-regexp": "^6.2.0",
36 "qrcode": "^1.4.4", 36 "qrcode": "^1.4.4",
37 "sortablejs": "^1.12.0", 37 "sortablejs": "^1.12.0",
38 - "vditor": "^3.7.3", 38 + "vditor": "^3.7.4",
39 "vue": "^3.0.4", 39 "vue": "^3.0.4",
40 "vue-i18n": "9.0.0-beta.14", 40 "vue-i18n": "9.0.0-beta.14",
41 "vue-router": "^4.0.1", 41 "vue-router": "^4.0.1",
@@ -48,7 +48,7 @@ @@ -48,7 +48,7 @@
48 "devDependencies": { 48 "devDependencies": {
49 "@commitlint/cli": "^11.0.0", 49 "@commitlint/cli": "^11.0.0",
50 "@commitlint/config-conventional": "^11.0.0", 50 "@commitlint/config-conventional": "^11.0.0",
51 - "@iconify/json": "^1.1.276", 51 + "@iconify/json": "^1.1.277",
52 "@ls-lint/ls-lint": "^1.9.2", 52 "@ls-lint/ls-lint": "^1.9.2",
53 "@purge-icons/generated": "^0.4.1", 53 "@purge-icons/generated": "^0.4.1",
54 "@types/echarts": "^4.9.3", 54 "@types/echarts": "^4.9.3",
src/api/demo/model/optionsModel.ts 0 → 100644
  1 +import { BasicFetchResult } from '/@/api/model/baseModel';
  2 +
  3 +export interface DemoOptionsItem {
  4 + label: string;
  5 + value: string;
  6 +}
  7 +
  8 +/**
  9 + * @description: Request list return value
  10 + */
  11 +export type DemoOptionsGetResultModel = BasicFetchResult<DemoOptionsItem[]>;
src/api/demo/select.ts 0 → 100644
  1 +import { defHttp } from '/@/utils/http/axios';
  2 +import { DemoOptionsGetResultModel } from './model/optionsModel';
  3 +
  4 +enum Api {
  5 + OPTIONS_LIST = '/select/getDemoOptions',
  6 +}
  7 +
  8 +/**
  9 + * @description: Get sample options value
  10 + */
  11 +export function optionsListApi() {
  12 + return defHttp.request<DemoOptionsGetResultModel>({
  13 + url: Api.OPTIONS_LIST,
  14 + method: 'GET',
  15 + });
  16 +}
src/components/Form/src/BasicForm.vue
1 <template> 1 <template>
2 - <Form v-bind="{ ...$attrs, ...$props }" ref="formElRef" :model="formModel">  
3 - <Row :class="getProps.compact ? 'compact-form-row' : ''" :style="getRowWrapStyle"> 2 + <Form v-bind="{ ...$attrs, ...$props }" :class="getFormClass" ref="formElRef" :model="formModel">
  3 + <Row :style="getRowWrapStyle">
4 <slot name="formHeader" /> 4 <slot name="formHeader" />
5 <template v-for="schema in getSchema" :key="schema.field"> 5 <template v-for="schema in getSchema" :key="schema.field">
6 <FormItem 6 <FormItem
@@ -18,7 +18,6 @@ @@ -18,7 +18,6 @@
18 </FormItem> 18 </FormItem>
19 </template> 19 </template>
20 20
21 - <!-- -->  
22 <FormAction 21 <FormAction
23 v-bind="{ ...getProps, ...advanceState }" 22 v-bind="{ ...getProps, ...advanceState }"
24 @toggle-advanced="handleToggleAdvanced" 23 @toggle-advanced="handleToggleAdvanced"
@@ -46,8 +45,10 @@ @@ -46,8 +45,10 @@
46 import useAdvanced from './hooks/useAdvanced'; 45 import useAdvanced from './hooks/useAdvanced';
47 import { useFormEvents } from './hooks/useFormEvents'; 46 import { useFormEvents } from './hooks/useFormEvents';
48 import { createFormContext } from './hooks/useFormContext'; 47 import { createFormContext } from './hooks/useFormContext';
  48 + import { useAutoFocus } from './hooks/useAutoFocus';
49 49
50 import { basicProps } from './props'; 50 import { basicProps } from './props';
  51 + import { useDesign } from '/@/hooks/web/useDesign';
51 52
52 export default defineComponent({ 53 export default defineComponent({
53 name: 'BasicForm', 54 name: 'BasicForm',
@@ -71,6 +72,8 @@ @@ -71,6 +72,8 @@
71 const schemaRef = ref<Nullable<FormSchema[]>>(null); 72 const schemaRef = ref<Nullable<FormSchema[]>>(null);
72 const formElRef = ref<Nullable<FormActionType>>(null); 73 const formElRef = ref<Nullable<FormActionType>>(null);
73 74
  75 + const { prefixCls } = useDesign('basic-form');
  76 +
74 // Get the basic configuration of the form 77 // Get the basic configuration of the form
75 const getProps = computed( 78 const getProps = computed(
76 (): FormProps => { 79 (): FormProps => {
@@ -78,6 +81,15 @@ @@ -78,6 +81,15 @@
78 } 81 }
79 ); 82 );
80 83
  84 + const getFormClass = computed(() => {
  85 + return [
  86 + prefixCls,
  87 + {
  88 + [`${prefixCls}--compact`]: unref(getProps).compact,
  89 + },
  90 + ];
  91 + });
  92 +
81 // Get uniform row style 93 // Get uniform row style
82 const getRowWrapStyle = computed( 94 const getRowWrapStyle = computed(
83 (): CSSProperties => { 95 (): CSSProperties => {
@@ -115,7 +127,7 @@ @@ -115,7 +127,7 @@
115 defaultValueRef, 127 defaultValueRef,
116 }); 128 });
117 129
118 - const { transformDateFunc, fieldMapToTime } = toRefs(props); 130 + const { transformDateFunc, fieldMapToTime, autoFocusFirstItem } = toRefs(props);
119 131
120 const { handleFormValues, initDefault } = useFormValues({ 132 const { handleFormValues, initDefault } = useFormValues({
121 transformDateFuncRef: transformDateFunc, 133 transformDateFuncRef: transformDateFunc,
@@ -125,6 +137,13 @@ @@ -125,6 +137,13 @@
125 formModel, 137 formModel,
126 }); 138 });
127 139
  140 + useAutoFocus({
  141 + getSchema,
  142 + autoFocusFirstItem,
  143 + isInitedDefault: isInitedDefaultRef,
  144 + formElRef: formElRef as Ref<FormActionType>,
  145 + });
  146 +
128 const { 147 const {
129 handleSubmit, 148 handleSubmit,
130 setFieldsValue, 149 setFieldsValue,
@@ -217,8 +236,51 @@ @@ -217,8 +236,51 @@
217 getSchema, 236 getSchema,
218 formActionType, 237 formActionType,
219 setFormModel, 238 setFormModel,
  239 + prefixCls,
  240 + getFormClass,
220 ...formActionType, 241 ...formActionType,
221 }; 242 };
222 }, 243 },
223 }); 244 });
224 </script> 245 </script>
  246 +<style lang="less">
  247 + @import (reference) '../../../design/index.less';
  248 + @prefix-cls: ~'@{namespace}-basic-form';
  249 +
  250 + .@{prefix-cls} {
  251 + .ant-form-item {
  252 + &-label label::after {
  253 + margin: 0 6px 0 2px;
  254 + }
  255 +
  256 + &-with-help {
  257 + margin-bottom: 0;
  258 + }
  259 +
  260 + &:not(.ant-form-item-with-help) {
  261 + margin-bottom: 20px;
  262 + }
  263 +
  264 + &.suffix-item {
  265 + .ant-form-item-children {
  266 + display: flex;
  267 + }
  268 +
  269 + .suffix {
  270 + display: inline-block;
  271 + padding-left: 6px;
  272 + }
  273 + }
  274 + }
  275 +
  276 + .ant-form-explain {
  277 + font-size: 14px;
  278 + }
  279 +
  280 + &--compact {
  281 + .ant-form-item {
  282 + margin-bottom: 8px;
  283 + }
  284 + }
  285 + }
  286 +</style>
src/components/Form/src/componentMap.ts
@@ -19,6 +19,7 @@ import { @@ -19,6 +19,7 @@ import {
19 } from 'ant-design-vue'; 19 } from 'ant-design-vue';
20 20
21 import RadioButtonGroup from './components/RadioButtonGroup.vue'; 21 import RadioButtonGroup from './components/RadioButtonGroup.vue';
  22 +import ApiSelect from './components/ApiSelect.vue';
22 import { BasicUpload } from '/@/components/Upload'; 23 import { BasicUpload } from '/@/components/Upload';
23 24
24 const componentMap = new Map<ComponentType, Component>(); 25 const componentMap = new Map<ComponentType, Component>();
@@ -32,6 +33,7 @@ componentMap.set(&#39;InputNumber&#39;, InputNumber); @@ -32,6 +33,7 @@ componentMap.set(&#39;InputNumber&#39;, InputNumber);
32 componentMap.set('AutoComplete', AutoComplete); 33 componentMap.set('AutoComplete', AutoComplete);
33 34
34 componentMap.set('Select', Select); 35 componentMap.set('Select', Select);
  36 +componentMap.set('ApiSelect', ApiSelect);
35 // componentMap.set('SelectOptGroup', Select.OptGroup); 37 // componentMap.set('SelectOptGroup', Select.OptGroup);
36 // componentMap.set('SelectOption', Select.Option); 38 // componentMap.set('SelectOption', Select.Option);
37 componentMap.set('TreeSelect', TreeSelect); 39 componentMap.set('TreeSelect', TreeSelect);
src/components/Form/src/components/ApiSelect.vue 0 → 100644
  1 +<template>
  2 + <Select v-bind="attrs" :options="options" v-model:value="state">
  3 + <template #[item]="data" v-for="item in Object.keys($slots)">
  4 + <slot :name="item" v-bind="data" />
  5 + </template>
  6 + <template #suffixIcon v-if="loading">
  7 + <LoadingOutlined spin />
  8 + </template>
  9 + <template #notFoundContent v-if="loading">
  10 + <span>
  11 + <LoadingOutlined spin class="mr-1" />
  12 + {{ t('component.form.apiSelectNotFound') }}
  13 + </span>
  14 + </template>
  15 + </Select>
  16 +</template>
  17 +<script lang="ts">
  18 + import { defineComponent, PropType, ref, watchEffect } from 'vue';
  19 + import { Select } from 'ant-design-vue';
  20 + import { isFunction } from '/@/utils/is';
  21 + import { useRuleFormItem } from '/@/hooks/component/useFormItem';
  22 + import { useAttrs } from '/@/hooks/core/useAttrs';
  23 + import { get } from 'lodash-es';
  24 +
  25 + import { LoadingOutlined } from '@ant-design/icons-vue';
  26 + import { useI18n } from '/@/hooks/web/useI18n';
  27 +
  28 + type OptionsItem = { label: string; value: string; disabled?: boolean };
  29 +
  30 + export default defineComponent({
  31 + name: 'RadioButtonGroup',
  32 + components: {
  33 + Select,
  34 + LoadingOutlined,
  35 + },
  36 + props: {
  37 + value: {
  38 + type: String as PropType<string>,
  39 + },
  40 + api: {
  41 + type: Function as PropType<(arg: Recordable) => Promise<OptionsItem[]>>,
  42 + default: null,
  43 + },
  44 + params: {
  45 + type: Object as PropType<Recordable>,
  46 + default: () => {},
  47 + },
  48 + resultField: {
  49 + type: String as PropType<string>,
  50 + default: '',
  51 + },
  52 + },
  53 + setup(props) {
  54 + const options = ref<OptionsItem[]>([]);
  55 + const loading = ref(false);
  56 + const attrs = useAttrs();
  57 + const { t } = useI18n();
  58 +
  59 + // Embedded in the form, just use the hook binding to perform form verification
  60 + const [state] = useRuleFormItem(props);
  61 +
  62 + watchEffect(() => {
  63 + fetch();
  64 + });
  65 +
  66 + async function fetch() {
  67 + const api = props.api;
  68 + if (!api || !isFunction(api)) return;
  69 +
  70 + try {
  71 + loading.value = true;
  72 + const res = await api(props.params);
  73 + if (Array.isArray(res)) {
  74 + options.value = res;
  75 + return;
  76 + }
  77 + if (props.resultField) {
  78 + options.value = get(res, props.resultField) || [];
  79 + }
  80 + } catch (error) {
  81 + console.warn(error);
  82 + } finally {
  83 + loading.value = false;
  84 + }
  85 + }
  86 + return { state, attrs, options, loading, t };
  87 + },
  88 + });
  89 +</script>
src/components/Form/src/components/FormItem.tsx
@@ -3,7 +3,6 @@ import type { FormActionType, FormProps } from &#39;../types/form&#39;; @@ -3,7 +3,6 @@ import type { FormActionType, FormProps } from &#39;../types/form&#39;;
3 import type { FormSchema } from '../types/form'; 3 import type { FormSchema } from '../types/form';
4 import type { ValidationRule } from 'ant-design-vue/lib/form/Form'; 4 import type { ValidationRule } from 'ant-design-vue/lib/form/Form';
5 import type { TableActionType } from '/@/components/Table'; 5 import type { TableActionType } from '/@/components/Table';
6 -import type { ComponentType } from '../types';  
7 6
8 import { defineComponent, computed, unref, toRefs } from 'vue'; 7 import { defineComponent, computed, unref, toRefs } from 'vue';
9 import { Form, Col } from 'ant-design-vue'; 8 import { Form, Col } from 'ant-design-vue';
@@ -16,7 +15,6 @@ import { createPlaceholderMessage, setComponentRuleType } from &#39;../helper&#39;; @@ -16,7 +15,6 @@ import { createPlaceholderMessage, setComponentRuleType } from &#39;../helper&#39;;
16 import { upperFirst, cloneDeep } from 'lodash-es'; 15 import { upperFirst, cloneDeep } from 'lodash-es';
17 16
18 import { useItemLabelWidth } from '../hooks/useLabelWidth'; 17 import { useItemLabelWidth } from '../hooks/useLabelWidth';
19 -import { isNumber } from '/@/utils/is';  
20 import { useI18n } from '/@/hooks/web/useI18n'; 18 import { useI18n } from '/@/hooks/web/useI18n';
21 19
22 export default defineComponent({ 20 export default defineComponent({
@@ -81,7 +79,7 @@ export default defineComponent({ @@ -81,7 +79,7 @@ export default defineComponent({
81 if (!isFunction(componentProps)) { 79 if (!isFunction(componentProps)) {
82 return componentProps; 80 return componentProps;
83 } 81 }
84 - return componentProps({ schema, tableAction, formModel, formActionType }) || {}; 82 + return componentProps({ schema, tableAction, formModel, formActionType }) ?? {};
85 }); 83 });
86 84
87 const getDisable = computed(() => { 85 const getDisable = computed(() => {
@@ -99,7 +97,7 @@ export default defineComponent({ @@ -99,7 +97,7 @@ export default defineComponent({
99 return disabled; 97 return disabled;
100 }); 98 });
101 99
102 - function getShow() { 100 + const getShow = computed(() => {
103 const { show, ifShow } = props.schema; 101 const { show, ifShow } = props.schema;
104 const { showAdvancedButton } = props.formProps; 102 const { showAdvancedButton } = props.formProps;
105 const itemIsAdvanced = showAdvancedButton 103 const itemIsAdvanced = showAdvancedButton
@@ -124,7 +122,7 @@ export default defineComponent({ @@ -124,7 +122,7 @@ export default defineComponent({
124 } 122 }
125 isShow = isShow && itemIsAdvanced; 123 isShow = isShow && itemIsAdvanced;
126 return { isShow, isIfShow }; 124 return { isShow, isIfShow };
127 - } 125 + });
128 126
129 function handleRules(): ValidationRule[] { 127 function handleRules(): ValidationRule[] {
130 const { 128 const {
@@ -171,7 +169,7 @@ export default defineComponent({ @@ -171,7 +169,7 @@ export default defineComponent({
171 } 169 }
172 } 170 }
173 171
174 - // 最大输入长度规则校验 172 + // Maximum input length rule check
175 const characterInx = rules.findIndex((val) => val.max); 173 const characterInx = rules.findIndex((val) => val.max);
176 if (characterInx !== -1 && !rules[characterInx].validator) { 174 if (characterInx !== -1 && !rules[characterInx].validator) {
177 rules[characterInx].message = 175 rules[characterInx].message =
@@ -180,20 +178,6 @@ export default defineComponent({ @@ -180,20 +178,6 @@ export default defineComponent({
180 return rules; 178 return rules;
181 } 179 }
182 180
183 - function handleValue(component: ComponentType, field: string) {  
184 - const val = props.formModel[field];  
185 - if (['Input', 'InputPassword', 'InputSearch', 'InputTextArea'].includes(component)) {  
186 - if (val && isNumber(val)) {  
187 - props.setFormModel(field, `${val}`);  
188 -  
189 - // props.formModel[field] = `${val}`;  
190 - return `${val}`;  
191 - }  
192 - return val;  
193 - }  
194 - return val;  
195 - }  
196 -  
197 function renderComponent() { 181 function renderComponent() {
198 const { 182 const {
199 renderComponentContent, 183 renderComponentContent,
@@ -217,7 +201,6 @@ export default defineComponent({ @@ -217,7 +201,6 @@ export default defineComponent({
217 201
218 const value = target ? (isCheck ? target.checked : target.value) : e; 202 const value = target ? (isCheck ? target.checked : target.value) : e;
219 props.setFormModel(field, value); 203 props.setFormModel(field, value);
220 - // props.formModel[field] = value;  
221 }, 204 },
222 }; 205 };
223 const Comp = componentMap.get(component) as typeof defineComponent; 206 const Comp = componentMap.get(component) as typeof defineComponent;
@@ -233,7 +216,7 @@ export default defineComponent({ @@ -233,7 +216,7 @@ export default defineComponent({
233 216
234 const isCreatePlaceholder = !propsData.disabled && autoSetPlaceHolder; 217 const isCreatePlaceholder = !propsData.disabled && autoSetPlaceHolder;
235 let placeholder; 218 let placeholder;
236 - // RangePicker place为数组 219 + // RangePicker place is an array
237 if (isCreatePlaceholder && component !== 'RangePicker' && component) { 220 if (isCreatePlaceholder && component !== 'RangePicker' && component) {
238 placeholder = unref(getComponentsProps)?.placeholder || createPlaceholderMessage(component); 221 placeholder = unref(getComponentsProps)?.placeholder || createPlaceholderMessage(component);
239 } 222 }
@@ -242,7 +225,7 @@ export default defineComponent({ @@ -242,7 +225,7 @@ export default defineComponent({
242 propsData.formValues = unref(getValues); 225 propsData.formValues = unref(getValues);
243 226
244 const bindValue: Recordable = { 227 const bindValue: Recordable = {
245 - [valueField || (isCheck ? 'checked' : 'value')]: handleValue(component, field), 228 + [valueField || (isCheck ? 'checked' : 'value')]: props.formModel[field],
246 }; 229 };
247 230
248 const compAttr: Recordable = { 231 const compAttr: Recordable = {
@@ -284,7 +267,7 @@ export default defineComponent({ @@ -284,7 +267,7 @@ export default defineComponent({
284 } 267 }
285 268
286 function renderItem() { 269 function renderItem() {
287 - const { itemProps, slot, render, field } = props.schema; 270 + const { itemProps, slot, render, field, suffix } = props.schema;
288 const { labelCol, wrapperCol } = unref(itemLabelWidthProp); 271 const { labelCol, wrapperCol } = unref(itemLabelWidthProp);
289 const { colon } = props.formProps; 272 const { colon } = props.formProps;
290 273
@@ -296,17 +279,27 @@ export default defineComponent({ @@ -296,17 +279,27 @@ export default defineComponent({
296 : renderComponent(); 279 : renderComponent();
297 }; 280 };
298 281
  282 + const showSuffix = !!suffix;
  283 +
  284 + const getSuffix = isFunction(suffix) ? suffix(unref(getValues)) : suffix;
  285 +
299 return ( 286 return (
300 <Form.Item 287 <Form.Item
301 name={field} 288 name={field}
302 colon={colon} 289 colon={colon}
  290 + class={{ 'suffix-item': showSuffix }}
303 {...(itemProps as Recordable)} 291 {...(itemProps as Recordable)}
304 label={renderLabelHelpMessage()} 292 label={renderLabelHelpMessage()}
305 rules={handleRules()} 293 rules={handleRules()}
306 labelCol={labelCol} 294 labelCol={labelCol}
307 wrapperCol={wrapperCol} 295 wrapperCol={wrapperCol}
308 > 296 >
309 - {() => getContent()} 297 + {() => (
  298 + <>
  299 + {getContent()}
  300 + {showSuffix && <span class="suffix">{getSuffix}</span>}
  301 + </>
  302 + )}
310 </Form.Item> 303 </Form.Item>
311 ); 304 );
312 } 305 }
@@ -317,7 +310,7 @@ export default defineComponent({ @@ -317,7 +310,7 @@ export default defineComponent({
317 const { baseColProps = {} } = props.formProps; 310 const { baseColProps = {} } = props.formProps;
318 311
319 const realColProps = { ...baseColProps, ...colProps }; 312 const realColProps = { ...baseColProps, ...colProps };
320 - const { isIfShow, isShow } = getShow(); 313 + const { isIfShow, isShow } = unref(getShow);
321 314
322 const getContent = () => { 315 const getContent = () => {
323 return colSlot 316 return colSlot
src/components/Form/src/helper.ts
1 import type { ValidationRule } from 'ant-design-vue/lib/form/Form'; 1 import type { ValidationRule } from 'ant-design-vue/lib/form/Form';
2 import type { ComponentType } from './types/index'; 2 import type { ComponentType } from './types/index';
3 import { useI18n } from '/@/hooks/web/useI18n'; 3 import { useI18n } from '/@/hooks/web/useI18n';
  4 +import { isNumber } from '/@/utils/is';
4 5
5 const { t } = useI18n(); 6 const { t } = useI18n();
6 7
@@ -41,6 +42,14 @@ export function setComponentRuleType(rule: ValidationRule, component: ComponentT @@ -41,6 +42,14 @@ export function setComponentRuleType(rule: ValidationRule, component: ComponentT
41 } 42 }
42 } 43 }
43 44
  45 +export function handleInputNumberValue(component?: ComponentType, val: any) {
  46 + if (!component) return val;
  47 + if (['Input', 'InputPassword', 'InputSearch', 'InputTextArea'].includes(component)) {
  48 + return val && isNumber(val) ? `${val}` : val;
  49 + }
  50 + return val;
  51 +}
  52 +
44 /** 53 /**
45 * 时间字段 54 * 时间字段
46 */ 55 */
src/components/Form/src/hooks/useAdvanced.ts
1 import type { ColEx } from '../types'; 1 import type { ColEx } from '../types';
2 import type { AdvanceState } from '../types/hooks'; 2 import type { AdvanceState } from '../types/hooks';
3 -import { ComputedRef, Ref } from 'vue'; 3 +import type { ComputedRef, Ref } from 'vue';
4 import type { FormProps, FormSchema } from '../types/form'; 4 import type { FormProps, FormSchema } from '../types/form';
5 5
6 import { computed, unref, watch } from 'vue'; 6 import { computed, unref, watch } from 'vue';
src/components/Form/src/hooks/useAutoFocus.ts 0 → 100644
  1 +import type { ComputedRef, Ref } from 'vue';
  2 +import type { FormSchema, FormActionType } from '../types/form';
  3 +
  4 +import { unref, nextTick, watchEffect } from 'vue';
  5 +
  6 +interface UseAutoFocusContext {
  7 + getSchema: ComputedRef<FormSchema[]>;
  8 + autoFocusFirstItem: Ref<boolean>;
  9 + isInitedDefault: Ref<boolean>;
  10 + formElRef: Ref<FormActionType>;
  11 +}
  12 +export async function useAutoFocus({
  13 + getSchema,
  14 + autoFocusFirstItem,
  15 + formElRef,
  16 + isInitedDefault,
  17 +}: UseAutoFocusContext) {
  18 + watchEffect(async () => {
  19 + if (unref(isInitedDefault) || !unref(autoFocusFirstItem)) return;
  20 + await nextTick();
  21 + const schemas = unref(getSchema);
  22 + const formEl = unref(formElRef);
  23 + const el = (formEl as any)?.$el as HTMLElement;
  24 + if (!formEl || !el || !schemas || schemas.length === 0) return;
  25 +
  26 + const firstItem = schemas[0];
  27 + // Only open when the first form item is input type
  28 + if (!firstItem.component.includes('Input')) return;
  29 +
  30 + const inputEl = el.querySelector('.ant-row:first-child input') as Nullable<HTMLInputElement>;
  31 + if (!inputEl) return;
  32 + inputEl?.focus();
  33 + });
  34 +}
src/components/Form/src/hooks/useFormEvents.ts
@@ -6,7 +6,7 @@ import { unref, toRaw } from &#39;vue&#39;; @@ -6,7 +6,7 @@ import { unref, toRaw } from &#39;vue&#39;;
6 6
7 import { isArray, isFunction, isObject, isString } from '/@/utils/is'; 7 import { isArray, isFunction, isObject, isString } from '/@/utils/is';
8 import { deepMerge, unique } from '/@/utils'; 8 import { deepMerge, unique } from '/@/utils';
9 -import { dateItemType } from '../helper'; 9 +import { dateItemType, handleInputNumberValue } from '../helper';
10 import moment from 'moment'; 10 import moment from 'moment';
11 import { cloneDeep } from 'lodash-es'; 11 import { cloneDeep } from 'lodash-es';
12 import { error } from '/@/utils/log'; 12 import { error } from '/@/utils/log';
@@ -49,29 +49,32 @@ export function useFormEvents({ @@ -49,29 +49,32 @@ export function useFormEvents({
49 /** 49 /**
50 * @description: Set form value 50 * @description: Set form value
51 */ 51 */
52 - async function setFieldsValue(values: any): Promise<void> { 52 + async function setFieldsValue(values: Recordable): Promise<void> {
53 const fields = unref(getSchema) 53 const fields = unref(getSchema)
54 .map((item) => item.field) 54 .map((item) => item.field)
55 .filter(Boolean); 55 .filter(Boolean);
56 56
57 const validKeys: string[] = []; 57 const validKeys: string[] = [];
58 Object.keys(values).forEach((key) => { 58 Object.keys(values).forEach((key) => {
59 - const element = values[key]; 59 + const schema = unref(getSchema).find((item) => item.field === key);
  60 + let value = values[key];
  61 +
  62 + value = handleInputNumberValue(schema?.component, value);
60 // 0| '' is allow 63 // 0| '' is allow
61 - if (element !== undefined && element !== null && fields.includes(key)) { 64 + if (value !== undefined && value !== null && fields.includes(key)) {
62 // time type 65 // time type
63 if (itemIsDateType(key)) { 66 if (itemIsDateType(key)) {
64 - if (Array.isArray(element)) {  
65 - const arr: any[] = [];  
66 - for (const ele of element) { 67 + if (Array.isArray(value)) {
  68 + const arr: moment.Moment[] = [];
  69 + for (const ele of value) {
67 arr.push(moment(ele)); 70 arr.push(moment(ele));
68 } 71 }
69 formModel[key] = arr; 72 formModel[key] = arr;
70 } else { 73 } else {
71 - formModel[key] = moment(element); 74 + formModel[key] = moment(value);
72 } 75 }
73 } else { 76 } else {
74 - formModel[key] = element; 77 + formModel[key] = value;
75 } 78 }
76 validKeys.push(key); 79 validKeys.push(key);
77 } 80 }
src/components/Form/src/props.ts
@@ -65,6 +65,8 @@ export const basicProps = { @@ -65,6 +65,8 @@ export const basicProps = {
65 actionColOptions: Object as PropType<Partial<ColEx>>, 65 actionColOptions: Object as PropType<Partial<ColEx>>,
66 // 显示重置按钮 66 // 显示重置按钮
67 showResetButton: propTypes.bool.def(true), 67 showResetButton: propTypes.bool.def(true),
  68 + // 是否聚焦第一个输入框,只在第一个表单项为input的时候作用
  69 + autoFocusFirstItem: propTypes.bool,
68 // 重置按钮配置 70 // 重置按钮配置
69 resetButtonOptions: Object as PropType<Partial<ButtonProps>>, 71 resetButtonOptions: Object as PropType<Partial<ButtonProps>>,
70 72
src/components/Form/src/types/form.ts
@@ -82,6 +82,8 @@ export interface FormProps { @@ -82,6 +82,8 @@ export interface FormProps {
82 rulesMessageJoinLabel?: boolean; 82 rulesMessageJoinLabel?: boolean;
83 // Whether to show collapse and expand buttons 83 // Whether to show collapse and expand buttons
84 showAdvancedButton?: boolean; 84 showAdvancedButton?: boolean;
  85 + // Whether to focus on the first input box, only works when the first form item is input
  86 + autoFocusFirstItem?: boolean;
85 // Automatically collapse over the specified number of rows 87 // Automatically collapse over the specified number of rows
86 autoAdvancedLine?: number; 88 autoAdvancedLine?: number;
87 // Whether to show the operation button 89 // Whether to show the operation button
@@ -139,6 +141,8 @@ export interface FormSchema { @@ -139,6 +141,8 @@ export interface FormSchema {
139 // Required 141 // Required
140 required?: boolean; 142 required?: boolean;
141 143
  144 + suffix?: string | number | ((values: RenderCallbackParams) => string | number);
  145 +
142 // Validation rules 146 // Validation rules
143 rules?: Rule[]; 147 rules?: Rule[];
144 // Check whether the information is added to the label 148 // Check whether the information is added to the label
src/components/Form/src/types/index.ts
@@ -89,6 +89,7 @@ export type ComponentType = @@ -89,6 +89,7 @@ export type ComponentType =
89 | 'InputNumber' 89 | 'InputNumber'
90 | 'InputCountDown' 90 | 'InputCountDown'
91 | 'Select' 91 | 'Select'
  92 + | 'ApiSelect'
92 | 'SelectOptGroup' 93 | 'SelectOptGroup'
93 | 'SelectOption' 94 | 'SelectOption'
94 | 'TreeSelect' 95 | 'TreeSelect'
src/design/ant/index.less
@@ -49,37 +49,6 @@ @@ -49,37 +49,6 @@
49 } 49 }
50 50
51 // ================================= 51 // =================================
52 -// ==============form===============  
53 -// =================================  
54 -.ant-form-item.deltag .ant-form-item-required::before {  
55 - content: '';  
56 -}  
57 -  
58 -.ant-form-item-with-help {  
59 - margin-bottom: 0;  
60 -}  
61 -  
62 -.ant-form-item {  
63 - &-label label::after {  
64 - margin: 0 6px 0 2px;  
65 - }  
66 -}  
67 -  
68 -.ant-form-item:not(.ant-form-item-with-help) {  
69 - margin-bottom: 20px;  
70 -}  
71 -  
72 -.ant-form-explain {  
73 - font-size: 14px;  
74 -}  
75 -  
76 -.compact-form-row {  
77 - .ant-form-item {  
78 - margin-bottom: 8px;  
79 - }  
80 -}  
81 -  
82 -// =================================  
83 // ==============empty============== 52 // ==============empty==============
84 // ================================= 53 // =================================
85 .ant-empty-image { 54 .ant-empty-image {
src/locales/lang/en/component/form.ts
@@ -8,4 +8,6 @@ export default { @@ -8,4 +8,6 @@ export default {
8 choose: 'Please Choose ', 8 choose: 'Please Choose ',
9 9
10 maxTip: 'The number of characters should be less than {0}', 10 maxTip: 'The number of characters should be less than {0}',
  11 +
  12 + apiSelectNotFound: 'Wait for data loading to complete...',
11 }; 13 };
src/locales/lang/zh_CN/component/form.ts
@@ -8,4 +8,6 @@ export default { @@ -8,4 +8,6 @@ export default {
8 choose: '请选择', 8 choose: '请选择',
9 9
10 maxTip: '字符数应小于{0}位', 10 maxTip: '字符数应小于{0}位',
  11 +
  12 + apiSelectNotFound: '请等待数据加载完成...',
11 }; 13 };
src/utils/http/axios/index.ts
@@ -105,28 +105,29 @@ const transform: AxiosTransform = { @@ -105,28 +105,29 @@ const transform: AxiosTransform = {
105 if (apiUrl && isString(apiUrl)) { 105 if (apiUrl && isString(apiUrl)) {
106 config.url = `${apiUrl}${config.url}`; 106 config.url = `${apiUrl}${config.url}`;
107 } 107 }
  108 + const params = config.params || {};
108 if (config.method?.toUpperCase() === RequestEnum.GET) { 109 if (config.method?.toUpperCase() === RequestEnum.GET) {
109 - if (!isString(config.params)) { 110 + if (!isString(params)) {
110 config.data = { 111 config.data = {
111 // 给 get 请求加上时间戳参数,避免从缓存中拿数据。 112 // 给 get 请求加上时间戳参数,避免从缓存中拿数据。
112 - params: Object.assign(config.params || {}, createNow(joinTime, false)), 113 + params: Object.assign(params || {}, createNow(joinTime, false)),
113 }; 114 };
114 } else { 115 } else {
115 // 兼容restful风格 116 // 兼容restful风格
116 - config.url = config.url + config.params + `${createNow(joinTime, true)}`; 117 + config.url = config.url + params + `${createNow(joinTime, true)}`;
117 config.params = undefined; 118 config.params = undefined;
118 } 119 }
119 } else { 120 } else {
120 - if (!isString(config.params)) {  
121 - formatDate && formatRequestDate(config.params);  
122 - config.data = config.params; 121 + if (!isString(params)) {
  122 + formatDate && formatRequestDate(params);
  123 + config.data = params;
123 config.params = undefined; 124 config.params = undefined;
124 if (joinParamsToUrl) { 125 if (joinParamsToUrl) {
125 config.url = setObjToUrlParams(config.url as string, config.data); 126 config.url = setObjToUrlParams(config.url as string, config.data);
126 } 127 }
127 } else { 128 } else {
128 // 兼容restful风格 129 // 兼容restful风格
129 - config.url = config.url + config.params; 130 + config.url = config.url + params;
130 config.params = undefined; 131 config.params = undefined;
131 } 132 }
132 } 133 }
src/views/demo/form/RuleForm.vue
@@ -170,7 +170,7 @@ @@ -170,7 +170,7 @@
170 } 170 }
171 function setFormValues() { 171 function setFormValues() {
172 setFieldsValue({ 172 setFieldsValue({
173 - field1: '1111', 173 + field1: 1111,
174 field5: ['1'], 174 field5: ['1'],
175 field7: '1', 175 field7: '1',
176 }); 176 });
src/views/demo/form/index.vue
@@ -2,6 +2,7 @@ @@ -2,6 +2,7 @@
2 <div class="m-4"> 2 <div class="m-4">
3 <CollapseContainer title="基础示例"> 3 <CollapseContainer title="基础示例">
4 <BasicForm 4 <BasicForm
  5 + autoFocusFirstItem
5 :labelWidth="100" 6 :labelWidth="100"
6 :schemas="schemas" 7 :schemas="schemas"
7 :actionColOptions="{ span: 24 }" 8 :actionColOptions="{ span: 24 }"
@@ -16,11 +17,13 @@ @@ -16,11 +17,13 @@
16 import { CollapseContainer } from '/@/components/Container/index'; 17 import { CollapseContainer } from '/@/components/Container/index';
17 import { useMessage } from '/@/hooks/web/useMessage'; 18 import { useMessage } from '/@/hooks/web/useMessage';
18 19
  20 + import { optionsListApi } from '/@/api/demo/select';
19 const schemas: FormSchema[] = [ 21 const schemas: FormSchema[] = [
20 { 22 {
21 field: 'field1', 23 field: 'field1',
22 component: 'Input', 24 component: 'Input',
23 label: '字段1', 25 label: '字段1',
  26 +
24 colProps: { 27 colProps: {
25 span: 8, 28 span: 8,
26 }, 29 },
@@ -46,7 +49,7 @@ @@ -46,7 +49,7 @@
46 { 49 {
47 field: 'field2', 50 field: 'field2',
48 component: 'Input', 51 component: 'Input',
49 - label: '字段2', 52 + label: '带后缀',
50 defaultValue: '111', 53 defaultValue: '111',
51 colProps: { 54 colProps: {
52 span: 8, 55 span: 8,
@@ -56,6 +59,7 @@ @@ -56,6 +59,7 @@
56 console.log(e); 59 console.log(e);
57 }, 60 },
58 }, 61 },
  62 + suffix: '天',
59 }, 63 },
60 { 64 {
61 field: 'field3', 65 field: 'field3',
@@ -208,6 +212,19 @@ @@ -208,6 +212,19 @@
208 ], 212 ],
209 }, 213 },
210 }, 214 },
  215 +
  216 + {
  217 + field: 'field30',
  218 + component: 'ApiSelect',
  219 + label: '远程下拉',
  220 + required: true,
  221 + componentProps: {
  222 + api: optionsListApi,
  223 + },
  224 + colProps: {
  225 + span: 8,
  226 + },
  227 + },
211 { 228 {
212 field: 'field20', 229 field: 'field20',
213 component: 'InputNumber', 230 component: 'InputNumber',
yarn.lock
@@ -1076,10 +1076,10 @@ @@ -1076,10 +1076,10 @@
1076 resolved "https://registry.npmjs.org/@iconify/iconify/-/iconify-2.0.0-rc.4.tgz#46098fb544a4eb3af724219e4955c9022801835e" 1076 resolved "https://registry.npmjs.org/@iconify/iconify/-/iconify-2.0.0-rc.4.tgz#46098fb544a4eb3af724219e4955c9022801835e"
1077 integrity sha512-YCSECbeXKFJEIVkKgKMjUzJ439ysufmL/a31B1j7dCvnHaBWsX9J4XehhJgg/aTy3yvhHaVhI6xt1kSMZP799A== 1077 integrity sha512-YCSECbeXKFJEIVkKgKMjUzJ439ysufmL/a31B1j7dCvnHaBWsX9J4XehhJgg/aTy3yvhHaVhI6xt1kSMZP799A==
1078 1078
1079 -"@iconify/json@^1.1.276":  
1080 - version "1.1.276"  
1081 - resolved "https://registry.npmjs.org/@iconify/json/-/json-1.1.276.tgz#c8d51751abc84cc73a466f55bc2f352686451786"  
1082 - integrity sha512-Ra/mGT+n38vhi/i1cjsPYOmSR2d6rNIXZ+OsrIWp9J35zAPQ93sSTQMpTyxZdLu3QxU0vYwtcaC7h/Y1/3H3wg== 1079 +"@iconify/json@^1.1.277":
  1080 + version "1.1.277"
  1081 + resolved "https://registry.npmjs.org/@iconify/json/-/json-1.1.277.tgz#e11e01833b05845ce1afc5ad61759804f6ed2eb2"
  1082 + integrity sha512-66n4lsv57iRwtcb2Q8ax8iasVLzFz9VWcqtgobHVrvyfsVqf8hSldJELnTl/gtqayqa35pT4mHEpdfsqt1mnLA==
1083 1083
1084 "@intlify/core-base@9.0.0-beta.14": 1084 "@intlify/core-base@9.0.0-beta.14":
1085 version "9.0.0-beta.14" 1085 version "9.0.0-beta.14"
@@ -1831,18 +1831,18 @@ @@ -1831,18 +1831,18 @@
1831 vscode-languageserver-textdocument "^1.0.1" 1831 vscode-languageserver-textdocument "^1.0.1"
1832 vscode-uri "^2.1.2" 1832 vscode-uri "^2.1.2"
1833 1833
1834 -"@vueuse/core@^4.0.0":  
1835 - version "4.0.0"  
1836 - resolved "https://registry.npmjs.org/@vueuse/core/-/core-4.0.0.tgz#5bea3eaa848e3b3e00427f5053fb98e7e4834b0f"  
1837 - integrity sha512-BBkqriC2j9SH/LuHCggS2MP7VSwBfGkTB9qQh1lzadodk2TnM1JHwM76f3G0hCGqqhEF7ab8Xs+1M1PlvuEQYA== 1834 +"@vueuse/core@^4.0.1":
  1835 + version "4.0.1"
  1836 + resolved "https://registry.npmjs.org/@vueuse/core/-/core-4.0.1.tgz#be90fd09de0264dbe61c571b5967334ca94d8cb2"
  1837 + integrity sha512-bC6H/ES9aFnzp6rT3W3d5j/CqB8mN1UrvBj1RO639QMwxPbJ5/JDjDD4HHtOdIZfA82d6p2Ijbv4Y04mXmkHng==
1838 dependencies: 1838 dependencies:
1839 - "@vueuse/shared" "4.0.0" 1839 + "@vueuse/shared" "4.0.1"
1840 vue-demi latest 1840 vue-demi latest
1841 1841
1842 -"@vueuse/shared@4.0.0":  
1843 - version "4.0.0"  
1844 - resolved "https://registry.npmjs.org/@vueuse/shared/-/shared-4.0.0.tgz#d495b8fd2f28a453ef0fccae175ca848a4a84bb0"  
1845 - integrity sha512-8tn1BpnaMJU2LqFyFzzN6Dvmc1uDsSlb3Neli5bwwb9f+rcASpuOS3nAWAY6/rIODZP1iwXDNCL4rNFR3YxYtQ== 1842 +"@vueuse/shared@4.0.1":
  1843 + version "4.0.1"
  1844 + resolved "https://registry.npmjs.org/@vueuse/shared/-/shared-4.0.1.tgz#28750d34400cd0cabf2576342c5ee7471b0e27bd"
  1845 + integrity sha512-7SQ1OqUPiuOSe5OFGIn5NvawZ7mfID5V4AwsHwpMAQn22Ex73az6TFE1N/6fL4rZBx6wLrkPfVO9v7vSsOkvlg==
1846 dependencies: 1846 dependencies:
1847 vue-demi latest 1847 vue-demi latest
1848 1848
@@ -8039,10 +8039,10 @@ vary@^1.1.2: @@ -8039,10 +8039,10 @@ vary@^1.1.2:
8039 resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" 8039 resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
8040 integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= 8040 integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=
8041 8041
8042 -vditor@^3.7.3:  
8043 - version "3.7.3"  
8044 - resolved "https://registry.npmjs.org/vditor/-/vditor-3.7.3.tgz#6f7bdee7dca758985b29be1533ed952178f0aac4"  
8045 - integrity sha512-2EHwAc9l+HOo6dcScSJDPmVTsVuEqHK2ucZwAHgvctpua3pMz/CAGMHgPoyB5X1Pju7yrLfsESHZh8V6Ndh6rg== 8042 +vditor@^3.7.4:
  8043 + version "3.7.4"
  8044 + resolved "https://registry.npmjs.org/vditor/-/vditor-3.7.4.tgz#e2ec46f009e99d4ef1804d4ef355d44be7efb9a3"
  8045 + integrity sha512-NfpXCoiVEeaORwGPNaxVDQGHs6Sib2RlI+slSFc5eXV8pFfYM639O6iOLjG2Ks+lN7nM9SsmpcGXwnQ0/S90xA==
8046 dependencies: 8046 dependencies:
8047 diff-match-patch "^1.0.5" 8047 diff-match-patch "^1.0.5"
8048 8048